Web Console
July 18, 2026 · View on GitHub
Status: normative specification for the web console. Normative for the console described in
console.md(the design document — rationale, research, design language). This spec extendsdocs/spec/spec.md§26.6 and supersedes its "read-only, later phase" framing. Journeys:console-user-journeys.md. (The phase/WP execution plan that built this was a working artifact undertmp/, not part of the published docs; the phase conformance gates live inpackages/web-console/test/phase*-gate/.)Authority order for implementers:
docs/spec/spec.md(platform behavior) → this spec (console behavior) →docs/api-reference.md(wire contract) →CONVENTIONS.md(binding engineering conventions).RFC-2119 language: MUST / SHOULD / MAY.
§1 Purpose and scope
1.1 The console is a single-page web application served by the backend at
/console, giving every human persona of docs/user-journeys.md a browser
surface over the public /v1 API.
1.2 Parity rule. The console MUST consume only the public /v1 API plus
the console-support endpoints defined in §3–§4. It MUST NOT get private API
routes. (Corollary: anything the console can do, curl can do.)
1.3 Non-goals (v1 of this spec): a platform-admin surface (no /v1
platform-admin API exists, by design); payment forms / invoices / tax UI
(delegated to the billing adapter's hosted pages, §11); metering re-computation
client-side; native mobile apps.
1.4 The CLI (Pi client extension) remains the primary interactive client. The
console is the observation, administration, and anywhere-fallback surface
(§10). Surfaces MUST deep-link to each other: the extension prints
/console/sessions/<id> URLs; the console shows equivalent CLI commands where
an action is CLI-preferred (fork → resume, §10.4).
§2 Definitions
- Mode — deployment presentation:
solo,team, orsaas(§5). - Scope — API-key scopes per
api-reference.md(read,write,admin). - Console session — the server-side record binding a browser cookie to a validated API key (§4).
- Ledger — the tenant balance record set of §11 (saas mode).
- DP-n — design principles DP-1…DP-14 in
console.md§3; they are normative for UI behavior and cited from this spec.
§3 Serving, config, and headers
3.1 The backend MUST serve the built console at /console and /console/*
via the existing pre-auth onRequest hook (packages/backend/src/api/console.ts),
same-origin with /v1. Unknown /console/* paths (client-side routes) MUST
serve index.html (history-API fallback); asset paths keep extension-based
content types.
3.2 GET /console/config — public, unauthenticated. Response body exactly:
{ "mode": "solo" | "team" | "saas", "onboardingEnabled": true|false }
Mode source: CONSOLE_MODE env var if set; otherwise derived
(ONBOARDING_ENABLED=true → saas, else solo). The endpoint MUST NOT
expose versions, instance ids, or any other configuration.
3.3 Mode is a presentation concern only. No /v1 behavior may differ by
mode. A console in any mode against any backend MUST degrade gracefully
(e.g., team console on a saas backend simply shows no signup).
3.4 Security headers on all /console* responses: a CSP of
default-src 'self' (no inline script, no external hosts — everything
bundled, no CDN ever), X-Content-Type-Options: nosniff,
Referrer-Policy: no-referrer, and frame-ancestors 'none'.
§4 Authentication: console sessions
4.1 The browser MUST NOT hold the API key in JavaScript-readable storage
(localStorage/sessionStorage/JS-visible cookies). The v1 console's
localStorage mechanism is retired.
4.2 POST /console/session — body { "apiKey": "..." }. The backend
validates the key (existing auth path), and on success stores the key
reference server-side bound to a new random session id, returned as a cookie:
HttpOnly; Secure; SameSite=Strict; Path=/. Response body:
{ "scopes": [...], "tenant": { id, name } }. Invalid key → standard error
envelope, 401.
4.3 Requests from the console to /v1/* ride the cookie: the backend's auth
middleware MUST accept the console-session cookie as an alternative to the
Authorization bearer header, resolving to the same key → same scopes, same
rate limits, same audit identity. Bearer, when present, wins.
4.4 DELETE /console/session — destroys the server-side record and clears
the cookie (sign out). GET /console/session — returns
{ scopes, tenant, expiresAt } for the current cookie, 401 if none (used on
app boot).
4.5 CSRF. All state-changing requests authenticated by cookie MUST carry
the custom header X-Console-Csrf: 1; the backend MUST reject (403) mutating
cookie-authenticated requests without it. (Custom-header check; SameSite=Strict
is defense-in-depth, not the mechanism.) Bearer-authenticated requests are
exempt.
4.6 TTL. Console sessions expire server-side with a sliding TTL.
Defaults by mode: solo 30 days, team 7 days, saas 24 hours;
CONSOLE_SESSION_TTL overrides all. Revoking the underlying API key MUST
invalidate its console sessions on next use.
4.7 Storage (decided, console.md §10.1): a Postgres table
(console_sessions), so sign-out and key revocation delete server-side state
immediately and the sliding TTL is a column update. The implementing WP adds
the migration and an expiry-cleanup sweep, and regenerates db-schema.md.
§5 Modes
5.1 solo — self-hosted single user. No signup surface. First-run sign-in
explains how to obtain the first key (spec §24 onboarding / journey P1.7).
Quota display de-emphasized. Settings includes a backend health widget reading
public /healthz//readyz.
5.2 team — self-hosted multi-user. No signup surface. Key issuance and
rotation flows prominent in Settings; sign-in copy addresses issued keys and
least privilege.
5.3 saas — multitenant service. Signup page (§9.6) present iff
onboardingEnabled. Home leads with balance + burn (§11.8). Settings gains
Billing (§11.8).
5.4 Every mode difference MUST be expressible as: a route present/absent, a copy variant, or an ordering/emphasis change. Anything else is a spec change.
§6 Authorization presentation (scope-variant UI)
6.1 The UI derives capability purely from the scopes returned in §4.2/§4.4. It MUST hide (not disable) top-level sections the key cannot use, and disable (with explanation) inline actions within visible sections (DP-6).
6.2 Scope map (mirrors the platform's enforcement — the platform spec is
authoritative) — read: all browsing/observation surfaces. +write: session
interaction (create, message, interrupt, tool confirmation, fork), job
trigger, and resource-management mutations (agents, environments, vaults,
jobs, webhooks — the backend requires write for these). admin: API-key
management (issue/revoke — backend-enforced admin) and the Settings
surface. Disabled-action reasons MUST name the scope the backend actually
requires for that action.
6.3 A read-only console session MUST be visibly badged ("browsing
read-only"). A key with admin scope SHOULD trigger a one-time
least-privilege nudge (DP-8).
6.4 The backend remains the sole enforcer. Client-side hiding is UX, not
security; every mutating call still fails server-side without scope, and the
console MUST render such failures per DP-9 (error code + request id + doc
link).
§7 Information architecture
7.1 Left sidebar, exactly these top-level items, filtered by scope and mode: Home, Sessions, Agents, Jobs, Resources (environments · vaults · memory stores · files · skills), Settings (API keys · webhooks · tenant · billing[saas] · health[solo/team]). No deeper sidebar nesting; a top-level item MAY swap in a contextual submenu (Resources, Settings).
7.2 Home: recents, favorites, active sessions, sessions in
requires_action, and the mode's headline strip (§5; DP-14: ≤5 metrics, with
trend). Favorites/recents are client-side per-browser state (no API).
7.3 Sessions is one list (DP-4): columns id, title, status, agent@version,
environment, created, cost; filters status/agent/environment; cursor
pagination per api-reference.md. Session detail tabs: Trace (default),
Tree, Usage, Outputs, Conversation (§10; phase-4).
7.4 Trace tab: chronological entries from GET /v1/sessions/:id/entries;
tool executions render name, input, output, truncation flag; event-type
filter; payloads collapsed by default and lazy-expanded (DP-2). While the
session is running, the trace live-tails via §8.
7.5 Sessions in requires_action MUST surface globally: sidebar badge + Home
section, not only on their own page.
7.6 Every rendered resource id is monospaced, one-click-copyable, and a link to its detail route (deep-link rule, §1.4).
7.7 Empty states carry the create flow (scope permitting) and the equivalent CLI command (DP-5). Destructive/terminal actions (archive agent, delete vault, revoke key…) use typed confirmation naming real consequences fetched from the API where available (DP-7).
§8 Live streaming
8.1 Live views consume GET /v1/sessions/:id/stream (SSE) using fetch
streaming, not native EventSource (control over headers, reconnect, and
replay position). Reconnect MUST resume from the last seen position per the
api-reference SSE reconnect/replay contract; a dropped stream falls back to
entry polling with no missed events (mirrors the extension's degradation
behavior).
8.2 Live data updates the TanStack Query cache (invalidation/patch); no parallel state store.
8.3 Ambient status (DP-11). The document title and favicon MUST reflect
the viewed session's status (running / completed / failed /
requires_action) so a background tab is informative.
§9 Screens — normative minimums
Per-resource screens MUST cover at least the operations listed; all via
public /v1 routes as documented in api-reference.md.
- 9.1 Agents (admin): list, detail with version history, create, PATCH (creates new version — UI MUST say so), archive (terminal; DP-7 dialog MUST state auto-archival of referencing jobs).
- 9.2 Environments (admin): list/create/edit/archive/delete;
cloudshows image/resources/network policy with one-line policy explanations (DP-6);self_hosteddetail shows worker keys (mint = §T5),work-stats(depth, oldestQueuedAt, workersPolling) and drain (work-stop, with{force}as an explicit second step). - 9.3 Vaults & credentials (admin): create vault, add credential by
category, validate button, archive. Secret fields are write-only: the UI
MUST NOT display, echo, or retain secret values after submit (DP-8), and
MUST explain the
model_provider_keyfail-closed rule (DP-6). - 9.4 Jobs: list/detail/runs (all scopes); create/pause/unpause/archive/
manual-run (
writeper the API — see §6.2); auto-paused jobs MUST show their pause reason prominently. - 9.5 Memory stores / files / skills: list + detail + content viewing per
API; memory version history with restore (restore requires the backend
restore op — WP-C4.0; until it ships the history is audit-only and the UI
says so); mutations
write-scoped per the API (§6.2). - 9.6 Onboarding (saas): signup form →
POST /v1/onboarding/signup→ key shown exactly once (copy-and-confirm) → first-run checklist (model key → agent → first session; DP-12). MUST NOT require payment details. Once §11 ships, signup also grants the trial balance per §11.1 (email-verification gated); before that, saas signup simply has no balance mechanics. - 9.7 API keys (admin): list, issue (scopes opt-up from
["read"], secret shown once), revoke (DP-7). - 9.8 Webhooks (admin): register (event-type picker),
whsec_shown once, test-delivery button, auto-disabled state surfaced with re-enable path. - 9.9 Tenant / usage: quota-vs-limit, spend over time (per §11.5
endpoints when available; per-session aggregation until then), breakdown by
agent and
metadata.userId. Cost observability is all-modes (§11.2).
§10 Conversation view ("continue from any browser")
10.1 A write-scoped user MUST be able to continue an idle session from the
browser: composer sends user.message events (waking the session), replies
render via §8. Seed rendering uses GET /v1/sessions/:id/messages; the
Conversation lens is conversation-shaped, distinct from Trace (DP-4).
10.2 Turn lifecycle: messages sent mid-turn queue client-side — the API
rejects mid-turn user.message events (409 session_not_idle, ROB-5), so
the console holds queued messages and dispatches them at the next turn
boundary, rendering the queue honestly (queued state visible, nothing
silently dropped, surviving tab switches); interrupt is available;
requires_action requests surface in the composer area itself.
10.3 The view MUST state resume caveats inline where relevant: processes from prior turns are not preserved; outputs are downloadable but there is no local cwd (DP-6).
10.4 A "continue in Pi instead" affordance shows /remote:resume <id>.
10.5 Layout MUST be usable on a phone-width viewport. Responsive-only
(decided, console.md §10.5): no PWA manifest or service worker — a service
worker interacts with §3.4 CSP and §4 cookie auth and the option was
deliberately declined for phase 4.
§11 Commercialization facilities (saas)
11.1 Model. Prepaid, single-unit (dollars). No plans, tiers, seats,
subscriptions, or entitlement mapping. One price line (decided,
console.md §10.7): marked-up model spend; sandbox compute is absorbed
into the rate, guarded by concurrent-sandbox quotas and idle-session caps —
there is no second meter. Tenant lifecycle: trial → active, plus
suspended (balance ≤ 0). Trial grant (decided, §10.8): $5 at signup, no
card, activated only on email verification — the verification flow (token +
a minimal email-sender seam) is defined by the implementing WP; the console
MUST surface the unverified state and the resend action. Suspension is
fail-soft: reads MUST work; new sessions/turns MUST NOT start; running work
stops as budget_exhausted semantics already stop it. No past_due state
exists.
11.2 Cost observability ≠ billing. Usage/spend display (§9.9) is all-modes core; balance/top-up is a saas-mode layer on the same data.
11.3 Ledger. Balance and its history (grants, top-ups, drains, adjustments) live in backend Postgres; every entry carries an idempotency key. The backend ledger — not any payment engine — is the source of truth for usage and balance. Enforcement (11.1) reads only local ledger state; nothing in a request path calls a payment engine.
11.4 Metering export. BILLING_SINK=webhook events get a documented
schema: idempotency key, tenant id, time-bucketed aggregated quantities
(never per-turn). Additional sinks (e.g. stripe) are additive
implementations of the same seam.
11.5 Usage-over-time API. Tenant-scoped, time-bucketed usage (day/month
granularity; by agent; by metadata.userId) exposed on /v1 (exact routes
defined in the implementing WP against api-reference conventions; the wire
contract lands in contracts + api-reference.md first).
11.6 Threshold events. Webhook event types tenant.balance_low
(configurable threshold) and tenant.balance_exhausted, emitted through the
existing webhook subsystem. Consumed alike by console banner, email, and
auto-charge.
11.7 Billing adapter. All payment-engine code (Stripe as reference) lives
in packages/billing-adapter — the payment SDK appears nowhere else. The
adapter: creates hosted checkout/portal URLs on request; consumes payment
webhooks; credits the ledger through a narrow machine-credential provisioning
surface (host-agent auth pattern; NOT a tenant API key). Ledger credit from a
payment event MUST be idempotent under webhook replay.
Auto-charge (opt-in, off by default): threshold + amount; saved payment
method and off-session charging are adapter-internal; hard caps per day and
per month; auto-disable + notify after N consecutive failures (no silent
retries); every auto-charge appears in the ledger like any top-up.
11.8 Console (saas). Home: balance, burn sparkline, "lasts ~N days at
current rate". Settings → Billing: balance, ledger history, top-up (link-out
to hosted checkout), auto-charge toggle (threshold/amount/caps/last-charge
visible), receipts link-out. With no adapter configured, money buttons are
absent and everything else works. suspended state shows exactly what still
works and the single fixing action (DP-9).
11.9 The console MUST NOT compute money client-side; it displays what the API reports.
§12 Performance, accessibility, quality budgets
12.1 Initial JS ≤ 200 KB gzipped; route-level code splitting. First meaningful paint < 1 s against a local backend; interaction latency < 100 ms. Budgets are CI-enforced from phase 1 (DP-10).
12.2 Keyboard navigability for all tables/dialogs/forms; ARIA live regions on streaming views; axe-core clean on every route; both themes contrast-safe (DP-13). Light + dark theme from day one.
12.3 Web-console package tests follow CONVENTIONS.md binding rules —
notably fakes at the seam: API-client contract tests run against the real
in-process Fastify app (via testkit), not a stubbed fetch.
§13 Conformance checklist (per release)
- §1.2 no non-
/v1calls beyond §3–§4 endpoints (build smoke test greps). - §3.4 CSP present; zero external requests in a full route crawl.
- §4.1 no API key in any JS-readable storage after sign-in.
- §4.5 mutating cookie-auth request without CSRF header → 403.
- §6.1 scope matrix: each scope sees exactly its surfaces.
- §8.1 stream drop → resume with no missed/duplicated entries.
- §9.3 no secret value appears in any response fixture or DOM snapshot.
- §11.7 payment webhook replay credits ledger exactly once.
- §12.1 budgets green in CI; §12.2 axe-core clean.