feat: capture relay — MCP capture tool for non-library harnesses (#55, capture 49f) #61

Merged
mathias merged 2 commits from feat/capture-mcp-relay into main 2026-06-23 05:26:55 +00:00
Owner

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)
  • Tests: forwards + preserves principal; unauth rejected; caller-cannot-forge-origin; confidential-via-us-nexus refused; sovereign allowed
  • 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

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)
mathias added 2 commits 2026-06-22 22:21:35 +00:00
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>
feat(mcp): capture relay tool — MCP door for non-library harnesses (#55)
CI / Lint / Test / Vet (pull_request) Successful in 13s
CI / Mirror to GitHub (pull_request) Has been skipped
7cf5bc221d
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>
mathias merged commit 76514215f4 into main 2026-06-23 05:26:55 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#61