feat(setup): add analyst role to install-prompt renderer
This commit is contained in:
parent
59324f9361
commit
29e28ccbd3
2 changed files with 130 additions and 1 deletions
|
|
@ -103,6 +103,8 @@ permitted that fallback chain — it's not improvising-around-a-TLS-error.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
# Marketplace name as published by app.marketplace_server.packager.
|
||||
# Hard-coded here (rather than imported) to keep this module dependency-free
|
||||
# and trivially testable. If the value ever drifts, the regression test
|
||||
|
|
@ -606,6 +608,50 @@ def _preamble_lines(*, has_ca: bool) -> list[str]:
|
|||
return lines
|
||||
|
||||
|
||||
def _analyst_init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
|
||||
"""Steps 2-3 — `agnes init` (auth + workspace bootstrap) + smoke verify.
|
||||
|
||||
Replaces the admin-flow login + verify steps. `agnes init` is non-interactive:
|
||||
`--token` carries the PAT, `--server-url` carries the origin. The bootstrap
|
||||
PAT has a 1 h TTL — if the user takes longer than that to paste this prompt,
|
||||
the init call returns 401 and the user re-clicks "Generate prompt" on the
|
||||
install page.
|
||||
"""
|
||||
return [
|
||||
"",
|
||||
"2) Bootstrap your analyst workspace in this directory:",
|
||||
f" agnes init --server-url \"{server_url_placeholder}\" --token \"{{token}}\" --workspace .",
|
||||
"",
|
||||
" This authenticates with the PAT, fetches your CLAUDE.md (RBAC-filtered),",
|
||||
" installs Claude Code SessionStart/End hooks (auto-refresh), and runs an",
|
||||
" initial `agnes pull` so your DuckDB views are ready.",
|
||||
"",
|
||||
"3) Verify the data is queryable:",
|
||||
" agnes catalog",
|
||||
"",
|
||||
" This should list the tables your account has grants for. Empty list",
|
||||
" means your admin hasn't granted you access yet — contact them.",
|
||||
]
|
||||
|
||||
|
||||
def _analyst_finale_lines(confirm_step_num: str, has_ca: bool) -> list[str]:
|
||||
"""Final Confirm step for analyst role. Shorter than admin: no marketplace, no plugins, no skills."""
|
||||
bullets = [
|
||||
" - `agnes --version` output",
|
||||
" - First few lines of `agnes catalog` (tables you can see)",
|
||||
" - Confirmation that `./CLAUDE.md` and `./AGNES_WORKSPACE.md` exist",
|
||||
" - Confirmation that `./.claude/settings.json` contains SessionStart/End hooks",
|
||||
]
|
||||
if has_ca:
|
||||
bullets.append(" - Which CA bundle source got picked in step 0(d)")
|
||||
return [
|
||||
"",
|
||||
f"{confirm_step_num}) Confirm:",
|
||||
" Tell me \"Agnes analyst workspace is ready\" and summarize:",
|
||||
*bullets,
|
||||
]
|
||||
|
||||
|
||||
def resolve_lines(
|
||||
wheel_filename: str,
|
||||
*,
|
||||
|
|
@ -613,6 +659,7 @@ def resolve_lines(
|
|||
self_signed_tls: bool = False,
|
||||
server_host: str = "",
|
||||
ca_pem: str | None = None,
|
||||
role: Literal["analyst", "admin"] = "admin",
|
||||
) -> list[str]:
|
||||
"""Return the template lines with server-side placeholders substituted.
|
||||
|
||||
|
|
@ -643,7 +690,14 @@ def resolve_lines(
|
|||
The resulting URL (`/cli/wheel/agnes.whl`) will 404 at download time, but
|
||||
the instruction text still renders so operators can see the snippet shape
|
||||
and diagnose the missing wheel on the server.
|
||||
|
||||
`role="analyst"` short-circuits to the analyst-workspace layout
|
||||
(`_resolve_analyst_lines`) — see that function for the layout. Default
|
||||
`role="admin"` keeps the admin layout below byte-identical to before.
|
||||
"""
|
||||
if role == "analyst":
|
||||
return _resolve_analyst_lines(wheel_filename, ca_pem=ca_pem)
|
||||
|
||||
names = list(plugin_install_names or [])
|
||||
has_marketplace = bool(names)
|
||||
has_ca = bool(ca_pem and ca_pem.strip())
|
||||
|
|
@ -693,6 +747,30 @@ def resolve_lines(
|
|||
]
|
||||
|
||||
|
||||
def _resolve_analyst_lines(wheel_filename: str, *, ca_pem: str | None) -> list[str]:
|
||||
"""Analyst workspace-bootstrap layout. Self-contained — no admin-only steps.
|
||||
|
||||
Drops marketplace, plugins, skills, diagnose, login, and whoami (all of
|
||||
those are admin-only or subsumed by `agnes init`). Reuses the trust
|
||||
block, preamble, and install-CLI helpers from the admin path.
|
||||
"""
|
||||
has_ca = bool(ca_pem and ca_pem.strip())
|
||||
confirm_step = "4" # numbering: 0 (TLS optional), 1, 2, 3, 4
|
||||
|
||||
lines: list[str] = []
|
||||
if has_ca:
|
||||
lines.extend(_tls_trust_block(ca_pem)) # type: ignore[arg-type]
|
||||
lines.extend(_preamble_lines(has_ca=has_ca))
|
||||
lines.extend(_install_cli_lines(has_ca=has_ca)) # step 1
|
||||
lines.extend(_analyst_init_lines()) # steps 2-3
|
||||
lines.extend(_analyst_finale_lines(confirm_step, has_ca=has_ca)) # step 4
|
||||
|
||||
return [
|
||||
line.replace("{wheel_filename}", wheel_filename)
|
||||
for line in lines
|
||||
]
|
||||
|
||||
|
||||
def render_setup_instructions(
|
||||
server_url: str,
|
||||
token: str,
|
||||
|
|
@ -702,13 +780,14 @@ def render_setup_instructions(
|
|||
self_signed_tls: bool = False,
|
||||
server_host: str = "",
|
||||
ca_pem: str | None = None,
|
||||
role: Literal["analyst", "admin"] = "admin",
|
||||
) -> str:
|
||||
"""Render the setup instructions as a single string.
|
||||
|
||||
Used server-side for tests and any non-JS rendering path. The browser
|
||||
clipboard flow uses the JS renderer embedded in the Jinja partial; both
|
||||
must produce byte-identical output for a given (server_url, token,
|
||||
wheel, plugins, flag, host, ca_pem) tuple.
|
||||
wheel, plugins, flag, host, ca_pem, role) tuple.
|
||||
"""
|
||||
lines = resolve_lines(
|
||||
wheel_filename,
|
||||
|
|
@ -716,6 +795,7 @@ def render_setup_instructions(
|
|||
self_signed_tls=self_signed_tls,
|
||||
server_host=server_host,
|
||||
ca_pem=ca_pem,
|
||||
role=role,
|
||||
)
|
||||
text = "\n".join(lines)
|
||||
return text.replace("{server_url}", server_url).replace("{token}", token)
|
||||
|
|
|
|||
49
tests/test_setup_instructions_analyst.py
Normal file
49
tests/test_setup_instructions_analyst.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Tests for analyst-branch rendering of /setup paste prompt."""
|
||||
|
||||
from app.web.setup_instructions import render_setup_instructions
|
||||
|
||||
|
||||
def test_render_analyst_role_basic():
|
||||
text = render_setup_instructions(
|
||||
server_url="https://agnes.example.com",
|
||||
token="agnes_pat_TEST",
|
||||
wheel_filename="agnes-0.32.0-py3-none-any.whl",
|
||||
role="analyst",
|
||||
)
|
||||
# Required content for analyst role:
|
||||
assert "uv tool install" in text
|
||||
assert "agnes init" in text
|
||||
assert "--token" in text and "agnes_pat_TEST" in text
|
||||
assert "--server-url" in text and "https://agnes.example.com" in text
|
||||
assert "agnes catalog" in text # smoke verify step
|
||||
# Forbidden content (admin-only):
|
||||
assert "marketplace" not in text
|
||||
assert "claude plugin install" not in text
|
||||
assert "agnes skills install" not in text
|
||||
assert "agnes diagnose" not in text
|
||||
|
||||
|
||||
def test_render_admin_role_unchanged():
|
||||
"""Default role=admin keeps the existing layout."""
|
||||
text = render_setup_instructions(
|
||||
server_url="https://agnes.example.com",
|
||||
token="agnes_pat_TEST",
|
||||
wheel_filename="agnes-0.32.0-py3-none-any.whl",
|
||||
# role omitted — defaults to "admin"
|
||||
)
|
||||
assert "agnes auth import-token" in text # admin uses import-token, not agnes init
|
||||
assert "agnes diagnose" in text # admin keeps diagnose
|
||||
|
||||
|
||||
def test_render_analyst_with_ca_pem():
|
||||
"""Analyst role + private CA → TLS trust block reused from admin path."""
|
||||
text = render_setup_instructions(
|
||||
server_url="https://agnes.example.com",
|
||||
token="agnes_pat_TEST",
|
||||
wheel_filename="agnes-0.32.0-py3-none-any.whl",
|
||||
role="analyst",
|
||||
ca_pem="-----BEGIN CERTIFICATE-----\nMIIBxxx\n-----END CERTIFICATE-----",
|
||||
)
|
||||
assert "AGNES_CA_PEM" in text # heredoc marker from trust block
|
||||
assert "ca-bundle.pem" in text
|
||||
assert "agnes init" in text # analyst-specific step still present
|
||||
Loading…
Reference in a new issue