capture #49d: POST /capture REST adapter + OAuth2 + I1 gate (server-derived origin) #53

Closed
opened 2026-06-22 20:27:28 +00:00 by mathias · 1 comment
Owner

Parent: #49 · Spec: specs/capture-bdd-spec.md §1, §4.1–4.2 · Depends on: #50 (tags), #51 (use-case), #52 (tracker) · Blocks: #49f

Scope

The HTTP door + the load-bearing I1 gate. Wire CaptureService (#51) behind POST /capture with OAuth2/JWT auth, implement the server-derived harness-origin → trust-zone mapping (Q2/§4.2), and enforce the I1 sovereignty gate using the classification tags (#50). After this, capture works end-to-end for harnesses that can run against the REST endpoint directly (Claude Code CLI, Agentsquad) — the relay (#49f) is not required for these.

This is where the gate stops being theoretical, so it's downstream of the taxonomy, the use-case, and the tracker.

What to build

  1. POST /capture adapter — JWT-authed (reuse existing Authentik/JWT middleware), thin: decode CaptureRequest → call CaptureService.Capture → encode CaptureReceipt. No business logic in the handler.
  2. Server-derived harness origin (Q2/§4.2): map the authenticated principal/origin → trust-zone (sovereign-soil / us-nexus). context.harness is descriptive-only, NEVER a gate input. A caller-asserted label that disagrees with the derived origin is logged as a security event.
  3. I1 sovereignty gate: resolve effective classification (caller-declared cross-checked against #50's derived tag, stricter wins); if effective = confidential AND origin = us-nexus → refuse before any write, name the invariant.
  4. HTTP semantics: 200 all-ok; 207-style body on partial; 4xx/5xx all-failed; dry_run validates + returns would-be receipt.
  5. Route registered; OpenAPI/handler docs updated.

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 origin → refuse before any write, names the invariant
  • Effective classification = stricter(declared, #50-derived); mismatch logged
  • HTTP status semantics (200 / 207-partial / 4xx-5xx); dry_run writes nothing
  • Tests: happy path, confidential-via-us-nexus-refused, confidential-via-sovereign-allowed, asserted-label-ignored, stricter-wins, partial-failure status, dry-run
  • task check green; secret-handling respected
  • If deployed as a service: manifest under infra/k3s/apps/** (I3) — coordinate with infra

Notes

Gherkin scenarios this makes live: the entire I1 sovereignty block + classification-model-(C) block in the spec. This is the milestone where "capture works" is true for direct-REST harnesses. The audit path (#49e) and relay (#49f) build on top.

**Parent:** #49 · **Spec:** `specs/capture-bdd-spec.md` §1, §4.1–4.2 · **Depends on:** #50 (tags), #51 (use-case), #52 (tracker) · **Blocks:** #49f ## Scope The HTTP door + the load-bearing I1 gate. Wire `CaptureService` (#51) behind `POST /capture` with OAuth2/JWT auth, implement the **server-derived harness-origin → trust-zone** mapping (Q2/§4.2), and enforce the I1 sovereignty gate using the classification tags (#50). After this, capture works end-to-end for harnesses that can run against the REST endpoint directly (Claude Code CLI, Agentsquad) — **the relay (#49f) is not required for these.** This is where the gate stops being theoretical, so it's downstream of the taxonomy, the use-case, and the tracker. ## What to build 1. **`POST /capture` adapter** — JWT-authed (reuse existing Authentik/JWT middleware), thin: decode `CaptureRequest` → call `CaptureService.Capture` → encode `CaptureReceipt`. No business logic in the handler. 2. **Server-derived harness origin (Q2/§4.2):** map the authenticated principal/origin → trust-zone (sovereign-soil / us-nexus). **`context.harness` is descriptive-only, NEVER a gate input.** A caller-asserted label that disagrees with the derived origin is logged as a security event. 3. **I1 sovereignty gate:** resolve effective classification (caller-declared cross-checked against #50's derived tag, stricter wins); if effective = confidential AND origin = us-nexus → **refuse before any write**, name the invariant. 4. **HTTP semantics:** 200 all-ok; 207-style body on partial; 4xx/5xx all-failed; `dry_run` validates + returns would-be receipt. 5. Route registered; OpenAPI/handler docs updated. ## 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 origin → refuse before any write, names the invariant - [ ] Effective classification = stricter(declared, #50-derived); mismatch logged - [ ] HTTP status semantics (200 / 207-partial / 4xx-5xx); `dry_run` writes nothing - [ ] Tests: happy path, confidential-via-us-nexus-refused, confidential-via-sovereign-allowed, asserted-label-ignored, stricter-wins, partial-failure status, dry-run - [ ] `task check` green; secret-handling respected - [ ] If deployed as a service: manifest under `infra/k3s/apps/**` (I3) — coordinate with infra ## Notes Gherkin scenarios this makes live: the entire I1 sovereignty block + classification-model-(C) block in the spec. This is the milestone where "capture works" is true for direct-REST harnesses. The audit path (#49e) and relay (#49f) build on top.
Author
Owner

Implemented — PR #59 (open, base main)

PR: #59 · branches from main (#50/#51/#52 merged).

I1 gate (in the use-case): CaptureContext.Origin (server-derived Zone); confidential + us-nexus → refuse before any write (ErrSovereigntyRefused), refusal audited. Asserted-vs-derived harness mismatch → security event. Gate fires only on explicit us-nexus so the unset default can't misfire.

REST adapter (capturehttp): auth (static wins, then Dex JWT) returns the principal + auth path the chassis middleware hides → derives origin via OriginResolver. Principal/origin server-set, overwriting any forged body (TestCallerCannotForgeOrigin). Status: 200 / 207 partial / 502 all-failed / 403 refusal / 400 validation.

Origin → zone (spec §4.2, made concrete): static token + allowlisted JWT subjects → sovereign; every other principal → us-nexus (fail safe). Sovereign JWT subjects via BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS.

Audit: minimal audit.SlogSink (I5 record to logs); #54 swaps in loki+buffer+degradation behind the same interface.

Wiring: /capture reuses the MCP server's graph-wired brain store (one impl, new BrainStore() accessor) + classification tags; mounts only when Gitea is configured.

All ACs met; task check green; token only ever in the auth header (never logged/argv).

Testability note: handler depends on a small Validator interface (chassis *JWTValidator satisfies it) so the JWT/origin path is testable without a live JWKS.

Deferred per the epic: real audit sink + degradation → #54; ai-sessions SummaryWriter → nil for now (summary item fails as partial, rest lands); k3s manifest (I3) → infra/#55.

Brain: wiki/hyperguild/decisions/gate-on-server-derived-signals-fail-safe.

Milestone: with #50–#53 merged, capture works end-to-end for direct-REST harnesses (Claude Code CLI, Agentsquad). Remaining: #54 (I5 audit path + classification-aware degradation) → #55 (central relay + I2 ledger, the heaviest security weight).

## Implemented — PR #59 (open, base main) **PR:** https://git.d-ma.be/mathias/hyperguild/pulls/59 · branches from `main` (#50/#51/#52 merged). **I1 gate (in the use-case):** `CaptureContext.Origin` (server-derived `Zone`); confidential + us-nexus → refuse **before any write** (`ErrSovereigntyRefused`), refusal audited. Asserted-vs-derived harness mismatch → security event. Gate fires only on explicit us-nexus so the unset default can't misfire. **REST adapter (`capturehttp`):** auth (static wins, then Dex JWT) returns the **principal + auth path** the chassis middleware hides → derives origin via `OriginResolver`. Principal/origin server-set, overwriting any forged body (`TestCallerCannotForgeOrigin`). Status: 200 / 207 partial / 502 all-failed / 403 refusal / 400 validation. **Origin → zone (spec §4.2, made concrete):** static token + allowlisted JWT subjects → sovereign; **every other principal → us-nexus (fail safe)**. Sovereign JWT subjects via `BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS`. **Audit:** minimal `audit.SlogSink` (I5 record to logs); #54 swaps in loki+buffer+degradation behind the same interface. **Wiring:** `/capture` reuses the MCP server's graph-wired brain store (one impl, new `BrainStore()` accessor) + classification tags; mounts only when Gitea is configured. All ACs met; `task check` green; token only ever in the auth header (never logged/argv). **Testability note:** handler depends on a small `Validator` interface (chassis `*JWTValidator` satisfies it) so the JWT/origin path is testable without a live JWKS. **Deferred per the epic:** real audit sink + degradation → #54; ai-sessions `SummaryWriter` → nil for now (summary item fails as partial, rest lands); k3s manifest (I3) → infra/#55. Brain: `wiki/hyperguild/decisions/gate-on-server-derived-signals-fail-safe`. **Milestone:** with #50–#53 merged, capture works end-to-end for direct-REST harnesses (Claude Code CLI, Agentsquad). Remaining: **#54** (I5 audit path + classification-aware degradation) → **#55** (central relay + I2 ledger, the heaviest security weight).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#53