Humans reach rooms through a web UI the broker serves, behind oauth2-proxy
Status: Accepted Date: 2026-09-27 Deciders: Smana (Platform Owner) Related Spec: SP2 — Collaboration rooms
Context
Developers must watch runs, queue work, take the driver token and approve actions (UX reviews, 2026-09-27: “developers cannot access anything”). The page renders LLM output, so a token readable by the page is a token an injected transcript can steal (T10). Approvals must work from a phone.
Decision Drivers
- No token the page’s script can read
- Every semantic of the room (driver, approval cards, queue, fork) in one client with no install
- No approving or steering from a place a local agent can drive (§8)
- Reuse a pattern the platform already runs
Considered Options
Option 1: A web UI served by the broker, behind oauth2-proxy
Pros: the session is an HttpOnly, SameSite=Strict cookie; oauth2-proxy is already the
platform’s pattern (Headlamp on gcp-0); a phone works; one deploy.
Cons: a small UI to own.
Option 2: A Headlamp plugin
Cons: Headlamp is for cluster operators; developers are GitOps-only and hold no Kubernetes binding; its auth model is Kubernetes RBAC, not room roles.
Option 3: A CLI only
Cons: no phone approvals, no blog screenshots, and a CLI is exactly where a local agent can act.
Option 4: An AHP facade for existing clients
Cons: AHP is pre-1.0 and no client this team uses speaks it.
Option 5: A browser app with a public PKCE client
Cons: the access token lives in the page, next to rendered LLM output.
Decision Outcome
Chosen option: “Option 1”, with roomctl for read, chat, queue and fork (SP2 phase 6, not yet built).
Consequences
Positive
- Developers in
agents-memberwatch every room; the page never holds a token. - The broker re-validates both tokens, so a bypass of oauth2-proxy still authenticates nobody.
Negative
rooms-proxyis the platform’s first ZITADEL client issuing JWT access tokens.agent-systemreads only OpenBao’sagentsmount, whichexternal-secretscannot read. The OIDC sync writes the secret to the cloud secret store like every client, and its existing OpenBao mirror copies it toagents, so OpenBao keeps one writer. Only a hosting sync run with--mirror-openbaoproduces that copy. Only gcp-0’s hosting sync passes it; aws-0’s would need the flag added if AWS hosts again. No ExternalSecret reads the cloud copy, so on GCP nothing is ever granted access to it.
Neutral
- WebSockets cross the Tailscale Gateway with
timeouts.request: 0sand a 30 s ping. - The ID token must carry the
rooms-proxyclient asazp; ZITADEL’s JWT access tokens carryclient_idinstead. Both need the project id and an allowlisted client inaud. ZITADEL adds the project id toaudonly when oauth2-proxy requests the project audience scope. The broker reads the project id and client ids from files at use, because gcp-0 mints a fresh ZITADEL, and fresh ids, on every build.
Implementation Notes
infrastructure/base/room-broker/ (oauth2-proxy, route), scripts/provision/zitadel-oidc-clients.sh
(rooms-proxy, agents-admin, agents-member, --grant). A fresh ZITADEL holds no grants, so
--grant agents-admin=<email> is re-run after each build, once that user has logged in.
| Part | State |
|---|---|
| Web UI: watch, post and queue, steer, interrupt, hand to a role | Built (SP2 phases 2–4) |
| Approval cards | SP2 phase 5, not on the integration branch yet |
roomctl and fork | SP2 phase 6, not started |
oauth2-proxy bearer tokens for roomctl | Off (skip-jwt-bearer-tokens: "false") until phase 6 |