refactor(welcome-template): drop role param; resolve plugins per-user unconditionally

Removes the `role: Literal["analyst", "admin"] = "admin"` parameter from
`compute_default_agent_prompt`. The same RBAC pass
(`marketplace_filter.resolve_allowed_plugins`) now runs for every user —
admin or not. Users with no `resource_grants` rows get the
no-marketplace layout; users with grants get the marketplace block
inserted. Admin-vs-analyst is no longer a layout branch.

`render_agent_prompt_banner` no longer derives a `role` from
`user.is_admin`; it just delegates to `compute_default_agent_prompt`.
Two `compute_default_agent_prompt(...role=role)` call sites in
`app/web/router.py::setup_page` are updated to drop the keyword so the
route keeps rendering — Task 5 will remove the `?role=` query
parameter and the silent admin-downgrade block from the route signature
itself.

Tests: drop role-aware assertions from test_welcome_template_renderer
and test_welcome_template_api. Both files now assert the unified
default contains `agnes init` + `uv tool install` and bans the legacy
`agnes auth import-token` / `agnes auth whoami` verbs.

Plan: docs/superpowers/plans/2026-05-04-unified-setup-prompt.md task 4.
This commit is contained in:
ZdenekSrotyr 2026-05-04 22:13:46 +02:00
parent 74b7f6e254
commit 291079b1d2
4 changed files with 45 additions and 38 deletions

View file

@ -768,11 +768,11 @@ async def setup_page(
except (TemplateError, Exception) as exc: except (TemplateError, Exception) as exc:
logger.warning("setup_page: override render failed (%s); falling back to default", exc) logger.warning("setup_page: override render failed (%s); falling back to default", exc)
setup_script_text = compute_default_agent_prompt( setup_script_text = compute_default_agent_prompt(
conn, user=user, server_url=base_url, role=role, conn, user=user, server_url=base_url,
) )
else: else:
setup_script_text = compute_default_agent_prompt( setup_script_text = compute_default_agent_prompt(
conn, user=user, server_url=base_url, role=role, conn, user=user, server_url=base_url,
) )
# Split for the legacy setup_instructions_lines list variable that the # Split for the legacy setup_instructions_lines list variable that the

View file

@ -24,7 +24,7 @@ import os
import re import re
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
import duckdb import duckdb
@ -130,21 +130,22 @@ def compute_default_agent_prompt(
*, *,
user: dict[str, Any] | None, user: dict[str, Any] | None,
server_url: str, server_url: str,
role: Literal["analyst", "admin"] = "admin",
) -> str: ) -> str:
"""Return the live default setup script from setup_instructions.resolve_lines(). """Return the live default setup script from setup_instructions.resolve_lines().
This is the full bash bootstrap prompt that /setup shows when no admin This is the unified bash bootstrap prompt that /setup shows when no
override is set. The returned string is bash (not HTML) callers must admin override is set. The returned string is bash (not HTML)
NOT pass it through _sanitize_banner_html. callers must NOT pass it through _sanitize_banner_html.
``conn`` and ``user`` are forwarded to resolve the RBAC-filtered plugin ``conn`` and ``user`` are forwarded to resolve the RBAC-filtered plugin
install list (anonymous visitors / no conn get the no-marketplace layout). install list. The same RBAC pass runs for everyone (admin and
``server_url`` is used to derive the server host for the marketplace block. non-admin alike): users with no plugin grants get the no-marketplace
layout (Confirm = step 6); users with grants get the marketplace + plugins
block inserted (Confirm = step 8). Anonymous visitors / no conn fall
through to the no-marketplace layout.
``role`` selects the layout: ``"admin"`` (default) keeps the existing ``server_url`` is used to derive the server host for the marketplace
full bootstrap, ``"analyst"`` short-circuits to the trimmed analyst block.
workspace flow (no marketplace, plugins forced empty).
""" """
try: try:
from app.web.setup_instructions import resolve_lines from app.web.setup_instructions import resolve_lines
@ -153,12 +154,13 @@ def compute_default_agent_prompt(
_wheel = _find_wheel() _wheel = _find_wheel()
_wheel_filename = _wheel.name if _wheel else "agnes.whl" _wheel_filename = _wheel.name if _wheel else "agnes.whl"
# Analyst flow has no marketplace concept — skip the RBAC plugin # RBAC plugin resolution is unconditional — same code path for
# resolution entirely so the analyst tile renders the same lines for # admin and non-admin. Users with no `resource_grants` rows get an
# everyone (and so resolve_lines's analyst short-circuit fires # empty list and the no-marketplace layout; users with grants get
# regardless of whether the caller has plugin grants). # the marketplace block. Admin-vs-analyst is no longer a layout
# branch.
plugin_install_names: list[str] = [] plugin_install_names: list[str] = []
if role == "admin" and user and conn is not None: if user and conn is not None:
try: try:
from src import marketplace_filter from src import marketplace_filter
plugin_install_names = [ plugin_install_names = [
@ -243,11 +245,10 @@ def render_agent_prompt_banner(
# Fall through to default # Fall through to default
# No override (or broken override) — return live default bash script. # No override (or broken override) — return live default bash script.
# Pick role by user identity: admins get the full CLI install + marketplace # Same unified flow for everyone; admin-vs-analyst is no longer a
# flow (existing behavior). Everyone else gets the analyst workspace # layout branch. The marketplace block is gated by the caller's
# bootstrap. The dashboard CTA hits this path; without role-by-identity, # plugin grants in `resource_grants`, which `compute_default_agent_prompt`
# analysts would get admin instructions they can't actually execute. # resolves unconditionally.
role = "admin" if (user and user.get("is_admin")) else "analyst"
return compute_default_agent_prompt( return compute_default_agent_prompt(
conn, user=user, server_url=server_url, role=role, conn, user=user, server_url=server_url,
) )

View file

@ -38,9 +38,11 @@ def test_admin_get_template_initially_null(seeded_app):
# default field must be present and contain the live setup script # default field must be present and contain the live setup script
assert "default" in body assert "default" in body
assert body["default"] # non-empty assert body["default"] # non-empty
# Admin layout marker — `agnes auth import-token` is the login step. # Unified layout markers — `agnes init` and `uv tool install` are
assert "agnes auth" in body["default"] # mandatory; legacy `agnes auth import-token` is gone.
assert "agnes init" in body["default"]
assert "uv tool install" in body["default"] assert "uv tool install" in body["default"]
assert "agnes auth import-token" not in body["default"]
# No legacy verb in the rendered default # No legacy verb in the rendered default
assert "da analyst setup" not in body["default"] assert "da analyst setup" not in body["default"]

View file

@ -39,35 +39,39 @@ def _user(email="alice@example.com"):
def test_returns_default_script_when_no_override(conn): def test_returns_default_script_when_no_override(conn):
"""When no override is set, render_agent_prompt_banner returns the live """When no override is set, render_agent_prompt_banner returns the live
setup script (not an empty string). unified setup script (not an empty string). Every caller admin or
non-admin sees `agnes init` (the workspace-rails delivery
A non-admin user gets the analyst layout: `agnes init` subsumes auth and mechanism). Whether the marketplace block appears depends on the
the trimmed flow has no `agnes auth` line. An admin user gets the full caller's plugin grants in `resource_grants`, NOT on `is_admin`.
CLI bootstrap with `agnes auth import-token`.
""" """
out = render_agent_prompt_banner(conn, user=_user(), server_url="https://example.com") out = render_agent_prompt_banner(conn, user=_user(), server_url="https://example.com")
# Must be non-empty — the default IS the setup script # Must be non-empty — the default IS the setup script
assert out != "" assert out != ""
# Analyst layout: `agnes init` is the bootstrap step. # Unified layout: `agnes init` is mandatory.
assert "agnes init" in out assert "agnes init" in out
# Legacy admin-only auth verbs are gone — `agnes init` subsumes them.
assert "agnes auth import-token" not in out
assert "agnes auth whoami" not in out
# No legacy verb anywhere in the rendered default # No legacy verb anywhere in the rendered default
assert "da analyst setup" not in out assert "da analyst setup" not in out
assert "da sync" not in out assert "da sync" not in out
def test_compute_default_returns_setup_script(conn): def test_compute_default_returns_setup_script(conn):
"""compute_default_agent_prompt returns a non-empty string with setup """compute_default_agent_prompt returns a non-empty string with the
script markers including {server_url} and agnes commands. unified setup-script markers, including the {server_url} placeholder
and the agnes init line.
Default role is `admin`, which renders the full CLI install + login flow.
""" """
out = compute_default_agent_prompt(conn, user=_user(), server_url="https://example.com") out = compute_default_agent_prompt(conn, user=_user(), server_url="https://example.com")
assert out != "" assert out != ""
# {server_url} placeholder must survive (not replaced by Jinja2) # {server_url} placeholder must survive (not replaced by Jinja2)
assert "{server_url}" in out assert "{server_url}" in out
# Admin layout references the agnes CLI install + login flow # Unified layout: install + init are always present.
assert "agnes auth" in out assert "agnes init" in out
assert "uv tool install" in out assert "uv tool install" in out
# Admin-only auth verbs replaced by `agnes init`.
assert "agnes auth import-token" not in out
assert "agnes auth whoami" not in out
# No legacy verb anywhere in the rendered default # No legacy verb anywhere in the rendered default
assert "da analyst setup" not in out assert "da analyst setup" not in out
@ -237,7 +241,7 @@ def test_render_failure_falls_back_to_default_not_exception(conn):
out = render_agent_prompt_banner(conn, user=_user(), server_url="https://example.com") out = render_agent_prompt_banner(conn, user=_user(), server_url="https://example.com")
# Must not raise — falls back to the live default script (non-empty) # Must not raise — falls back to the live default script (non-empty)
assert out != "" assert out != ""
# Non-admin → analyst layout: `agnes init` is the bootstrap step. # Unified layout: `agnes init` is the bootstrap step regardless of role.
assert "agnes init" in out assert "agnes init" in out
assert "uv tool install" in out assert "uv tool install" in out