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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
# Marketplace name as published by app.marketplace_server.packager.
|
# Marketplace name as published by app.marketplace_server.packager.
|
||||||
# Hard-coded here (rather than imported) to keep this module dependency-free
|
# Hard-coded here (rather than imported) to keep this module dependency-free
|
||||||
# and trivially testable. If the value ever drifts, the regression test
|
# 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
|
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(
|
def resolve_lines(
|
||||||
wheel_filename: str,
|
wheel_filename: str,
|
||||||
*,
|
*,
|
||||||
|
|
@ -613,6 +659,7 @@ def resolve_lines(
|
||||||
self_signed_tls: bool = False,
|
self_signed_tls: bool = False,
|
||||||
server_host: str = "",
|
server_host: str = "",
|
||||||
ca_pem: str | None = None,
|
ca_pem: str | None = None,
|
||||||
|
role: Literal["analyst", "admin"] = "admin",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Return the template lines with server-side placeholders substituted.
|
"""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 resulting URL (`/cli/wheel/agnes.whl`) will 404 at download time, but
|
||||||
the instruction text still renders so operators can see the snippet shape
|
the instruction text still renders so operators can see the snippet shape
|
||||||
and diagnose the missing wheel on the server.
|
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 [])
|
names = list(plugin_install_names or [])
|
||||||
has_marketplace = bool(names)
|
has_marketplace = bool(names)
|
||||||
has_ca = bool(ca_pem and ca_pem.strip())
|
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(
|
def render_setup_instructions(
|
||||||
server_url: str,
|
server_url: str,
|
||||||
token: str,
|
token: str,
|
||||||
|
|
@ -702,13 +780,14 @@ def render_setup_instructions(
|
||||||
self_signed_tls: bool = False,
|
self_signed_tls: bool = False,
|
||||||
server_host: str = "",
|
server_host: str = "",
|
||||||
ca_pem: str | None = None,
|
ca_pem: str | None = None,
|
||||||
|
role: Literal["analyst", "admin"] = "admin",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Render the setup instructions as a single string.
|
"""Render the setup instructions as a single string.
|
||||||
|
|
||||||
Used server-side for tests and any non-JS rendering path. The browser
|
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
|
clipboard flow uses the JS renderer embedded in the Jinja partial; both
|
||||||
must produce byte-identical output for a given (server_url, token,
|
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(
|
lines = resolve_lines(
|
||||||
wheel_filename,
|
wheel_filename,
|
||||||
|
|
@ -716,6 +795,7 @@ def render_setup_instructions(
|
||||||
self_signed_tls=self_signed_tls,
|
self_signed_tls=self_signed_tls,
|
||||||
server_host=server_host,
|
server_host=server_host,
|
||||||
ca_pem=ca_pem,
|
ca_pem=ca_pem,
|
||||||
|
role=role,
|
||||||
)
|
)
|
||||||
text = "\n".join(lines)
|
text = "\n".join(lines)
|
||||||
return text.replace("{server_url}", server_url).replace("{token}", token)
|
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