Implements the relay code for #55 (capture 49f). The I2 ledger entry is merged first (infra #151), satisfying the shipping gate. This is the last sub-issue — closes "uniform across all harnesses."
The relay = an MCP capture tool
Per the decision: claude.ai chat / Crush / Pi / LLM Council can't run the use-case in-process and reach this server only via the existing /mcp OAuth connector — so the relay is an MCP tool, not a new service or raw-REST door. POST /capture (#53) stays the door for Code/CLI/Agentsquad/headless.
Thin / no standing visibility / audit-only: forwards to the same CaptureService as POST /capture; holds no state, retains nothing beyond the I5 audit record (#54). The six containment properties in the I2 ledger hold by construction.
Per-principal (ledger prop 4):ServeHTTP re-derives the caller's principal from the Bearer header — the chassis BearerMiddleware gates the request but discards the identity — and stashes it in context; the tool resolves the trust-zone origin from it.
No trust-zone upgrade (ledger prop 5): a caller-asserted harness/origin in the body is ignored. Origin is server-derived, so the I1 confidential refusal still fires for us-nexus callers (claude.ai's JWT principal isn't in the sovereign allowlist), while sovereign-allowlisted JWT principals and static-token (homelab CLI) callers pass.
One implementation: exported capturehttp.Authenticate + DecodeRequest so REST and MCP share the exact auth precedence + wire shape. Registered only when WithCapture is wired (tools()/handleCall/package doc).
OAuth2 token path for claude.ai (AC) — end-to-end, no new code
claude.ai already authenticates to brain-mcp via the configured Dex/OAuth custom-connector path (/.well-known/* + /oauth/token client-credentials in main). The capture tool rides that connector; its JWT subject resolves to us-nexus (not in BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS), so confidential is refused and internal/public proceeds — exactly the intended posture.
Acceptance criteria
Relay forwards capture, propagating the caller's own principal (no attribution-erasing shared identity)
Relay holds no standing visibility; retains nothing beyond the I5 audit log
OAuth2 token path for a non-library harness (claude.ai) end-to-end (existing /mcp connector)
Relay manifest under infra/k3s/apps/** (I3) — follow-up infra PR (see below)
Remaining: I3 manifest (infra, separate PR)
The relay ships in the existing ingestion image (auto-bumped by CI on merge), but the deployment needs:
env: BRAIN_GITEA_TOKEN (already required by #52), BRAIN_LOKI_URL, BRAIN_NTFY_URL/BRAIN_NTFY_TOKEN, BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS.
a persistent volume for <brain>/.audit-buffer/ (#54 — else durable-across-restart breaks on reschedule).
I'll prep that as an infra PR for your review (it touches a secret + a PV). Capture is fully functional for direct-REST harnesses today; the MCP door goes live once the ingestion deployment carries the env.
Implements the **relay code** for #55 (capture 49f). The I2 ledger entry is merged first (infra #151), satisfying the shipping gate. This is the last sub-issue — closes "uniform across **all** harnesses."
## The relay = an MCP `capture` tool
Per the decision: claude.ai chat / Crush / Pi / LLM Council can't run the use-case in-process and reach this server only via the **existing `/mcp` OAuth connector** — so the relay is an MCP tool, not a new service or raw-REST door. `POST /capture` (#53) stays the door for Code/CLI/Agentsquad/headless.
- **Thin / no standing visibility / audit-only:** forwards to the **same `CaptureService`** as `POST /capture`; holds no state, retains nothing beyond the I5 audit record (#54). The six containment properties in the I2 ledger hold by construction.
- **Per-principal (ledger prop 4):** `ServeHTTP` re-derives the caller's principal from the Bearer header — the chassis `BearerMiddleware` gates the request but discards the identity — and stashes it in context; the tool resolves the trust-zone origin from it.
- **No trust-zone upgrade (ledger prop 5):** a caller-asserted harness/origin in the body is ignored. Origin is server-derived, so the **I1 confidential refusal still fires** for us-nexus callers (claude.ai's JWT principal isn't in the sovereign allowlist), while sovereign-allowlisted JWT principals and static-token (homelab CLI) callers pass.
- **One implementation:** exported `capturehttp.Authenticate` + `DecodeRequest` so REST and MCP share the exact auth precedence + wire shape. Registered only when `WithCapture` is wired (tools()/handleCall/package doc).
## OAuth2 token path for claude.ai (AC) — end-to-end, no new code
claude.ai already authenticates to `brain-mcp` via the configured Dex/OAuth custom-connector path (`/.well-known/*` + `/oauth/token` client-credentials in `main`). The `capture` tool rides that connector; its JWT subject resolves to **us-nexus** (not in `BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS`), so confidential is refused and internal/public proceeds — exactly the intended posture.
## Acceptance criteria
- [x] Relay forwards capture, **propagating the caller's own principal** (no attribution-erasing shared identity)
- [x] Relay holds no standing visibility; retains nothing beyond the I5 audit log
- [x] OAuth2 token path for a non-library harness (claude.ai) end-to-end (existing /mcp connector)
- [x] Tests: forwards + preserves principal; unauth rejected; caller-cannot-forge-origin; confidential-via-us-nexus refused; sovereign allowed
- [x] `task check` green; secret-handling respected
- [ ] **Relay manifest under `infra/k3s/apps/**` (I3)** — follow-up infra PR (see below)
## Remaining: I3 manifest (infra, separate PR)
The relay ships in the existing ingestion image (auto-bumped by CI on merge), but the deployment needs:
- env: `BRAIN_GITEA_TOKEN` (already required by #52), `BRAIN_LOKI_URL`, `BRAIN_NTFY_URL`/`BRAIN_NTFY_TOKEN`, `BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS`.
- a **persistent volume** for `<brain>/.audit-buffer/` (#54 — else durable-across-restart breaks on reschedule).
I'll prep that as an infra PR for your review (it touches a secret + a PV). Capture is fully functional for direct-REST harnesses today; the MCP door goes live once the ingestion deployment carries the env.
Not auto-merged.
Refs #55
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Lifts the Bearer principal-derivation and the request→CaptureInput decode
out of the REST handler into exported package funcs, so the MCP capture
tool (#55 relay) reuses the exact same auth precedence and wire shape —
one implementation, not two. No behaviour change to POST /capture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the `capture` MCP tool: the #55 relay for harnesses that cannot run
the use-case in-process (claude.ai Chat/Cowork/Design, Crush, Pi, LLM
Council). They reach it through the existing /mcp OAuth connector.
- Thin: forwards to the SAME CaptureService as POST /capture; holds no
state and retains nothing beyond the I5 audit record. The containment
properties accepted in infra security-baseline (I2 ledger) hold by
construction.
- Per-principal: ServeHTTP re-derives the caller's principal from the
Bearer header (the chassis middleware gates but discards it) and stashes
it in context; the tool resolves the trust-zone origin from it. A
caller-asserted harness/origin in the body is ignored — origin is
server-derived, so the I1 confidential refusal still fires for us-nexus
callers (claude.ai), and sovereign-allowlisted JWT principals pass.
- Registered only when WithCapture is wired (all three sites: tools(),
handleCall, package doc); main wires REST + MCP from the same service,
resolver, and credentials.
Tests: listed-only-when-wired, forwards-via-static-principal,
confidential-via-us-nexus-refused, confidential-via-sovereign-allowed,
unauthenticated-rejected, caller-cannot-forge-origin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the relay code for #55 (capture 49f). The I2 ledger entry is merged first (infra #151), satisfying the shipping gate. This is the last sub-issue — closes "uniform across all harnesses."
The relay = an MCP
capturetoolPer the decision: claude.ai chat / Crush / Pi / LLM Council can't run the use-case in-process and reach this server only via the existing
/mcpOAuth connector — so the relay is an MCP tool, not a new service or raw-REST door.POST /capture(#53) stays the door for Code/CLI/Agentsquad/headless.CaptureServiceasPOST /capture; holds no state, retains nothing beyond the I5 audit record (#54). The six containment properties in the I2 ledger hold by construction.ServeHTTPre-derives the caller's principal from the Bearer header — the chassisBearerMiddlewaregates the request but discards the identity — and stashes it in context; the tool resolves the trust-zone origin from it.capturehttp.Authenticate+DecodeRequestso REST and MCP share the exact auth precedence + wire shape. Registered only whenWithCaptureis wired (tools()/handleCall/package doc).OAuth2 token path for claude.ai (AC) — end-to-end, no new code
claude.ai already authenticates to
brain-mcpvia the configured Dex/OAuth custom-connector path (/.well-known/*+/oauth/tokenclient-credentials inmain). Thecapturetool rides that connector; its JWT subject resolves to us-nexus (not inBRAIN_CAPTURE_SOVEREIGN_PRINCIPALS), so confidential is refused and internal/public proceeds — exactly the intended posture.Acceptance criteria
task checkgreen; secret-handling respectedinfra/k3s/apps/**(I3) — follow-up infra PR (see below)Remaining: I3 manifest (infra, separate PR)
The relay ships in the existing ingestion image (auto-bumped by CI on merge), but the deployment needs:
BRAIN_GITEA_TOKEN(already required by #52),BRAIN_LOKI_URL,BRAIN_NTFY_URL/BRAIN_NTFY_TOKEN,BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS.<brain>/.audit-buffer/(#54 — else durable-across-restart breaks on reschedule).I'll prep that as an infra PR for your review (it touches a secret + a PV). Capture is fully functional for direct-REST harnesses today; the MCP door goes live once the ingestion deployment carries the env.
Not auto-merged.
Refs #55
🤖 Generated with Claude Code