Implements #53 (capture 49d) — the HTTP door + the load-bearing I1 gate. "Capture works" end-to-end for direct-REST harnesses after this. Branches from main (deps #50/#51/#52 already merged).
What landed
I1 sovereignty gate (in the use-case, capture/):
CaptureContext.Origin (a server-derived Zone). The service refuses before any write when effective classification is confidential AND origin is us-nexus (ErrSovereigntyRefused), and audits the refusal (refusals must be reconstructable too).
A caller-asserted harness label naming a different zone than the derived origin → security event (context.Harness is descriptive-only, never a gate input — a control keyed on attacker-suppliable input is not a control).
Gate fires only on explicit ZoneUSNexus, so the unset default can't make it misfire; the adapter always sets a concrete zone.
REST adapter (capturehttp/):
POST /capture: authenticate → derive origin → decode → capture.Service → map receipt to status. Thin; no business logic.
Auth mirrors the chassis Bearer precedence (static token wins, then Dex JWT) but returns the principal + auth path — the chassis middleware hides both, and capture needs the principal to derive origin. Depends on a small Validator interface (the chassis *JWTValidator satisfies it) so the JWT/origin path is testable without a live JWKS.
OriginResolver: static-token caller + allowlisted JWT subjects → sovereign; every other principal → us-nexus (fail safe), so confidential is refused by default for unknown identities — same fail-safe-to-strict philosophy as #50. Principal/origin are server-set on the input, overwriting anything the caller's body asserted (TestCallerCannotForgeOrigin).
audit/ — minimal SlogSink: emits the I5 request-level record to structured logs (alloy/loki scrapes them), security events at warn. Placeholder behind AuditSink; #54 swaps in the loki+buffer+reconcile sink (confidential fails closed, internal degrades) without touching callers.
Wiring (main):/capture mounts behind the same static+JWT creds as /mcp, reuses the MCP server's graph-wired brain store (one implementation, via a new BrainStore() accessor) and the classification tags. Mounts only when a Gitea tracker is configured. Sovereign JWT principals via BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS (comma-sep).
Origin → trust-zone mapping (the spec §4.2 decision, made concrete)
The spec left the exact mechanism to implementation. Chosen, server-side only:
Auth path
Principal
Zone
static token
local-cli (homelab/Tailscale)
sovereign
JWT, subject in allowlist
e.g. koala-cli
sovereign
JWT, any other subject (e.g. claude.ai)
token subject
us-nexus (fail safe)
Acceptance criteria
POST /capture JWT-authed, thin adapter (decode → use-case → encode)
Harness origin server-derived from authenticated principal; context.harness not a gate input
Asserted-vs-derived origin mismatch logged as a security event
I1 gate: confidential + us-nexus → refuse before any write, names the invariant
task check green; secret-handling respected (token only in auth header, never logged/argv)
Deferred (correctly, per the epic)
Real audit sink (loki + buffer + classification-aware degradation) → #54. The slog sink never fails, so the I5 "refuse if unauditable" floor isn't exercised yet.
ai-sessions SummaryWriter impl → nil for now; a request with a summary fails that one item (partial receipt), everything else lands.
Implements #53 (capture 49d) — the HTTP door + the load-bearing I1 gate. **"Capture works" end-to-end for direct-REST harnesses after this.** Branches from `main` (deps #50/#51/#52 already merged).
## What landed
**I1 sovereignty gate (in the use-case, `capture/`):**
- `CaptureContext.Origin` (a server-derived `Zone`). The service refuses **before any write** when effective classification is `confidential` AND origin is `us-nexus` (`ErrSovereigntyRefused`), and **audits the refusal** (refusals must be reconstructable too).
- A caller-asserted harness label naming a different zone than the derived origin → **security event** (`context.Harness` is descriptive-only, never a gate input — a control keyed on attacker-suppliable input is not a control).
- Gate fires only on explicit `ZoneUSNexus`, so the unset default can't make it misfire; the adapter always sets a concrete zone.
**REST adapter (`capturehttp/`):**
- `POST /capture`: authenticate → derive origin → decode → `capture.Service` → map receipt to status. Thin; no business logic.
- Auth mirrors the chassis Bearer precedence (static token wins, then Dex JWT) but returns the **principal + auth path** — the chassis middleware hides both, and capture needs the principal to derive origin. Depends on a small `Validator` interface (the chassis `*JWTValidator` satisfies it) so the JWT/origin path is testable without a live JWKS.
- **`OriginResolver`:** static-token caller + allowlisted JWT subjects → `sovereign`; **every other principal → `us-nexus` (fail safe)**, so confidential is refused by default for unknown identities — same fail-safe-to-strict philosophy as #50. Principal/origin are server-set on the input, overwriting anything the caller's body asserted (`TestCallerCannotForgeOrigin`).
- HTTP status: **200** all-ok/dry-run · **207** partial · **502** all-failed · **403** I1 refusal · **400** fail-closed validation.
**`audit/` — minimal `SlogSink`:** emits the I5 request-level record to structured logs (alloy/loki scrapes them), security events at warn. Placeholder behind `AuditSink`; #54 swaps in the loki+buffer+reconcile sink (confidential fails closed, internal degrades) without touching callers.
**Wiring (`main`):** `/capture` mounts behind the same static+JWT creds as `/mcp`, reuses the MCP server's **graph-wired brain store** (one implementation, via a new `BrainStore()` accessor) and the classification tags. Mounts only when a Gitea tracker is configured. Sovereign JWT principals via `BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS` (comma-sep).
## Origin → trust-zone mapping (the spec §4.2 decision, made concrete)
The spec left the exact mechanism to implementation. Chosen, server-side only:
| Auth path | Principal | Zone |
|---|---|---|
| static token | `local-cli` (homelab/Tailscale) | sovereign |
| JWT, subject in allowlist | e.g. `koala-cli` | sovereign |
| JWT, any other subject (e.g. claude.ai) | token subject | **us-nexus (fail safe)** |
## Acceptance criteria
- [x] `POST /capture` JWT-authed, thin adapter (decode → use-case → encode)
- [x] Harness origin **server-derived** from authenticated principal; `context.harness` not a gate input
- [x] Asserted-vs-derived origin mismatch logged as a security event
- [x] I1 gate: confidential + us-nexus → refuse before any write, names the invariant
- [x] Effective classification = stricter(declared, #50-derived); mismatch logged
- [x] HTTP status semantics (200 / 207 / 403 / 400 / 502); `dry_run` writes nothing
- [x] Tests: happy, confidential-via-us-nexus-refused, confidential-via-sovereign-allowed, static-is-sovereign, caller-cannot-forge-origin, stricter-wins (service), partial→207, dry-run, 401, validation→400
- [x] `task check` green; secret-handling respected (token only in auth header, never logged/argv)
## Deferred (correctly, per the epic)
- **Real audit sink (loki + buffer + classification-aware degradation) → #54.** The slog sink never fails, so the I5 "refuse if unauditable" floor isn't exercised yet.
- **ai-sessions `SummaryWriter` impl** → nil for now; a request with a summary fails that one item (partial receipt), everything else lands.
- **k3s manifest (I3)** → infra repo (#55).
Not auto-merged — review + merge from claude.ai.
Closes #53
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds the trust-zone Origin to CaptureContext and the I1 gate to the
use-case: a confidential effective classification through a us-nexus
origin is refused before ANY write (ErrSovereigntyRefused), and the
refusal is itself audited. A caller-asserted harness label that names a
different zone than the server-derived origin is logged as a security
event — context.Harness is descriptive-only, never a gate input.
The gate triggers only on an explicit ZoneUSNexus, so the unset default
(ZoneUnknown) can never make it fire on caller-controllable input; the
REST adapter always sets a concrete zone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emits the request-level audit record to structured logs (scraped by the
existing alloy/loki substrate) and surfaces security events at warn
level. Placeholder behind the AuditSink interface — the classification-
aware loki+buffer+reconcile sink (confidential fails closed, internal
degrades) lands in #54 and replaces this without touching callers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The HTTP door for the capture capability. Thin: authenticate → derive
trust-zone origin → decode → capture.Service → map receipt to status.
- Auth mirrors the chassis Bearer precedence (static token wins, then Dex
JWT) but returns the resolved principal + auth path, which the chassis
middleware hides — capture needs the principal to derive the origin.
Depends on a small Validator interface (the chassis *JWTValidator
satisfies it) so the JWT/origin path is testable without a live JWKS.
- OriginResolver maps principal → trust zone: static-token caller and
allowlisted JWT subjects → sovereign; every other principal → us-nexus
(fail safe, so the I1 gate refuses confidential by default). Principal
and origin are server-set on the input, overwriting any body the caller
sent.
- HTTP status: 200 all-ok / dry-run, 207 partial, 502 all-failed, 403 on
the I1 refusal, 400 on fail-closed validation.
- Wired in main behind the same static+JWT credentials as /mcp, reusing
the MCP server's graph-wired brain store (one implementation) and the
classification tags (#50). Mounts only when a Gitea tracker is
configured. Sovereign JWT principals via BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS.
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 #53 (capture 49d) — the HTTP door + the load-bearing I1 gate. "Capture works" end-to-end for direct-REST harnesses after this. Branches from
main(deps #50/#51/#52 already merged).What landed
I1 sovereignty gate (in the use-case,
capture/):CaptureContext.Origin(a server-derivedZone). The service refuses before any write when effective classification isconfidentialAND origin isus-nexus(ErrSovereigntyRefused), and audits the refusal (refusals must be reconstructable too).context.Harnessis descriptive-only, never a gate input — a control keyed on attacker-suppliable input is not a control).ZoneUSNexus, so the unset default can't make it misfire; the adapter always sets a concrete zone.REST adapter (
capturehttp/):POST /capture: authenticate → derive origin → decode →capture.Service→ map receipt to status. Thin; no business logic.Validatorinterface (the chassis*JWTValidatorsatisfies it) so the JWT/origin path is testable without a live JWKS.OriginResolver: static-token caller + allowlisted JWT subjects →sovereign; every other principal →us-nexus(fail safe), so confidential is refused by default for unknown identities — same fail-safe-to-strict philosophy as #50. Principal/origin are server-set on the input, overwriting anything the caller's body asserted (TestCallerCannotForgeOrigin).audit/— minimalSlogSink: emits the I5 request-level record to structured logs (alloy/loki scrapes them), security events at warn. Placeholder behindAuditSink; #54 swaps in the loki+buffer+reconcile sink (confidential fails closed, internal degrades) without touching callers.Wiring (
main):/capturemounts behind the same static+JWT creds as/mcp, reuses the MCP server's graph-wired brain store (one implementation, via a newBrainStore()accessor) and the classification tags. Mounts only when a Gitea tracker is configured. Sovereign JWT principals viaBRAIN_CAPTURE_SOVEREIGN_PRINCIPALS(comma-sep).Origin → trust-zone mapping (the spec §4.2 decision, made concrete)
The spec left the exact mechanism to implementation. Chosen, server-side only:
local-cli(homelab/Tailscale)koala-cliAcceptance criteria
POST /captureJWT-authed, thin adapter (decode → use-case → encode)context.harnessnot a gate inputdry_runwrites nothingtask checkgreen; secret-handling respected (token only in auth header, never logged/argv)Deferred (correctly, per the epic)
SummaryWriterimpl → nil for now; a request with a summary fails that one item (partial receipt), everything else lands.Not auto-merged — review + merge from claude.ai.
Closes #53
🤖 Generated with Claude Code