Skip to content

Capability Gateway (WS-B)

Browser-facing FastAPI edge over the existing services — the partner to the Brain Builder SPA (web/brain-builder/). Adds CORS + REST (GET/POST/PATCH) + WebSocket chat + SSE and mirrors web/brain-builder/src/lib/api/contract.ts.

Auth (Phase 5, JWT + refresh tokens)

  • Login: POST /v1/auth/login {email,password}{access_token, refresh_token, expires_in, token_type:"bearer", principal}.
  • Refresh: POST /v1/auth/refresh {refresh_token} → same bundle with a NEW access + NEW refresh (rotating, single-use). Invalid/expired/reused refresh → 401 invalid_refresh (reuse revokes the whole chain — theft mitigation). Logout: POST /v1/auth/logout {refresh_token}{ok:true}.
  • GET /v1/auth/me (Bearer) → principal. POST /v1/auth/accounts (platform admin) provisions accounts.
  • Refresh tokens are opaque + stored hashed (app.refresh_token); access JWTs stay short-lived (JWT_EXPIRE_SECONDS); refresh TTL = REFRESH_EXPIRE_SECONDS (default 7d).
  • REST: Authorization: Bearer <jwt>. WS + SSE: ?token=<jwt> (browsers can't set those headers).
  • Two account types: tenant_user (bound to one brain_id + role Admin|Curator|Reader) and platform_admin (superuser — any brain, bypasses role→verb gating, plus tenant management GET/POST /v1/brains). A tenant user touching another brain → 403 brain_forbidden; a role that can't run a verb → 403 role_forbidden; missing/invalid token → 401.
  • Seed demo accounts: bb account seed (provision more with bb account create): | email | password | type/role | |---|---|---| | admin@polaris.io | polaris-admin-pw | platform_admin | | admin@meridian.legal | meridian-admin-pw | tenant Admin | | curator@meridian.legal | meridian-curator-pw | tenant Curator | | reader@meridian.legal | meridian-reader-pw | tenant Reader |
  • Deferred: per-verb/per-credential rate limiting (plan E7 AC).

Run

# stack must be up + a brain certified (so serve has a published version)
./scripts/start-compose.sh
APP_CONFIG=$(pwd)/env/local/app.env uv run python -m app.gateway_service   # :8000
# if :8000 is taken, pick another port:
APP_CONFIG=$(pwd)/env/local/app.env uv run uvicorn app.gateway.app:app --port 8088

Surface (all under /v1, scoped by X-Brain-Id / X-Role)

  • REST: GET .../overview|corpus|sources|change-sets|change-sets/{id}|versions|lifecycle|manifests|audit|ops/{op_id}, POST .../sources|ingest/process|certify|rebuild, PATCH .../change-sets/{id} ({decision: approve|reject|edit}). Role-gated; 403 {error:{code:"role_forbidden"}} on violation.
  • WebSocket chat: ws://host/v1/brains/{brain_id}/agents/{agent}/chat?role= — send {type:"query",verb,query,conversation_id}; receive routed → 7× trace (pre-gate→fsm→retrieve→assemble→reason→cite-then-verify→post-gate, instrumented from serve() via its on_stage hook) → envelope (+ artifact for assess/redline) → dashboard. Free-text routed to a verb; role-gated.
  • SSE: GET .../events (EventSource) — source.ingest, change_set.created|updated, curation.approve|reject, lifecycle.certify, brain.published, op.progress, op.done. Fed by gateway-performed actions (long ops + mutations). Frames go on the default message channel (data: {SseEvent} with type in the body — the SPA reads onmessage, not named events).
  • List shape: list endpoints (change-sets, sources, coverage, audit) return {items: [...], next_cursor}; single objects (overview, brain, lifecycle, change-set detail) are returned bare.

Wire the SPA to the real gateway

In web/brain-builder/.env (or .env.development): set VITE_USE_MOCKS=false and point the three bases at the gateway (adjust port if not 8000):

VITE_USE_MOCKS=false
VITE_API_BASE=http://localhost:8000/v1
VITE_WS_BASE=ws://localhost:8000/v1
VITE_SSE_BASE=http://localhost:8000/v1

No SPA code changes — the contract types are the seam.

Notes / follow-ups

  • In-process event hub (single worker). A Redis-backed fan-out generalizes to multiple workers.
  • draft verb returns not_implemented (backend stub). Librarian/Sentinel chat verbs return a lightweight ack — real Librarian work goes through REST (PATCH change-sets, POST certify).
  • Long ops (certify/rebuild/ingest_process) run in a background task with progress on SSE + pull fallback at GET .../ops/{op_id}.
  • serve() gained an optional on_stage callback (default None → unchanged behavior); the WS endpoint passes it to stream the trace.