security: API key auth on REST endpoints (#179)#195
Merged
Conversation
Closes the critical-severity unauthenticated REST API surface raised in issue #179. Adds an opt-in AGENT_BRAIN_API_KEY enforced via the X-API-Key header by a single `verify_api_key` FastAPI dependency, applied at the router level on all data routers (index, cache, folders, jobs, query, graph). The health router stays exempt by design. Startup gate refuses to bind any non-loopback host without a key set (exit 2 to match uvicorn's port-in-use convention so process managers can distinguish a misconfiguration from a crash). Loopback + no key keeps working with a loud warning to preserve single-user dev UX. When AGENT_BRAIN_API_KEY is set AND DEBUG=false, /docs, /redoc, and /openapi.json are mounted as None — there's no point requiring a key to hit endpoints if the schema describing them is publicly browsable. DEBUG=true keeps the docs open so developer workflows are unaffected. Constant-time compare via `secrets.compare_digest` for the key check. Tests: - tests/unit/api/test_security.py (6) — verify_api_key in isolation - tests/unit/api/test_auth_enforcement.py (20) — structural + behavioral (401/200) across all 6 gated routers, with a health- router exemption check - tests/unit/api/test_startup_gate.py (9) — exit-2 on non-loopback, warn on loopback, accept localhost/127.0.0.1/::1 aliases, /docs gating matrix across (key set/unset × DEBUG true/false) Server suite: 1304 passed / 28 skipped (was 1275 before this commit). Plan: docs/plans/2026-06-05-issue-179-api-key-auth.md Next: extend RuntimeState + propagate the key through CLI and MCP clients so enabling AGENT_BRAIN_API_KEY doesn't break either layer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes out the lockstep client-side work for issue #179: the agent-brain CLI and agent-brain-mcp both auto-resolve and send the X-API-Key header so that enabling AGENT_BRAIN_API_KEY on the server doesn't silently break either layer. RuntimeState extended: - `api_key: str | None = None` field — the running server publishes the configured key into `runtime.json` so other consumers can discover it without re-reading env / config. - `write_runtime()` chmods runtime.json to 0o600 since it may now carry a secret. - main.py `run()` populates the new field from `settings.AGENT_BRAIN_API_KEY` when constructing RuntimeState. CLI changes: - `DocServeClient(api_key=...)` and `DocServeClient.from_httpx(api_key=...)` inject `X-API-Key` into the underlying httpx.Client default headers. - `transport.open_client` calls a new `resolve_api_key()` that walks the precedence chain env → runtime.json → config.json → None and passes the result into both the HTTP and UDS construction paths. - `agent-brain init` now generates `secrets.token_urlsafe(32)` and writes it into project-local `.agent-brain/config.json` (chmod 0o600). A `--no-api-key` flag opts out for single-user loopback workflows. - `agent-brain start` exports the config.json key as AGENT_BRAIN_API_KEY in the server subprocess env (unless already set externally), so the server gates appropriately on first start without operator intervention. MCP changes: - New `_resolve_api_key(state_dir)` with precedence AGENT_BRAIN_MCP_API_KEY env → AGENT_BRAIN_API_KEY env → runtime.json::api_key → config.json::api_key → None. - `open_backend_client` injects the resolved key into both the HTTP and UDS httpx.Client default headers so the backend round-trip works against an authed Agent Brain server identically to the CLI. Documentation: - `.env.example` (repo root + agent-brain-server) document AGENT_BRAIN_API_KEY with the startup-gate / docs-gate semantics. - README quickstart calls out the new auto-generated key, the --no-api-key opt-out, and the non-loopback fail-fast. - CHANGELOG [Unreleased] entry under Security captures the full design, the BREAKING note for non-loopback users, the inventory of touched files, and the deferred CORS / OAuth follow-ups. Tests added across the three packages: - agent-brain-server/tests/unit/test_runtime.py — 3 new tests (chmod 0o600, api_key round-trip, default-None contract). - agent-brain-cli/tests/test_api_key_propagation.py — 11 tests covering DocServeClient + from_httpx header injection, resolve_api_key precedence (5 cases), and end-to-end through open_client. - agent-brain-cli/tests/test_init_api_key.py — 6 tests covering init key generation, --no-api-key opt-out, config.json 0o600, cross-project key uniqueness, and the post-init / pre-start config.json fallback for resolve_api_key. - agent-brain-mcp/tests/test_api_key_propagation.py — 8 tests covering MCP precedence chain (5 cases) and HTTP / UDS header injection. Verification (repo root): - `task before-push` — all checks passed, exit 0 (~3min). - `task pr-qa-gate` — passed, MCP coverage 91.89% (above 80% floor). Server suite: 1304 passed / 28 skipped. CLI suite: 433 passed. MCP suite: 468 passed / 96 deselected. Plan: docs/plans/2026-06-05-issue-179-api-key-auth.md Out of scope (separate follow-ups): CORS wildcard tightening, Authorization: Bearer / OAuth 2.1 (deferred to MCP v4 #188). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the critical-severity unauthenticated REST API surface raised in #179. Adds opt-in
AGENT_BRAIN_API_KEYenforced via theX-API-Keyheader by a singleverify_api_keyFastAPI dependency wired at the router level on every data router (/index,/index/cache,/index/folders,/index/jobs,/query,/graph). Health stays open by design. Server, CLI, and MCP move together so enabling auth doesn't silently break the MCP layer (the gap the original issue missed).Locked design decisions
sys.exit(2)so process managers can distinguish a misconfiguration from a crash). Preserves single-user dev UX; fail-fast for shared hosts.allow_origins=["*"]) tightening filed as a separate follow-up per the issue's own carve-out.AGENT_BRAIN_API_KEYis set ANDDEBUG=false,/docs,/redoc, and/openapi.jsonare also hidden — there's no point requiring auth for endpoints whose schema is publicly browsable.DEBUG=truekeeps them open.Surface touched
Server —
agent_brain_server/config/settings.pyaddsAGENT_BRAIN_API_KEY: str = "". Newagent_brain_server/api/security.py(≈40 LOC) holdsverify_api_key(constant-time compare viasecrets.compare_digest). 6 routers getdependencies=[Depends(verify_api_key)].api/main.pyfactors out_build_app()+_check_api_key_startup_gate(); the startup gate runs inrun()before uvicorn binds.runtime.pyextendsRuntimeStatewithapi_key: str | None = None;write_runtime()chmodsruntime.jsonto0o600since it may now carry a secret.CLI —
DocServeClient(api_key=...)andDocServeClient.from_httpx(api_key=...)injectX-API-Keyinto the underlyinghttpx.Clientdefault headers.transport.open_clientresolves via the newresolve_api_key()(env → runtime.json → config.json → None).agent-brain initgeneratessecrets.token_urlsafe(32)into project-local.agent-brain/config.json(chmod0o600);--no-api-keyopts out.agent-brain startexports the config.json key asAGENT_BRAIN_API_KEYin the server subprocess unless already set externally.MCP —
_resolve_api_key(state_dir)with precedenceAGENT_BRAIN_MCP_API_KEYenv →AGENT_BRAIN_API_KEYenv →runtime.json::api_key→config.json::api_key→ None.open_backend_clientinjects the resolved key into both HTTP and UDShttpx.Clientdefault headers.Docs —
.env.example(repo root + agent-brain-server) document the new variable. README quickstart calls out the auto-generated key, the--no-api-keyopt-out, and the non-loopback fail-fast.docs/CHANGELOG.md[Unreleased]Security entry captures the full inventory and the BREAKING note for non-loopback users.Tests added (43 new)
agent-brain-server/tests/unit/api/test_security.py— 6 tests forverify_api_keyin isolation (no-op when key empty, 401 missing / wrong, 200 correct, case-insensitive header).agent-brain-server/tests/unit/api/test_auth_enforcement.py— 20 tests covering structural (every gated router carries the dependency, health does not) and behavioral (401 without header, 200 with correct header) layers across all 6 gated routers + health exemption.agent-brain-server/tests/unit/api/test_startup_gate.py— 9 tests coveringexit 2on0.0.0.0, warn on loopback, all three loopback aliases (127.0.0.1/localhost/::1), silent when key set, and the/docsgating matrix across (key set/unset ×DEBUGtrue/false).agent-brain-server/tests/unit/test_runtime.py— 3 new tests (chmod0o600,api_keyround-trip, default-None contract preserves backwards compat).agent-brain-cli/tests/test_api_key_propagation.py— 11 tests coveringDocServeClient+from_httpxheader injection,resolve_api_keyprecedence (5 cases), end-to-end throughopen_client.agent-brain-cli/tests/test_init_api_key.py— 6 tests for init key generation,--no-api-keyopt-out,config.json0o600, cross-project key uniqueness, and the post-init / pre-start config.json fallback.agent-brain-mcp/tests/test_api_key_propagation.py— 8 tests for the MCP precedence chain (5 cases) and HTTP / UDS header injection.Test plan
task before-pushfrom repo root — exit 0 (~3min, runs format + lint + typecheck + tests across all 4 packages including the DR-5 monorepo gate)task pr-qa-gatefrom repo root — exit 0, MCP coverage 91.89% (above 80% floor)agent-brain-serveon default loopback, no key → starts with warning,curl /healthandcurl /queryboth 200 (no auth required path preserved)AGENT_BRAIN_API_KEY=xxx, restart →curl /querywithout header 401, with-H "X-API-Key: xxx"200,/health200 either wayAPI_HOST=0.0.0.0 agent-brain-servewithout key → exits 2 with clear log lineagent-brain query "test"end-to-end via CLI withruntime.json::api_keypresent → 200 (CLI auto-injects header)agent-brain-mcp --transport stdiowithAGENT_BRAIN_API_KEYenv set →search_documentstool succeeds against authed backendOut of scope (filed as separate follow-ups)
api/main.py:687-693usesallow_origins=["*"]withallow_credentials=True. Tracked separately per the issue's own carve-out.Authorization: Bearer/ OAuth 2.1 — covered by future MCP v4 (MCP v4: OAuth 2.1 for remote Agent Brain instances #188).X-API-Keyis the API-key idiom;Bearersemantics are for OAuth tokens.Plan
Full design and verification plan:
docs/plans/2026-06-05-issue-179-api-key-auth.mdCloses #179.
🤖 Generated with Claude Code