# Gateway brain-proxy — design
Date: 2026-07-06
Component: swarph-cli — gateway/server.py (new endpoint) + commands/brain_ask.py (client path)
Status: design approved (commander 2026-07-06); plan + implementation to follow on branch feat/gateway-brain-proxy.
Problem
Querying the mesh brain (gbrain, :8792/mcp) requires a separate per-cell gbrain_ token, distinct from the mesh identity every cell already holds (its peer/SSO token). That is a double-auth smell (commander, 2026-07-06): "not to run double auths anywhere — having another token when we already mint them on the mesh is weird."
The client already anticipates the fix. brain_ask.py's token precedence is GBRAIN_TOKEN → SWARPH_BRAIN_TOKEN → the mesh per-peer token, with the comment "Once gbrain accepts mesh peer tokens, the peer token IS [the token]." The gap is server-side: gbrain accepts only its own gbrain_ tokens (verified 2026-07-06 — a mesh peer token → 401 on :8792/mcp; a gbrain_ token → 200). The client is ready; the server side is unshipped.
Concretely, onboarding a cell to the brain today needs a gbrain_ token minted, delivered securely, and rotated — friction that just blocked workstation-lc. And the mesh's shared gbrain_ read token is over-scoped (read/write/admin, the 71-char api-key kind).
Goal
A cell queries the brain using its mesh peer/SSO token — the identity already minted on the mesh — so no per-cell gbrain_ token exists. Retire the double-auth without modifying gbrain (upstream garrytan/gbrain, MIT — the gateway is ours).
Design
1. New gateway endpoint — POST /brain/query
Added to gateway/server.py alongside the existing routes (/messages, etc.), FastAPI @app.post.
- Request:
Bearer:, body{"query": ".", "limit": } - Auth: reuse
_authorize(authorization)verbatim (server.py:764) — it already validates all three mesh regimes (meta_edgeSSO /per_peer_token/shared_token) and returns anAuthContext(peer, regime, …)NamedTuple, raisingHTTPException(401)on a bad token. No new auth code. - Proxy: the gateway constructs the gbrain MCP
querycall itself —{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query","arguments":{"query":— and POSTs it to,"limit":
}}} GATEWAY_GBRAIN_URLwithAuthorization: BearerandAccept: application/json, text/event-stream. It parses gbrain's SSE (result.content[0].text= a JSON chunk array) and returns a clean JSON body to the caller. - Response (200):
{"chunks": [ {slug, title, chunk_text, score, …}, … ]}— the gbrain chunk array, unwrapped from the SSE envelope. - Read-only by construction: the gateway only ever builds a
name=queryMCP call. There is no code path that emitswrite/admin/add_tag/etc. So even thoughGATEWAY_GBRAIN_TOKENis over-scoped, a caller can only ever read. Read-only is a property of the code, not a filter over caller input. - Attribution: log the querying peer (
auth.peer) — per-peer brain-query observability, free.
2. Gateway configuration (server-side only)
Two env vars, read at module top like the existing config (os.environ.get("MESH_GATEWAY_TOKEN", "") pattern, server.py:62):
GATEWAY_GBRAIN_URL— defaulthttp://100.107.222.72:8792/mcp.GATEWAY_GBRAIN_TOKEN— the one gbrain read token the gateway holds. Never leaves the gateway host.
If GATEWAY_GBRAIN_TOKEN is unset, /brain/query returns 503 ("brain proxy not configured") — the endpoint is inert until the operator wires the token, so shipping the code is safe before deployment.
3. Client path — brain_ask.py
New precedence, additive:
- If
SWARPH_BRAIN_GATEWAYis set,brain-askPOSTswith/brain/query {"query", "limit"}andAuthorization: Bearer(read from~/.config/swarph/, the path the code already computes in.peer_token _peer_token_path). It reads{"chunks": [...]}back. - If
SWARPH_BRAIN_GATEWAYis unset → today's direct:8792+gbrain_token path, unchanged (backward-compat).
So the endpoint the operator configures decides the auth: gateway → peer token; direct :8792 → gbrain_ token. No mixing.
4. Backward compatibility (load-bearing)
- Direct
:8792querying keeps working exactly as today — the proxy and the client gateway-path are purely additive. A cell with noSWARPH_BRAIN_GATEWAYset behaves identically to before. - gbrain is not modified.
Errors
| Condition | Response |
|---|---|
| Missing / bad mesh token | 401 (from _authorize, unchanged) |
| Missing/empty query in body | 400 |
| GATEWAY_GBRAIN_TOKEN unset | 503 "brain proxy not configured" |
| gbrain unreachable / non-2xx / unparseable SSE | 502 "brain upstream error" — fail loud, never swallow |
Testing (TDD)
Gateway (tests/ — the gateway suite mocks httpx/urllib upstreams; follow that pattern):
- Auth: valid peer token →
200; missing/Bearer bad→401(via_authorize). - Read-only by construction: the MCP body the proxy sends upstream always has
params.name == "query"— assert it; there is no input that makes it write/admin. - Proxy round-trip: a mocked gbrain SSE reply → the endpoint returns
{"chunks": [...]}matching the chunk array. - Upstream failure: gbrain 500 / connection error →
502(not a swallowed empty result). - Unconfigured:
GATEWAY_GBRAIN_TOKENunset →503.
Client (tests/test_brain_ask*.py pattern):
SWARPH_BRAIN_GATEWAYset → POSTs/brain/querywith the peer token, parses{"chunks"}.SWARPH_BRAIN_GATEWAYunset → the existing direct-:8792+gbrain_token path is unchanged (compat lock).
Ship & rollout
- Version bump
0.25.0 → 0.26.0; document/brain/query+ the two gateway env vars +SWARPH_BRAIN_GATEWAYin the relevant help/README. - swarph-cli is public PyPI — synthetic test fixtures only, no cell-private data, no real tokens.
- All rollout steps commander-gated and OUT of this plan's execution scope (plan ends at merged + green):
0.26.0.
2. Set GATEWAY_GBRAIN_URL + GATEWAY_GBRAIN_TOKEN on the gateway host (lab-ovh) + restart the gateway.
3. Set SWARPH_BRAIN_GATEWAY for workstation-lc (+ future cells) → her peer token now authenticates to the brain.
4. Revoke the stopgap gbrain_ token placed on workstation-lc.
Security properties
- One identity: a cell's mesh token is its brain auth — no second credential to mint, deliver, or rotate.
- Held token stays server-side:
GATEWAY_GBRAIN_TOKENnever reaches a cell. - Over-scope contained: cells get read-only through the proxy even though the held token is
read/write/admin— this partly closes the over-scope issue without waiting on the pending read-scoped re-mint. - Attribution: every brain query carries the authenticated peer identity.
Out of scope (YAGNI)
- Per-peer rate / spend caps on
/brain/query(a future gateway concern; note, don't build). - A
/brain/searchvariant (the RRFquerycovers the need; add later if a keyword-only path is wanted). - Server-side synthesis (
brain-askalready synthesizes client-side via the $0 facade). - Modifying gbrain to natively accept mesh tokens (the proxy makes this unnecessary; if ever wanted, it is a separate upstream discussion).