2026-06-12-trackb-governance-dashboard-design

Track B — LOCKED Design: Governance-First Dashboard Refresh

Concept: hybrid of "evidence-first" and "authority-first," landing on a Governance Posture home. Thesis: the dashboard's visible face is _posture + proof_ — what is enforced, who holds what authority, and verifiable evidence of every decision — with routing/usage intact but demoted to Operations. The raw ledger is the drill-down, not the homepage (CloudTrail is not the AWS console home; IAM's summary is what reviewers screenshot). Every claim the UI makes must be backed by a contract that exists at that phase — no surface ships in a degraded or fabricated state.

Stack constraints honored: Vanilla TS/Vite, existing Supabase/Keycloak auth via getFreshToken(), cross-origin authFetch + X-BR-Tenant-Id, files ≤ ~500 LOC, existing token system extended not replaced. Track B invents zero API shape; every missing read is a CONTRACT REQUEST (§6).

---

0. Critique Resolution Register

Every fatal/fixable objection from the six critiques, resolved or deferred explicitly:

#Objection (critique)Resolution
R1No containment path ("how do I stop it") — routes exist today (C0.1, C3.3)Resolved, Phase 1. New containment-actions.ts mounted in every principal/evidence detail surface: kill-switch scope (POST /auth/killswitch/scope, DELETE …/{type}/{id}), cert block-tenant, API-key disable — with blast-radius copy. Grant revoke joins it in Phase 2.
R2Grant mutation flagged-open; revoke can't be SDK-only (C0.1 vs C1.2)Resolved decision proposed to Track A: dashboard is read + simulate + revoke. Issue/attenuate stay apiKey/SDK-only ("agents hold credentials; humans observe, simulate, and contain"). POST /auth/grants/{id}/revoke for JWT humans is a blocking requirement for Phase 2 — revoke is break-glass, not CRUD.
R3"Who can do X" capability-inverse query missing (C0.2)Resolved. Added to grants contract: GET /auth/grants?capability=tool:shell&status=active (server-side, computed over envelope_json); surfaced as a "Who can…" tab + one-click pivot from deny detail.
R4Phase 1 ledger filter pills are a facade; merged tri-source feed silently truncates (C0.3, C2.1)Resolved. Merged tri-source ledger deleted from Phase 1. Phase 1 ships an honest "Security Events" view: single source, pills limited to type/limit/offset, explicit partial-data banner, no source/agent/verdict pills, no intersection drawer. Full chained ledger is Phase 3, gated on CR-1.
R5Scope denials / would_deny are not in security-events at all — audit-chain only, apiKey-only (C5.1)Resolved. Phase 1 never claims to show scope denials. The Phase 1 evidence view is labeled "Policy Violations" (governance/firewall/tool/anomaly events — taxonomy that actually exists) with in-product copy: "Scope denials and would-deny evidence arrive with the audit-chain API." Denials ledger + would-deny preview are Phase 3 (CR-1, CR-3).
R6mode-dial shows stored mode as if effective; cohort/kill-switch divergence = false ENFORCE (C3.1, C5.3)Resolved. Phase 1 dial is labeled "configured" everywhere, with a permanent sub-badge "effective state unknown — rollout state unavailable" (fetchStatus pattern). "Effective" wording, cohort/kill-switch badges, and the gated enforce-confirm flow unlock only with CR-2/CR-5 (Phase 3).
R7PATCH /auth/tenant-settings shallow-merges security → dial write clobbers guardian/toolFirewall; unversioned race (C5.2) — verified: tenant-config-ops.ts:68 "contentLogging + features merged deep, rest shallow"Resolved. Phase 1 dial does read-merge-write of the full security object (documented race, flagged in code comment + this doc). CR-2 adds a narrow PATCH /auth/security/scope-enforcement write route; dial switches to it when available.
R8policy/dry-run is the rules engine, not the scope composer; per-source lanes/signature footer unbuildable (C2.2, C3, C4.3, C5.3)Resolved. Phase 1 simulator is named "Policy Dry-Run" (never "PDP"), renders exactly {decision, matchedRule, ruleCount, mode} as a matched-rule card with microcopy "governance rules only — scope composition simulation arrives later." Per-source lanes render only from POST /auth/grants/{id}/check (Phase 2) or CR-4 scope-check (Phase 3). No fabricated signature footer, ever.
R9Gray "unavailable" chain badge in global chrome = wallpaper / anti-screenshot (C0-sec, C1.1, C4.1)Resolved. chain-verify-badge is not mounted at all until GET /auth/audit/verify exists (CR-1). It then appears on the Ledger page first; promoted to global chrome only after one stable release. Absent badge is honest; perpetual gray is not.
R10Client-side Ed25519 fails on canonicalization drift; false BROKEN is max-severity UI from a serialization bug (C2.3)Resolved. Lockstep requirement: @brainstormrouter/sdk-ts exports canonicalizeGrant (preferred) or /chain bundle includes canonical_bytes_b64 per link. Client verify failure falls back to server-attested state, never renders BROKEN. Badge copy: "signatures verified against JWKS fetched from brainstormrouter.com" — never "independently verified." BROKEN AT #n is reserved for server attestation.
R11Liaison plans relabeled "ephemeral grants" contradicts the ADR's own "Plans vs Grants vs Delegation" taxonomy (C4.2)Resolved. Plans are never styled or labeled as grants. They render in a distinct "Plans" section (Phase 4, CR-9) with copy "Plans are request-scoped; durable grants arrive with capability delegation." #grants nav item is capability-detected — hidden until /auth/grants responds; its empty state is an issue-your-first-grant SDK snippet (empty-as-onboarding).
R12Per-principal evidence feeds unimplementable on type/limit/offset-only security-events; wrong-empty feeds (C3.2)Resolved. CR-6 adds actor=, source=, date_from= to the existing security-events capability (cheap). Until granted, per-principal panels render "filtering unavailable — showing tenant-wide events," never a filtered-looking subset. Server-side filtering is a stated precondition in evidence-feed.ts.
R13Point-in-time composition: inspector shows _now_, denial happened _then_ (C3.2)Resolved. CR-1/CR-3 payload schemas require the decision-time composed-scope snapshot embedded in each denial/would_deny evidence row (composer already computes it on the request path). The intersection drawer renders the snapshot, labeled with its timestamp; the live inspector is a separate, clearly-labeled "current composition" view.
R14Alarm fatigue: warn floods, plan-issuance rows, undifferentiated amber (C0-sec, C3)Resolved. Ledger default filter = enforced DENY + REVOKE; warns appear as an aggregated count chip ("219 warned →") deep-linking to the would-deny view where they're the point. Repeated would_deny deduped via /auth/security-events/summary grouping ("would_deny × 41 · rule R · principal P"). Routine plan/grant-issued rows collapsed by default. Red strictly for enforced denials/revocations/chain-break.
R15Would-deny preview window-blindness (monthly batch agent invisible in 7d lookback) (C0-sec)Resolved. Enforce-confirm flow shows the lookback window, lets the operator widen it (CR-3 has date_from), and displays "oldest evidence available: N days."
R16Stat cards false all-clear on fetch failure (C0-sec)Resolved. "Data Unavailable" rendering (the state.ts fetchStatus pattern) is a stated acceptance criterion for every stat card, badge, dial, and feed on governance surfaces — enforced by test (§7).
R17"Humans" principal rows have no JWT list route (C5-sec)Deferred. Phase 1 directory is agents + API keys only; "humans" filter pill dropped. Members-list is CR-11, lowest priority — profile.memberships covers the self case and the demand is unproven.
R18CR liaison-read may be schema-level work, not a route mirror; CR scope-check is composer-refactor-sized (C5-sec)Acknowledged in CR list. CR-9 and CR-4 carry explicit "sizing: possibly schema/refactor work, not a thin mirror" flags so Track A can cost them honestly.
R19Mirror-tax-forever; generic JWT proxy might beat route-by-route mirrors (PA.7, C5-sec)Deferred to Track A as an explicit architecture question attached to the CR list. Track B consumes whichever shape ships; the per-route list below is the demand statement, not the implementation mandate.
R20Cross-plane doubt: "is the /auth mirror the same chain agents write via /v1?" (C4-sec)Resolved. Ledger page displays the chain head hash, so an apiKey-side GET /v1/audit/verify provably matches the dashboard — turns the mirror tax into a demo.
R21OPERATIONS mass vs. order; demotion blurs if sub-list ships expanded (C4-sec)Resolved. Collapsed Operations sub-list is a hard requirement with a snapshot test, not a wireframe nicety.
R22Vanilla TS friction at chain-tree/live-feed ambition (PA.6)Deferred with reason. Constraint holds; reuse map is verified real (delegation-tree, ev-*, forensic-detail-modal). Revisit as a flagged decision only if Phase 2 grant interactions blow the LOC budget in practice.

---

1. Information Architecture

1.1 Nav (sidebar groups; order is the message)

BRAINSTORM ROUTER                  [tenant ▾]   (chain badge: Phase 3+, see R9)
┌────────────────────────────┐
│ ● Posture            (HOME)│  #posture        NEW — default route
│                            │
│ EVIDENCE                   │
│   Ledger                   │  #evidence       Phase 1: "Policy Violations" scope;
│   Forensics                │  #forensics      Phase 3: chained audit ledger
│   Compliance               │  #compliance     (existing)
│                            │
│ AUTHORIZATION              │
│   Grants                   │  #grants         Phase 2; capability-detected (hidden until API responds)
│   Scopes & Enforcement     │  #enforcement    NEW — absorbs mode dials from security.ts
│   Policy Dry-Run           │  #simulator      NEW — never labeled "PDP" pre-/check
│   Guardrails               │  (existing)
│   Tool Firewall            │  (existing)
│                            │
│ IDENTITY                   │
│   Agents                   │  (roster + dossier, existing)
│   Trust & Identity         │  (#anomaly → security.ts; loses mode dials to #enforcement)
│   Delegation Map           │  (topology.ts, relabeled)
│                            │
│ OPERATIONS  (collapsed ▸)  │  Analytics · Logs · Costs · Routing Brain ·
│                            │  Models · Providers · Leaderboard · Evaluations
│ DEVELOP     (collapsed ▸)  │  Playground · Prompts · MCP
└────────────────────────────┘
 Settings sub-sidebar unchanged (Router Config, API Keys, Broadcasts, Quick Start, Admin)

Mechanics: extend PAGE_IDS with posture, evidence, enforcement, simulator, grants; LEGACY_MAP entries keep every old URL working (security/evidence→evidence, etc.). Default route flips analyticsposture in Phase 1 — safe because the posture page is honest and never empty on existing contracts (see 1.2A). Analytics remains first item in Operations and gains a "Governance posture" card linking up to #posture so the old landing advertises the new face.

1.2 Key pages

A. Governance Posture (#posture, default landing). The screenshot a security reviewer pastes into a vendor assessment — and never empty: "enforcement: off/off — here's how to turn it on" is itself useful content from minute zero.

┌──────────────────────────────────────────────────────────────────────────┐
│ GOVERNANCE POSTURE                                       last 7d ▾       │
│ ┌ ENFORCEMENT (configured) ──────────────┐ ┌ KILL SWITCH ─────────────┐  │
│ │ api_key   [ off | WARN | enforce ]     │ │ ○ clear · 0 scopes active│  │
│ │ br_scope  [ off | warn | ENFORCE ]     │ │ [view scopes]            │  │
│ │ ⓘ effective state unknown —            │ └──────────────────────────┘  │
│ │   rollout state unavailable (CR-2/5)   │ ┌ AGENTS ──────────────────┐  │
│ └────────────────────────────────────────┘ │ 38 active · trust mix ▂▅▇│  │
│ ┌ VIOLATIONS 7d ─────────────────────────┐ └──────────────────────────┘  │
│ │ governance 12 · firewall 4 · anomaly 1 │ ┌ GRANTS (Phase 2) ────────┐  │
│ │ top rule: R-7 (×9)        [→ evidence] │ │ 12 active · 3 revoked/wk │  │
│ └────────────────────────────────────────┘ └──────────────────────────┘  │
│ Phase 3 additions: chain ✓ badge · would-deny 7d sparkline · flag cohort │
└──────────────────────────────────────────────────────────────────────────┘

Phase 1 sources (all exist): /auth/tenant-settings (configured dials), /auth/killswitch/status+/scopes, /auth/security-events/summary, /auth/mesh/agents, /auth/governance/compliance. Every card inherits fetchStatus → "Data Unavailable", never a false zero (R16).

B. Evidence Ledger (#evidence). Phase 1 = honest single-source "Policy Violations" (R4, R5):

│ POLICY VIOLATIONS                    ⓘ Scope denials & would-deny evidence │
│ [type ▾] [limit]                       arrive with the audit-chain API     │
│ 14:02  governance.violation  rule R-7  actor research-agent-1   [detail]  │
│ … detail drawer: json-viewer over raw details · containment-actions block │

Phase 3 (CR-1) = the chained ledger:

│ EVIDENCE LEDGER     [chain ✓ verified · 14,203 · head a91f3c…]  [Export]   │
│ [verdict: DENY+REVOKE (default)] [source ▾] [actor ▾] [date] [⌕]           │
│ chip: "219 warned →" (links to would-deny view)            LIVE ● (SSE)    │
│ 14:02:11 DENY  researcher-7  model:gpt-5  source:plan  entry #14,198 ✓     │
│ ── drawer: decision-time scope snapshot (R13) · scope-intersection ·       │
│    principal card · containment-actions · [Open in Forensics] [Simulate]   │

Head hash displayed for cross-plane verification (R20). Export Phase 1 = "export visible rows" client-side; Phase 3 = /auth/audit/export (R4-adjacent, C2-sec).

C. Scopes & Enforcement (#enforcement). Phase 1: dials labeled configured, read-merge-write of full security object (R6, R7); Policy Dry-Run embedded. Phase 3: effective mode + cohort + kill-switch badges (CR-2/CR-5), would-deny preview with adjustable lookback + "oldest evidence: N days" (R15), gated enforce-confirm ("these 219 would have been blocked — proceed?"), per-principal current composition inspector via CR-4 (clearly labeled "now," distinct from decision-time snapshots).

D. Grants (#grants, Phase 2). Hidden until the API responds (R11). List with subject/status/root filters + "Who can…" tab (capability-inverse, R3). Detail = grant-chain-tree (per-hop scope narrowing, sig status, revoked-ancestor banner), json-viewer envelope, [Revoke (cascade)] (R2) + [Download evidence bundle] + [Check… → simulator]. Issue/attenuate render as SDK snippets, not buttons. Evidence-under-the-object: feed filtered to grant_context.grant_id at the bottom of the panel.

E. Policy Dry-Run / Simulator (#simulator). Phase 1: matched-rule card, exactly {decision, matchedRule, ruleCount, mode}, microcopy "governance rules only" (R8). Phase 2: grant-targeted /check with signed verdict + per-source lanes + kid footer. Phase 3: general composer lanes via CR-4.

F. Agent identity drawer (extends agent-dossier.ts): trust level, manifests, grants held (Phase 2), per-principal evidence feed (honest label until CR-6), containment-actions block (R1), envelope claims (Phase 4, CR-8).

---

2. Component Inventory (all in src/components/, ≤400 LOC each)

ComponentPhaseConsumed contractsNotes
posture-cards.ts1tenant-settings, killswitch/status+scopes, security-events/summary, mesh/agents, governance/compliancecomposition of ev-stat-card; fetchStatus mandatory
containment-actions.ts1POST /auth/killswitch/scope, DELETE …/{type}/{id}, POST /auth/certs/block-tenant, API-key disableblast-radius copy; active-scope indicator on rows whose principal is blocked; + grant revoke in Phase 2
mode-dial.ts1→3P1: GET/PATCH /auth/tenant-settings (read-merge-write full security, R7); P3: CR-2 (GET+PATCH), CR-3, CR-5"configured" label until CR-2/5; enforce gate + lookback control P3
dry-run-card.ts1→2/3P1: POST /auth/governance/policy/dry-run (exact fields); P2: /check mirror; P3: CR-4no fabricated lanes/signatures (R8)
evidence-feed.ts1→3P1: /auth/security-events (+summary for dedupe, R14); P3: CR-1 events + CR-6 params + CR-7 SSEthin ev-event-feed+ev-filter-pills wrapper; "tenant-wide" label until CR-6 (R12)
grant-chain-tree.ts2GET /auth/grants/{id}/chain, JWKSfork of delegation-tree.ts (scope-narrowing strips replace budget slices); hop click → forensic-detail-modal+json-viewer
chain-verify-badge.ts3 (mounted only then, R9)CR-1 /auth/audit/verify; Phase 2 grant-link layer: JWKS + SDK canonicalizeGrant (R10)states: verified ✓ / verifying / BROKEN (server-attested only) / unavailable-gray (transient only); broken-state click-through to a triage view (C3)
scope-intersection.ts2/3/check responses; CR-1/CR-3 decision-time snapshots (R13); CR-4never fed by dry-run; renders enforced_dimensions so coverage is never overstated
verdict-chip.ts1log/violation status fields that existcolor + glyph + word, never color alone

Reused as-is: ev-stat-card, ev-event-feed, ev-filter-pills, ev-sortable-table, json-viewer, forensic-detail-modal, governance-bar, threat-banner, delegation-tree (verified present).

---

3. Routing/Usage: Demoted but Loved

Zero feature removal. All LLMS/OBSERVABILITY pages move under one collapsed OPERATIONS group (hard requirement + snapshot test, R21). Integration touches: Logs rows gain verdict chips linking into evidence; Analytics gains the governance-posture card; decision-trace gains an "authority: grant grt\_… ✓" lane when grant_context lands (routing becomes a _consumer_ of the authority story); cost-by-grant is a Phase 4 nice-to-have. LEGACY_MAP preserves every bookmark.

---

4. Design Language

Extend the existing dark-industrial token system (--ink-, --bone, --sig-*); additions go in index.html :root (tokens.css is auto-synced — never hand-edit).

  • New tokens: --verdict-allow/-deny/-warn (warn-amber visually distinct from error-red everywhere — the warn→enforce journey is the product), --verdict-revoked (desaturated strike), --authority-active/-attenuated, --chain-verified — a reserved green used only for cryptographic verification, never generic "ok."
  • Verdicts are always color + glyph + word (✓ ALLOW / ✗ DENY / ⚠ WARN).
  • Density: operator-dense; tables over cards; monospace ids/kids/hashes with copy-on-click (numeral-parity test already guards tabular numerals); existing density toggle honored.
  • Tone: declarative, forensic, zero marketing copy. "14,203 entries · chain verified," "Chain verification unavailable (fetch failed)," "No denials in range." Never "Oops." UTC timestamps, local on hover. Every evidence view has Export.
  • Motion: none decorative, two exceptions — revoke cascade animating down the chain tree (200ms stagger; the demo moment) and the server-attested broken-chain banner.
  • New CSS: styles/pages/{posture,enforcement,grants,evidence}.css, each <300 LOC.

---

5. Phased Delivery

Phase 0 — Debt (no visual change, no backend). Split api.ts (3326 LOC) → src/api/index.ts barrel + src/api/{core,security,usage,agents}.ts (delete api.ts to avoid ./api resolution ambiguity, eat the one-line import churn — C2-sec); new domains api/{audit,grants,scopes}.ts start clean. Extract nav config from main.ts (<500 LOC). Rename security-center.ts→forensics.ts. Re-baseline visual snapshots.

Phase 1 — Posture + honest evidence (EXISTING CONTRACTS ONLY — zero Track A work). #posture home + default-route flip; nav re-IA + collapsed Operations; #enforcement with configured-only dials (read-merge-write, R6/R7); #evidence as "Policy Violations" (R4/R5); #simulator dry-run card (R8); containment-actions everywhere a principal appears (R1); verdict chips in Logs; alarm-fatigue defaults (R14). Not in Phase 1: chain badge, would-deny anything, source/agent pills, scope-intersection, grants nav item, "humans" pill (R17).

Phase 2 — Grants (gated on: Inc 3 shipped + grants JWT read-mirrors committed-or-cut + POST /auth/grants/{id}/revoke for humans + TS SDK with canonicalizeGrant). #grants read+simulate+revoke via @brainstormrouter/sdk-ts grants.* in api/grants.ts (lockstep rule — no hand-rolled fetch); chain tree; client JWKS verification with server-attested fallback (R10); evidence bundle download; "Who can…" tab (R3); API Keys grant-bound column; /check-fed intersection + signed verdict card; revoked-ancestor banners; capability-detected nav reveal with onboarding empty state (R11).

Phase 3 — Evidence honest (gated on contract requests; independent of Phase 2, may ship earlier/parallel as Track A delivers). CR-1 → ledger flips to /auth/audit/events, chain-verify-badge mounts (ledger first, chrome later, R9), head hash shown (R20), server export. CR-2/CR-5 → effective-mode dials, narrow enforcement write, cohort/kill-switch badges. CR-3 → would-deny preview + gated enforce flow + lookback control (R15) + decision-time snapshots in drawers (R13). CR-6 → per-principal feeds. CR-7 → SSE live tail replaces polling. CR-4 → general composer lanes in simulator.

Phase 4 — Polish (remaining CRs). Liaison "Plans" view (CR-9, distinct from grants, R11); identity envelope panel (CR-8); cost-by-grant; saved ledger filters; scheduled export; members list if CR-11 granted.

---

6. Consolidated CONTRACT REQUESTS to Track A

All /auth/* routes: JWT-bridge, tenant-scoped via X-BR-Tenant-Id, added to the CORS allowlist in src/api/server.ts and forwarded by src/gateway/api-mount.ts (CLAUDE.md triage order). Standing architecture question (R19): consider a generic JWT-bridge proxy with permission mapping instead of route-by-route mirrors — this list is the demand statement either way. Priority: CR-1 > CR-2 > CR-3 > CR-6 > grants block (with Inc 3) > CR-4 > CR-5 > CR-7 > CR-8 > CR-9 > CR-11.

  1. CR-1 — Audit chain read (mirror /v1/audit/*).

GET /auth/audit/events?actor=&action=&resource_type=&resource_id=&outcome=&source=&date_from=&date_to=&q=&limit=&offset={ events: AuditEvent[], total }with a typed evidence payload (C2.1): details: { source, principal, requested: {models?, tools?}, enforced_dimensions, reason, scope_snapshot } where scope_snapshot is the decision-time composed scope (R13). GET /auth/audit/verify{ valid, chainLength, headHash, brokenAt? }. GET /auth/audit/export?format=csv|json|cef&date_from=&date_to= → raw download. GET/PUT /auth/audit/retention (PUT admin-gated).

  1. CR-2 — Scope-enforcement resolved state + narrow write (R6/R7).

GET /auth/security/scope-enforcement{ sources: { api_key: {storedMode, effectiveMode, inCohort, killSwitch}, br_scope: {…}, delegation?: {…} } } (resolved via resolveScopeMode, not the raw blob). PATCH /auth/security/scope-enforcement — narrow, enum-validated, race-free write owning only its sub-key (today's shallow PATCH /auth/tenant-settings clobbers sibling security.* keys — verified tenant-config-ops.ts:68).

  1. CR-3 — Would-deny evidence. GET /auth/security/would-deny?source=&date_from=&limit=&offset={ events: [{requestId, source, principal, requested, reason, scope_snapshot, createdAt}], total, bySource, oldestEvidenceAt } (oldest-evidence field per R15).
  2. CR-4 — General composer shadow check. POST /auth/security/scope-check {principal, requested:{models?,tools?}}{ verdict, perSource:{api_key,plan,br_scope,delegation}, enforced_dimensions } — dry composition, no enforcement side effects. Sizing flag: composed-scope.ts is interwoven with request context; this is composer-refactor work, not a thin route (R18). /check covers grants only; this gap survives Inc 3.
  3. CR-5 — Feature-flag posture. GET /auth/feature-flags{ flags: [{name, enabled, cohort?, rolloutPercent?, tenantBlocked}] } (covers enforce_*, grants_api, tenants_blocked).
  4. CR-6 — Security-events filter params (cheap, pre-CR-1 stopgap). Add actor=, source=, date_from= to the existing /auth/security-events capability (today: type/limit/offset only, 200 cap).
  5. CR-7 — SSE event types on /auth/events (currently ping-only; sse-broadcaster.ts exists, genuinely cheap): scope_denial | would_deny | grant_issued | grant_revoked | chain_break with {requestId?, grantId?, source?, principal, requested?, reason, ts}.
  6. CR-8 — Identity envelope inspection. GET /auth/agents/{agentId}/envelope{ trustLevel, spiffeId, claims, authMethod, kid, expiresAt, signatureValid }.
  7. CR-9 — Liaison read surface. GET /auth/liaison/sessions?status=&limit=; GET /auth/liaison/{id}/plans[{planId, allowedModels, allowedTools, ceilingUsd, committed, fingerprint, issuedAt}]. Sizing flag: liaison routes are write-shaped; this may require new persistence/query schema (R18).
  8. CR-grants block (lands with Inc 3; Phase 2 gate, committed-or-cut before Phase 2 starts — R2):
  • Read mirrors: GET /auth/grants?subject=&status=&root=&capability= (capability-inverse param per R3, computed server-side over effective attenuated envelope_json scopes), GET /auth/grants/{id}, GET /auth/grants/{id}/chain, POST /auth/grants/{id}/check.
  • POST /auth/grants/{id}/revoke for JWT humans — blocking requirement (break-glass, not CRUD). Issue/attenuate stay apiKey/SDK-only (resolved decision, R2).
  • Lockstep: SDK exports canonicalizeGrant or /chain bundle includes canonical_bytes_b64 per link (R10).
  • Confirm CORS on public GET /.well-known/brainstorm/grant-keys for the dashboard origin.
  1. CR-11 (optional, lowest): tenant members list for a "humans" principal directory (R17).

---

7. Test & Verification Obligations

Every phase: existing theme-tokens.test.ts + stat-numeral-font-parity.test.ts stay green; Playwright visual snapshots re-baselined per phase; LOC budget check (no new file >500 LOC).

Phase 0: barrel-split import-equivalence test (every former api.ts export resolvable); zero-visual-diff snapshot run.

Phase 1:

  • No-fake-green suite: for each posture card / dial / feed, mock a 500 and assert "Data Unavailable" renders — never 0, never green (R16).
  • No-overclaim suite: assert dial label is "configured" and effective-state badge present (R6); assert simulator card contains no per-source lanes or signature elements (R8); assert "Policy Violations" banner copy present (R5); assert chain badge is absent from the DOM (R9).
  • mode-dial read-merge-write test: PATCH payload must contain the full prior security object (guardian/toolFirewall preserved) (R7).
  • containment flow e2e: deny row → contain → POST /auth/killswitch/scope payload + active-scope indicator (R1).
  • Snapshot: Operations group collapsed by default (R21). Router: default route posture; all LEGACY_MAP redirects.

Phase 2:

  • Canonicalization round-trip against SDK fixtures: verify(sig, canonicalizeGrant(envelope_json)) for known-good vectors; drift-injection test asserts fallback to server-attested, never client-rendered BROKEN (R10).
  • Grant revoke e2e incl. cascade banner; capability-detected nav (hidden when /auth/grants 404s); "Plans ≠ grants" — no grant styling/status chips on plan objects (R11); "Who can…" tab returns server-computed results, no client intersection (R3).
  • Badge copy assertion: "verified against JWKS fetched from…" exact string (R10).

Phase 3:

  • Ledger default filter = DENY+REVOKE with warn aggregate chip (R14); pills map 1:1 to CR-1 query params (no client-side-only filters — R4); truncation banner when total > rendered.
  • Effective-vs-configured: cohort-off fixture must render effective OFF while stored ENFORCE (R6).
  • Enforce-confirm flow: lookback control + "oldest evidence" rendered (R15). Decision-time snapshot rendered from row payload, not live recompute (R13). SSE reconnect/out-of-order tolerance. Head hash equals /auth/audit/verify.headHash (R20).

---

8. Decomposable Work Items

Phase 0 — W0.1 api barrel split (1 PR/domain: core, security, usage, agents). W0.2 nav-config extraction from main.ts. W0.3 security-center.ts→forensics.ts rename + LEGACY_MAP. W0.4 snapshot re-baseline.

Phase 1 — W1.1 router: add ids, default→posture. W1.2 nav groups + collapsed Operations. W1.3 posture-cards.ts + #posture page + CSS. W1.4 mode-dial.ts (configured-only, read-merge-write) + #enforcement page; remove dials from security.ts. W1.5 #evidence Policy Violations view + detail drawer. W1.6 containment-actions.ts + mounts (evidence drawer, agent dossier). W1.7 dry-run-card.ts + #simulator. W1.8 verdict chips in Logs. W1.9 analytics governance card. W1.10 token additions in index.html :root. W1.11 Phase-1 test suites (§7).

Phase 2 (after gate) — W2.1 api/grants.ts over SDK. W2.2 #grants list + Who-can tab. W2.3 grant-chain-tree.ts. W2.4 detail panel: envelope viewer, bundle download, evidence feed. W2.5 revoke flow + cascade animation. W2.6 client JWKS verify layer + fallback. W2.7 /check simulator + scope-intersection.ts. W2.8 API Keys grant-bound column. W2.9 nav capability-detection + onboarding empty state. W2.10 Phase-2 tests.

Phase 3 (per-CR, independently landable) — W3.1 api/audit.ts + ledger flip + export + head hash (CR-1). W3.2 chain-verify-badge.ts on ledger, then chrome (CR-1). W3.3 dial upgrade: effective mode + narrow PATCH (CR-2/5). W3.4 would-deny preview + enforce gate + snapshots (CR-3). W3.5 per-principal feeds (CR-6). W3.6 SSE live tail (CR-7). W3.7 composer lanes in simulator (CR-4). W3.8 Phase-3 tests.

Phase 4 — W4.1 Plans view (CR-9). W4.2 envelope panel (CR-8). W4.3 cost-by-grant. W4.4 saved filters + scheduled export. W4.5 members directory (CR-11, if granted).

---

Key paths: site/dashboard/src/{router.ts,main.ts,api.ts→api/index.ts,state.ts,event-stream.ts}; pages {security.ts,security-center.ts→forensics.ts,tool-firewall.ts,guardrails.ts,compliance.ts,agents.ts,agent-dossier.ts}; components {delegation-tree,ev-event-feed,ev-filter-pills,ev-stat-card,ev-sortable-table,json-viewer,forensic-detail-modal,governance-bar,threat-banner,decision-trace}.ts; styles/tokens.css (auto-synced from index.html). Backend contract anchors: src/api/capabilities/security/audit-export.ts, src/api/capabilities/auth/{usage-insights.ts,governance-forensics.ts,tenant-config-ops.ts,budget-killswitch.ts,events.ts,api-keys.ts,agents-mesh.ts}, src/api/capabilities/liaison/*, src/security/{scope-match.ts,effective-scope.ts,trust-envelope/}, src/api/routes/completions/composed-scope.ts, src/infra/feature-flags.ts, docs/ship-log/2026-06-12-inc3-capability-grants-delegation.md (§API surface).