# Gateway Brain-Proxy Implementation Plan
> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Add a mesh-gateway endpoint POST /brain/query that authenticates a cell by its mesh token (via the gateway's existing _authorize) and proxies a read-only query to gbrain using one gateway-held token โ so no per-cell gbrain_ token exists.
Architecture: The gateway endpoint constructs the gbrain MCP query call itself (read-only by construction โ no write/admin path ever built), proxies it to gbrain over stdlib urllib.request wrapped in run_in_threadpool (no new runtime dep), and returns a clean JSON chunk array. The brain-ask client gains a SWARPH_BRAIN_GATEWAY branch that hits that endpoint with the cell's peer token; the direct-:8792 path is untouched.
Tech Stack: Python 3, FastAPI (gateway), urllib.request + starlette.concurrency.run_in_threadpool, pytest + fastapi.testclient.TestClient.
Global Constraints
- Read-only by construction โ the gateway ONLY ever builds an MCP call with
params.name == "query". No code path emits write/admin. A test asserts the upstream body is alwaysname=query. - Reuse
_authorize(authorization)verbatim (gateway/server.py:764) โ no new auth code. Bad/missing token โHTTPException(401)unchanged. GATEWAY_GBRAIN_URLdefaulthttp://100.107.222.72:8792/mcp;GATEWAY_GBRAIN_TOKENunset โ endpoint returns503. Read viaos.environ.get(...)at module top (matchesAUTH_TOKEN = os.environ.get("MESH_GATEWAY_TOKEN", "")atserver.py:62).- Client env
SWARPH_BRAIN_GATEWAY(distinct from the pre-existing--gateway/GBRAIN_MCP_URLwhich names the gbrain endpoint). When set โ gateway path with the peer token; when unset โ today's direct:8792path, byte-unchanged. - No new runtime dependency โ the gateway's outbound HTTP uses stdlib
urllib.requestin a threadpool. - Fail loud โ gbrain unreachable / non-2xx / unparseable โ
502(never a swallowed empty result). - Public PyPI โ synthetic test fixtures only; NO real tokens in code or tests.
- Version bump
0.25.0 โ 0.26.0in bothpyproject.tomlandsrc/swarph_cli/__init__.py. - TDD; plan ends at merged + green. Publish, gateway env config, re-pointing workstation-lc, and revoking the stopgap token are commander-gated and OUT of scope.
- Branch
feat/gateway-brain-proxyis checked out; stage only the named files per commit (nevergit add -A; untracked local-only.codegraph/must not be committed). - Test invocation:
cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest(namespace pkg, no install;-v pythonis not on PATH).
File Structure
src/swarph_cli/gateway/server.pyโ module-top config vars; a sync helper_brain_query_upstream(question, limit); a PydanticBrainQueryRequest; the@app.post("/brain/query")endpoint.src/swarph_cli/commands/brain_ask.pyโ a_gateway_query(...)helper; aSWARPH_BRAIN_GATEWAYbranch inrun_brain_ask.tests/test_gateway_brain.pyโ NEW; TestClient tests for the endpoint.tests/test_brain_ask_command.pyโ add client-path tests.pyproject.toml,src/swarph_cli/__init__.pyโ version bump.
---
Task 1: Gateway /brain/query endpoint + config
Files:
- Modify:
src/swarph_cli/gateway/server.py(config near:62; helper + model + endpoint near the other@app.postroutes) - Test:
tests/test_gateway_brain.py(create)
Interfaces:
- Consumes:
_authorize(authorization) -> AuthContext(server.py:764); the FastAPIapp. - Produces:
POST /brain/queryaccepting{"query": str, "limit": int=8}withBearerโ{"chunks": [ {slug,title,chunk_text,score,...}, ... ]}. Sync helper_brain_query_upstream(question: str, limit: int) -> list(buildsname=queryMCP body, POSTs to gbrain, returns the chunk array; raises on any upstream failure).
- [ ] Step 1: Write the failing tests โ create
tests/test_gateway_brain.py:
`python
import json
import os
import importlib
import pytest
from types import SimpleNamespace
def _load_app(monkeypatch, *, gbrain_token="gbrain_hostheld", auth="tok_shared"): monkeypatch.setenv("MESH_GATEWAY_TOKEN", auth) monkeypatch.setenv("GATEWAY_GBRAIN_TOKEN", gbrain_token) monkeypatch.setenv("GATEWAY_GBRAIN_URL", "http://127.0.0.1:8792/mcp") monkeypatch.setenv("MESH_DB_PATH", ":memory:") from swarph_cli.gateway import server importlib.reload(server) # re-read module-top env config return server
def _sse(chunks): inner = json.dumps(chunks) env = {"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": inner}]}} return "event: message\ndata: " + json.dumps(env) + "\n\n"
def _client(server): from fastapi.testclient import TestClient return TestClient(server.app)
def test_brain_query_authenticates_and_returns_chunks(monkeypatch): server = _load_app(monkeypatch) chunks = [{"slug": "s1", "title": "T", "chunk_text": "hello", "score": 0.9}] monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: chunks) r = _client(server).post("/brain/query", json={"query": "hi", "limit": 5}, headers={"Authorization": "Bearer tok_shared"}) assert r.status_code == 200 assert r.json() == {"chunks": chunks}
def test_brain_query_bad_token_401(monkeypatch): server = _load_app(monkeypatch) monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: []) r = _client(server).post("/brain/query", json={"query": "hi"}, headers={"Authorization": "Bearer wrong"}) assert r.status_code == 401
def test_brain_query_unconfigured_503(monkeypatch): server = _load_app(monkeypatch, gbrain_token="") # GATEWAY_GBRAIN_TOKEN unset r = _client(server).post("/brain/query", json={"query": "hi"}, headers={"Authorization": "Bearer tok_shared"}) assert r.status_code == 503
def test_brain_query_empty_query_400(monkeypatch): server = _load_app(monkeypatch) monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: []) r = _client(server).post("/brain/query", json={"query": " "}, headers={"Authorization": "Bearer tok_shared"}) assert r.status_code == 400
def test_brain_query_upstream_error_502(monkeypatch): server = _load_app(monkeypatch) def boom(q, n): raise RuntimeError("gbrain down") monkeypatch.setattr(server, "_brain_query_upstream", boom) r = _client(server).post("/brain/query", json={"query": "hi"}, headers={"Authorization": "Bearer tok_shared"}) assert r.status_code == 502
def test_upstream_helper_is_read_only_by_construction(monkeypatch):
"""The MCP body the proxy sends upstream ALWAYS has params.name == 'query'."""
server = _load_app(monkeypatch)
captured = {}
class FakeResp:
def read(self): return _sse([{"slug": "s"}]).encode()
def __enter__(self): return self
def __exit__(self, *a): return False
def fake_urlopen(req, timeout=None):
captured["body"] = json.loads(req.data.decode())
captured["auth"] = req.get_header("Authorization")
return FakeResp()
monkeypatch.setattr(server.urllib.request, "urlopen", fake_urlopen)
out = server._brain_query_upstream("anything", 8)
assert out == [{"slug": "s"}]
assert captured["body"]["params"]["name"] == "query" # read-only lock
assert captured["auth"] == "Bearer gbrain_hostheld" # gateway's held token
`
- [ ] Step 2: Run the tests to verify they fail
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_gateway_brain.py -v
Expected: FAIL โ AttributeError / 404 (endpoint + _brain_query_upstream don't exist yet).
- [ ] Step 3: Add the config vars โ in
server.py, near the other module-top config (AUTH_TOKEN = os.environ.get("MESH_GATEWAY_TOKEN", "")~:62), add:
`python
GATEWAY_GBRAIN_URL = os.environ.get("GATEWAY_GBRAIN_URL", "http://100.107.222.72:8792/mcp")
GATEWAY_GBRAIN_TOKEN = os.environ.get("GATEWAY_GBRAIN_TOKEN", "")
`
Confirm import urllib.request and import json are present at the top of server.py (add import urllib.request if missing).
- [ ] Step 4: Add the upstream helper โ in
server.py(module level, near other helpers). This is the ONLY place the MCP body is built, so read-only lives here:
`python
def _brain_query_upstream(question: str, limit: int) -> list:
"""Proxy a READ-ONLY gbrain query. Builds the MCP query call itself (never
write/admin), POSTs to GATEWAY_GBRAIN_URL with the gateway-held token, returns
the chunk array. Raises on any upstream/parse failure (caller maps to 502)."""
body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "query",
"arguments": {"query": question, "limit": limit, "expand": False}}}
req = urllib.request.Request(
GATEWAY_GBRAIN_URL, data=json.dumps(body).encode("utf-8"), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "application/json, text/event-stream")
req.add_header("Authorization", f"Bearer {GATEWAY_GBRAIN_TOKEN}")
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 โ fixed tailnet URL
raw = resp.read().decode("utf-8")
payload = raw
if "data:" in raw:
for line in raw.splitlines():
s = line.strip()
if s.startswith("data:"):
payload = s[len("data:"):].strip()
break
doc = json.loads(payload)
parsed = json.loads(doc["result"]["content"][0]["text"])
return parsed if isinstance(parsed, list) else []
`
- [ ] Step 5: Add the request model + endpoint โ in
server.py, near the other@app.postroutes. Add the importfrom starlette.concurrency import run_in_threadpoolat the top if absent, and (with the other Pydantic models)from pydantic import BaseModel, Fieldis already imported (the file defines request models likeMessagePostRequest):
`python
class BrainQueryRequest(BaseModel):
query: str
limit: int = 8
@app.post("/brain/query")
async def brain_query(req: BrainQueryRequest,
authorization: Optional[str] = Header(None)) -> dict:
auth = _authorize(authorization) # 401 on bad mesh token
if not GATEWAY_GBRAIN_TOKEN:
raise HTTPException(503, "brain proxy not configured")
if not req.query.strip():
raise HTTPException(400, "query is required")
try:
chunks = await run_in_threadpool(_brain_query_upstream, req.query, req.limit)
except Exception as e: # fail loud โ never a swallowed empty result
log.warning("brain proxy upstream error for peer=%s: %s", auth.peer, e)
raise HTTPException(502, "brain upstream error")
log.info("brain query peer=%s regime=%s limit=%d chunks=%d",
auth.peer, auth.regime, req.limit, len(chunks))
return {"chunks": chunks}
`
- [ ] Step 6: Run the tests to verify they pass
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_gateway_brain.py -v
Expected: PASS โ all six tests green.
- [ ] Step 7: Commit
`bash
cd /home/ubuntu/swarph-cli
git add src/swarph_cli/gateway/server.py tests/test_gateway_brain.py
git commit -m "feat(gateway): POST /brain/query โ mesh-token auth, read-only gbrain proxy
Reuses _authorize; builds the MCP query itself (read-only by construction);
proxies with one gateway-held token; 401/400/502/503; per-peer attribution."
`
---
Task 2: brain-ask gateway client path
Files:
- Modify:
src/swarph_cli/commands/brain_ask.py(add_gateway_query; branch inrun_brain_ask~:161-170) - Test:
tests/test_brain_ask_command.py
Interfaces:
- Consumes:
_peer_token_path(self_name)(:87),_self_name()(:91),_http_post(url, body, token, accept, timeout)(:112). - Produces:
_gateway_query(gw_base: str, peer_token: str, question: str, limit: int) -> listโ POSTswith/brain/query {"query","limit"}+Bearer peer_token, returns thechunkslist.run_brain_askuses it whenSWARPH_BRAIN_GATEWAYis set.
- [ ] Step 1: Write the failing tests โ add to
tests/test_brain_ask_command.py:
`python
def test_gateway_query_posts_brain_query_with_peer_token(monkeypatch, tmp_path):
from swarph_cli.commands import brain_ask
captured = {}
def fake_http_post(url, body, token, accept="application/json, text/event-stream", timeout=30):
captured.update(url=url, body=body, token=token)
import json as _j
return _j.dumps({"chunks": [{"slug": "s1", "chunk_text": "x"}]})
monkeypatch.setattr(brain_ask, "_http_post", fake_http_post)
out = brain_ask._gateway_query("http://gw:8788", "peer_tok_123", "hello", 5)
assert out == [{"slug": "s1", "chunk_text": "x"}]
assert captured["url"] == "http://gw:8788/brain/query"
assert captured["body"] == {"query": "hello", "limit": 5}
assert captured["token"] == "peer_tok_123"
def test_run_brain_ask_uses_gateway_when_env_set(monkeypatch, tmp_path, capsys): from swarph_cli.commands import brain_ask # peer token on disk for _self_name() cfg = tmp_path / ".config" / "swarph" cfg.mkdir(parents=True) (cfg / "wlc.peer_token").write_text("peer_tok_xyz", encoding="utf-8") monkeypatch.setattr(brain_ask.Path, "home", classmethod(lambda cls: tmp_path)) monkeypatch.setenv("SWARPH_SELF", "wlc") monkeypatch.setenv("SWARPH_BRAIN_GATEWAY", "http://gw:8788") seen = {} def fake_gateway_query(gw, tok, q, n): seen.update(gw=gw, tok=tok, q=q) return [{"slug": "s1", "chunk_text": "hit"}] monkeypatch.setattr(brain_ask, "_gateway_query", fake_gateway_query) rc = brain_ask.run_brain_ask(["--no-synth", "what", "is", "up"]) assert rc == 0 assert seen["gw"] == "http://gw:8788" and seen["tok"] == "peer_tok_xyz" assert "s1" in capsys.readouterr().out
def test_run_brain_ask_direct_path_unchanged_when_gateway_unset(monkeypatch, capsys):
from swarph_cli.commands import brain_ask
monkeypatch.delenv("SWARPH_BRAIN_GATEWAY", raising=False)
monkeypatch.setenv("GBRAIN_TOKEN", "gbrain_direct")
called = {}
monkeypatch.setattr(brain_ask, "_mcp_query",
lambda url, tok, q, n: called.setdefault("hit", (url, tok)) or [{"slug": "d"}])
rc = brain_ask.run_brain_ask(["--no-synth", "hello"])
assert rc == 0
assert called["hit"][1] == "gbrain_direct" # direct path used the gbrain token
assert "d" in capsys.readouterr().out
`
- [ ] Step 2: Run to verify they fail
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_brain_ask_command.py -k "gateway or direct_path" -v
Expected: FAIL โ _gateway_query undefined; run_brain_ask has no gateway branch.
- [ ] Step 3: Add
_gateway_queryโ inbrain_ask.py, after_mcp_query(:127):
`python
def _gateway_query(gw_base: str, peer_token: str, question: str, limit: int) -> list:
"""Query the brain via the mesh gateway's /brain/query proxy, authenticating
with the cell's MESH peer token. The gateway holds the gbrain token; we never do."""
url = gw_base.rstrip("/") + "/brain/query"
raw = _http_post(url, {"query": question, "limit": limit}, peer_token,
accept="application/json")
return json.loads(raw).get("chunks", [])
`
- [ ] Step 4: Branch in
run_brain_askโ replace the retrieval block. Current (:161-170):
`python
token = _resolve_token(args.token_file, _self_name())
if not token:
sys.stderr.write(
"swarph brain-ask: no gbrain read token "
"(set GBRAIN_TOKEN / SWARPH_BRAIN_TOKEN, pass --token-file, or place a "
"mesh peer token at ~/.config/swarph/
try:
chunks = _mcp_query(args.gateway, token, question, args.limit)
`
New:
`python
gw = os.environ.get("SWARPH_BRAIN_GATEWAY")
if gw:
self_name = _self_name()
try:
peer_token = _peer_token_path(self_name).read_text(encoding="utf-8").strip()
except OSError:
peer_token = ""
if not peer_token:
sys.stderr.write(
f"swarph brain-ask: SWARPH_BRAIN_GATEWAY set but no mesh peer token at "
f"~/.config/swarph/{self_name}.peer_token\n")
return 2
try:
chunks = _gateway_query(gw, peer_token, question, args.limit)
except Exception as e: # noqa: BLE001 โ surface, don't swallow
sys.stderr.write(f"swarph brain-ask: gateway brain query failed: {e}\n")
return 1
else:
token = _resolve_token(args.token_file, _self_name())
if not token:
sys.stderr.write(
"swarph brain-ask: no gbrain read token "
"(set GBRAIN_TOKEN / SWARPH_BRAIN_TOKEN, pass --token-file, or place a "
"mesh peer token at ~/.config/swarph/`
(Keep the rest of the try body โ the except/synthesis/print โ exactly as it is after this line; only the retrieval source changes.)
- [ ] Step 5: Run to verify they pass + the file's existing tests still pass
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_brain_ask_command.py -v
Expected: PASS โ new gateway + compat tests pass, all pre-existing brain-ask tests still pass.
- [ ] Step 6: Commit
`bash
cd /home/ubuntu/swarph-cli
git add src/swarph_cli/commands/brain_ask.py tests/test_brain_ask_command.py
git commit -m "feat(brain-ask): SWARPH_BRAIN_GATEWAY path โ query the brain with the mesh peer token
When set, POSTs the gateway /brain/query proxy with the cell's peer token; the
direct :8792 + gbrain_ token path is unchanged when unset."
`
---
Task 3: Version bump 0.26.0 + docs + full-suite gate
Files:
- Modify:
pyproject.toml,src/swarph_cli/__init__.py,src/swarph_cli/commands/brain_ask.py(docstring env note) - Test:
tests/test_brain_ask_command.py(version-independent) + full suite
Interfaces: none (release bookkeeping + docs).
- [ ] Step 1: Write the failing test โ add to
tests/test_brain_ask_command.py:
`python
def test_version_is_0_26_0():
import swarph_cli
assert swarph_cli.__version__ == "0.26.0"
`
- [ ] Step 2: Run to verify it fails
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_brain_ask_command.py::test_version_is_0_26_0 -v
Expected: FAIL โ assert '0.25.0' == '0.26.0'.
- [ ] Step 3: Bump both pins + document the env var
src/swarph_cli/__init__.py (line with __version__ = "0.25.0") โ __version__ = "0.26.0".
pyproject.toml (line version = "0.25.0") โ version = "0.26.0".
In brain_ask.py, add to the module docstring's env section (near the GBRAIN_TOKEN note ~:14):
`
SWARPH_BRAIN_GATEWAY when set, query the brain via the mesh gateway's
/brain/query proxy using the cell's mesh peer token
(no per-cell gbrain_ token). Unset = direct :8792.
`
- [ ] Step 4: Run the version test + the FULL suite
Run: cd /home/ubuntu/swarph-cli && PYTHONPATH=src python3 -m pytest tests/test_brain_ask_command.py::test_version_is_0_26_0 -v && PYTHONPATH=src python3 -m pytest -q
Expected: PASS โ version test passes; entire repo suite green (no regression).
- [ ] Step 5: Commit
`bash
cd /home/ubuntu/swarph-cli
git add pyproject.toml src/swarph_cli/__init__.py src/swarph_cli/commands/brain_ask.py tests/test_brain_ask_command.py
git commit -m "chore(release): bump swarph-cli 0.25.0 -> 0.26.0 (gateway brain-proxy)"
`
---
Self-Review
1. Spec coverage:
POST /brain/query+ reuse_authorize+ read-only-by-construction โ Task 1 (_brain_query_upstreambuildsname=queryonly;test_upstream_helper_is_read_only_by_construction). โGATEWAY_GBRAIN_URL/GATEWAY_GBRAIN_TOKEN, unsetโ503 โ Task 1 (config +test_brain_query_unconfigured_503). โ- Errors 401/400/502/503 โ Task 1 tests. โ
- Client
SWARPH_BRAIN_GATEWAYpeer-token path + direct-:8792backward-compat โ Task 2 (_gateway_query, branch,test_run_brain_ask_direct_path_unchanged_when_gateway_unset). โ - No new runtime dep (
urllib+run_in_threadpool) โ Task 1 Steps 3โ5. โ - Fail-loud 502 โ Task 1 endpoint +
test_brain_query_upstream_error_502. โ - Version 0.26.0 both files + docs โ Task 3. โ
- Public PyPI / synthetic fixtures / no real tokens โ all tests use
gbrain_hostheld/peer_tok_*literals + mocks. โ - Rollout (publish/env/re-point/revoke) โ out of scope, absent from tasks. โ
2. Placeholder scan: No TBD/TODO; every code step shows full before/after. โ
3. Type consistency: _brain_query_upstream(question, limit) -> list and _gateway_query(gw_base, peer_token, question, limit) -> list are used identically in impl + tests. Endpoint returns {"chunks": list}; client reads .get("chunks", []). BrainQueryRequest(query, limit=8). Env names consistent: GATEWAY_GBRAIN_URL/TOKEN (server), SWARPH_BRAIN_GATEWAY (client). โ
Note for the executor: line anchors (:62, :161-170, etc.) are pre-change references; after edits later anchors shift โ locate by the quoted code, not the raw number. Confirm Header, HTTPException, Optional, BaseModel, and log are already imported in server.py (they are โ used by existing routes); add only urllib.request and run_in_threadpool if missing.