diff --git a/specs/capture-implementation-report.md b/specs/capture-implementation-report.md new file mode 100644 index 0000000..f05eea0 --- /dev/null +++ b/specs/capture-implementation-report.md @@ -0,0 +1,116 @@ +# Capture capability — implementation report (as-built) + +**Status:** Shipped 2026-06-23, tagged `v0.11.0`. Epic hyperguild #49 (sub-issues #50–#55) closed. +**Spec:** `specs/capture-bdd-spec.md` (the design contract this implements). +**Governed by:** `infra/docs/architecture/01-invariants.md` (I1–I5) + the I2 acceptance ledger entry in `infra/docs/security-baseline.md`. + +This document records what was actually built, where it lives, how it maps to the spec, and what was deferred — for onboarding and future audit. It does not restate the design rationale (see the spec and the linked brain entries). + +--- + +## 1. Outcome + +One uniform capture capability — insights → brain, action items → Gitea tickets, optional summary → ai-sessions — reachable identically from every harness: + +- **In-process / direct-REST harnesses** (Claude Code CLI, Agentsquad, claude.ai Code, headless): `POST /capture` on the brain server. +- **MCP-native harnesses** (claude.ai Chat/Cowork/Design, Crush, Pi, LLM Council): the `capture` MCP tool, reached over the existing `/mcp` OAuth connector. + +Both doors call the **same** `CaptureService`; only the transport and credential assembly differ. The persistence behaviour (validation, classification, I1 gate, orchestration, I5 audit, partial receipt) is written once. + +--- + +## 2. Architecture (as-built) + +``` + POST /capture (REST) capture MCP tool + capturehttp.Handler mcp.Server.brainCapture + \ / + \ (auth → principal → / + \ origin; decode) / + v v + capture.CaptureService (use-case, pure) + ┌───────────────┬───────────────┬──────────────┬───────────────┐ + BrainStore IssueTracker SummaryWriter ClassificationPolicy AuditSink + brainstore. gitea.Client (nil today) classification.Config audit.Degrading + Store (REST) Sink / SlogSink + │ │ + api.WriteNote/UpdateNote/ReadNote (#45) LokiCentral + FileBuffer + + wing index + auto-tunnel + graph re-index + NtfyNotifier + Reconcile +``` + +- **`internal/capture/`** — the use-case + ports + entities. Pure; no I/O. Owns validation (fail-closed), effective-classification resolution (stricter wins), the **I1 sovereignty gate**, best-effort orchestration, the **two-phase I5 audit** (Reserve before writes / Record after), and the partial-aware receipt. +- **`internal/brainstore/`** — concrete `BrainStore` wrapping the #45 `api` primitives + wiki upkeep (wing `_index`, auto-tunnel, graph re-index). The MCP `brain_write`/`brain_update`/`brain_get` handlers were re-pointed at it: one implementation, not two. +- **`internal/classification/`** — `public < internal < confidential` taxonomy + per-wing/repo tags from an optional `classification.yaml`; fail-safe to confidential. +- **`internal/gitea/`** — `IssueTracker` over the Gitea REST API; owner forced to `mathias`; token only in the Authorization header. +- **`internal/capturehttp/`** — the REST adapter + the shared `Authenticate` / `DecodeRequest` / `OriginResolver` (also used by the MCP tool). +- **`internal/audit/`** — `SlogSink` (default) and the `DegradingSink` (loki + durable `FileBuffer` + `NtfyNotifier` + `Reconcile`). +- **`internal/mcp/`** — the `capture` relay tool + principal threading (re-derives the caller's principal from the Bearer header the chassis middleware discards). + +--- + +## 3. Sub-issue → PR map + +| Sub | Issue | PR(s) | Delivered | +|-----|-------|-------|-----------| +| 49a | #50 | #56 | classification taxonomy + per-wing/repo tags (fail-safe to confidential) | +| 49b | #51 | #57 | `CaptureService` use-case + ports + entities; `BrainStore` extraction (MCP re-pointed) | +| 49c | #52 | #58 | Gitea `IssueTracker` (owner forced mathias; token never logged) | +| 49d | #53 | #59 | `POST /capture` REST + OAuth2 + I1 sovereignty gate (server-derived origin) | +| 49e | #54 | #60 | I5 audit path + classification-aware degradation (loki + buffer + reconcile) | +| 49f | #55 | #61, infra #151 (ledger), #152 (deploy) | MCP `capture` relay tool + I2 ledger + I3 deploy | + +Predecessor: #45 (`brain_update`/`brain_get` verbs, PR #46) — the read-after-write contract capture reuses. + +--- + +## 4. Invariant compliance + +| Inv | How satisfied | +|-----|---------------| +| **I1** sovereign containment | Effective classification = stricter(caller-declared, target-derived #50). Origin is **server-derived from the authenticated principal**, never `context.harness`. Confidential + us-nexus origin → refused before any write; refusal audited. Asserted-vs-derived mismatch → security event. | +| **I2** deliberate acceptance | The relay's cross-harness reach is recorded in `infra/docs/security-baseline.md` with six containment properties + Revisit-if, **merged before relay code shipped** (infra #151). | +| **I3** GitOps reconcilability | Env + `gitea-api-token` ExternalSecret under `infra/k3s/apps/supervisor/`, Flux-reconciled; image bumped by CD. No untracked runtime. | +| **I5** auditability | Every capture emits a request-level audit record. Classification-aware degradation: confidential + sink-down → hard-refuse; internal/public + sink-down → durable local buffer + ntfy + reconcile-on-recovery; floor → refuse if nothing can record. | + +--- + +## 5. Operational reference (env) + +Set on the `ingestion` deployment (`infra/k3s/apps/supervisor/ingestion-deployment.yaml`): + +| Env | Purpose | Notes | +|-----|---------|-------| +| `BRAIN_GITEA_URL` / `BRAIN_GITEA_TOKEN` | enables the `IssueTracker` → gates `/capture` + the MCP tool | token from 1P `DMABE_GITEA_API_TOKEN` via ESO; unset ⇒ capture disabled | +| `BRAIN_LOKI_URL` | activates the `DegradingSink` | unset ⇒ `SlogSink` (audit to stdout → alloy → loki; no refuse/buffer semantics) | +| `BRAIN_NTFY_URL` / `BRAIN_NTFY_TOKEN` | degraded-state alerts | optional | +| `BRAIN_CAPTURE_SOVEREIGN_PRINCIPALS` | JWT subjects treated as sovereign-soil | comma-separated; static-token caller is always sovereign; unknown JWT ⇒ us-nexus (fail safe) | +| `BRAIN_AUDIT_RECONCILE_INTERVAL` | buffer→loki replay tick | default 60s | + +The audit buffer lives at `/.audit-buffer/capture.jsonl` on the brain hostPath (nodeSelector-pinned to koala) — durable across restart without a separate PV. + +--- + +## 6. Tests + +67 test functions across the six packages. Coverage maps to the spec's Gherkin: happy path, supersede-not-duplicate, fail-closed validation, partial-failure receipt, dry-run, stricter-classification-wins, I1 confidential-via-us-nexus-refused / via-sovereign-allowed / asserted-label-ignored / caller-cannot-forge-origin, I5 confidential-refuse / internal-buffer / floor-refuse / reconcile / buffer-survives-restart, and the MCP relay tool (forwards, preserves principal, unauth rejected). `task check` green. + +--- + +## 7. Deferred (not in this epic) + +Tracked here so they aren't lost; file as issues when picked up: + +- **SKILL veneer** — the `close-session` SKILL becomes the claude.ai trigger/harvest layer that calls capture. +- **Per-harness token provisioning** for Crush / Pi / LLM Council (claude.ai is done via the existing `/mcp` connector). +- **Harvest adapters** — transcript-parse vs chat-memory-reconstruct vs agent-runlog, each assembling capture args at its own fidelity. +- **`SummaryWriter` impl** — ai-sessions summary persistence (the port + path logic exist; the concrete writer is nil today, so a request with a summary fails that one item). + +--- + +## 8. Brain learnings + +- `wiki/hyperguild/decisions/capture-classification-taxonomy` +- `wiki/hyperguild/decisions/gate-on-server-derived-signals-fail-safe` +- `wiki/hyperguild/decisions/two-phase-reserve-record-audit-gate` +- `wiki/hyperguild/failures/mcp-bearer-middleware-discards-principal` +- `wiki/hyperguild/facts/brain-mcp-embeddings-out-of-band-sync` (from #45, the predecessor)