# ADR 0015 — Bearer-token auth for every route + client/server split Status: accepted (2026-07-12) · Plan: plans/2026-07-12-wails-desktop-app.md, Phase 0 ## Context The control-room SPA was embedded in the `oikos` binary (`go:embed`, ADR 0001) and served at `/ui/*`. `combinedAuth` (`internal/httpapi/server.go`) opened a dev-open bypass — no credential required at all — whenever `OIKOS_ENV=dev` and no static token/OIDC issuer was configured. That was true not just in local dev but in the actual mac-mini production deploy: `docker-compose.yml`'s `api` service hardcoded `OIKOS_ENV: dev` with no token set, so every route (`/api/v1/*`, `/mcp`, and an `/agent` reverse-proxy mount to nomos that had never been wrapped in `combinedAuth` at all) was reachable unauthenticated from anywhere on the mesh/LAN. A planned Wails desktop client and any future non-browser client can't rely on same-origin requests or a dev-open bypass; they need the SPA to be a standalone, CORS-capable client that authenticates over HTTP like any other caller. ## Decision - Delete the SPA embed (`web/embed.go`, the `/ui/*` routes). `web/` is a standalone static build, deployed separately (`make ui` / `make deploy-ui`), served at `/` by Caddy with SPA fallback. - Remove the dev-open bypass entirely. Every route requires a valid static bearer token (`OIKOS_API_TOKEN` / `OIKOS_MCP_BEARER_TOKEN`) or an OIDC JWT, with two narrow exceptions: `/healthz` (liveness) and `POST /api/v1/clients/enroll` (IP-gated in the handler instead). `GET /api/v1/events/stream` additionally accepts the token as a `?token=` query param, since `EventSource` can't set custom headers. - Add CORS (`github.com/go-chi/cors`, `OIKOS_CORS_ORIGIN`, default `*`) so a cross-origin SPA (Vite dev server, a future Wails webview) can reach the API. No `AllowCredentials` — auth is a header, not a cookie, so credentialed CORS mode isn't needed and the two don't combine safely with a wildcard origin. - Wrap the previously-unauthenticated `/agent` proxy mount in the same `combinedAuth` middleware as every other route. - `cmd/nomos` becomes an authenticated client of `api`: it now sends `Authorization: Bearer $OIKOS_MCP_BEARER_TOKEN` on its own outbound calls (MCP + the chat-assent approval-decision endpoint), which it never did before — dev-open covered for it until now. - The SPA gets a runtime config module (`web/src/lib/config.ts`) and a first-launch `Config.svelte` screen: server URL + token, stored in `localStorage`, injected into every `fetch()` via a shared `fetchWithAuth` wrapper. Resolved fresh per request (not cached at module-load time), so the same build works same-origin or cross-origin without a rebuild. ## Consequences - Closing dev-open was a live security fix, not just future-proofing — verified post-deploy that unauthenticated requests to production now 401. - Nomos's *own* HTTP gateway (`cmd/nomos`, port 8092) still has no auth of its own — out of scope here, tracked separately (plans/2026-07-11-nomos-agent-code-review.md, finding C1). - Production Caddy (`dtoro/caddy-conf`, not this repo) does not yet expose `oikos.hubris.network` at all, so the interaction between Authentik forward-auth and bearer-token clients (a non-browser client can't complete a browser SSO redirect) is unresolved — needs an `@enroll`-style bypass for `/api/v1/*`/`/mcp`/`/agent/*` before public exposure. This repo's `compose/caddy/Caddyfile.oikos` (a reference copy, not deployed from here) has the bypass; the real config does not yet. - There is one shared bearer secret for all agents/clients, not per-client tokens — acceptable for the current fleet size, revisit if per-client revocation becomes necessary.