From db638cca113abf1b99ffa6557e7bfba6f41b82f0 Mon Sep 17 00:00:00 2001 From: mathias Date: Mon, 22 Jun 2026 17:25:03 +0000 Subject: [PATCH] docs(capture): add use-case + BDD spec for the capture capability (#49) Pre-build spec for the uniform cross-harness capture path. Grounds the design in the homelab invariants (I1-I5, now canonical on infra main): - I1 sovereignty gate: refuse confidential capture via us-nexus harness - I2: distributed-library form (no high-degree observer node) is admissible; central relay needs a security-baseline ledger entry - I5: capture is a privileged write path, must emit audit records Gherkin scenarios cover the happy path, the sovereignty refusal, supersession + staleness discipline (#45/#47), fail-closed validation, best-effort partial-failure receipts, dry-run, and fidelity supersession. Four open questions flagged for review (classification trust is the highest-risk one). --- specs/capture-bdd-spec.md | 155 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 specs/capture-bdd-spec.md diff --git a/specs/capture-bdd-spec.md b/specs/capture-bdd-spec.md new file mode 100644 index 0000000..48a0f4e --- /dev/null +++ b/specs/capture-bdd-spec.md @@ -0,0 +1,155 @@ +# Capture capability — use-case & BDD specification + +**Status:** Draft for review (pre-build). Spec precedes implementation because `capture` is a +privileged cross-harness write path touching brain + Gitea + ai-sessions. +**Tracks:** hyperguild #49. +**Governed by:** `infra/docs/architecture/01-invariants.md` (I1–I5), the admissibility test in +`00-synthesis-model.md`, and the distributed-consolidation shape mandated by +`brain/wiki/homelab/decisions/no-centralized-cross-harness-observer-2026-06-17.md`. + +--- + +## 1. Use-case (Clean Architecture form) + +**Name:** CaptureSession +**Actor:** A harness acting on the user's behalf (claude.ai Chat/Cowork/Code/Design, Claude Code +CLI, Crush, Pi, LLM Council, Agentsquad executor/reviewer) — or the user directly. +**Goal:** Durably persist a finished session's valuable output — insights → brain, action items → +Gitea tickets, optional summary → ai-sessions — with one uniform invocation, identical core +behaviour across harnesses. + +**Primary success scenario (essential steps):** +1. Caller assembles capture input (insights, tickets, optional summary) + context (harness, + session_ref, fidelity, actor, **data-classification**). +2. System validates the whole request (fail-closed). +3. System checks the **sovereignty gate** (I1): if the session is classified confidential AND the + caller's harness is a non-sovereign (us-nexus) surface, the capture is **refused** at this layer. +4. System persists insights (write or supersede), tickets (create/close/comment), summary — each + best-effort, recording per-item outcome. +5. System emits an **audit record** (I5) of who/what captured what, when, via which principal. +6. System returns a structured, partial-aware receipt. + +**Architectural shape:** the *logic* is a shared use-case (`CaptureService`), invoked **per-harness +against the caller's own credentials** (distributed consolidation — no high-degree observer node). +A central authenticated relay endpoint exists ONLY as a fallback for harnesses that cannot run the +use-case in-process (Crush/Pi/headless); the relay holds no standing visibility and retains nothing +beyond the I5 audit log. + +--- + +## 2. Invariant obligations (acceptance gates, not nice-to-haves) + +| Invariant | Obligation on `capture` | +|---|---| +| **I1 sovereign containment** | A confidential-classified session MUST NOT be captured through a us-nexus harness. Classification is explicit in `context`, enforced here, not assumed. | +| **I2 deliberate acceptance** | The *distributed-library* form opens no new acceptance. IF a central relay node is deployed, its cross-harness reach MUST be entered in `infra/docs/security-baseline.md` with Why-accepted / Revisit-if before it ships. | +| **I3 GitOps reconcilability** | IF `capture` runs as a deployed service, its manifest lives under `infra/k3s/apps/**` (sovereign source, Flux-reconciled). No untracked runtime. | +| **I4 decisions captured** | The distributed-vs-central decision and the intent-named-verb pattern are recorded (ADR + brain). | +| **I5 auditability** | Every capture emits a request-level audit record to the alloy/loki substrate: actor/principal, harness, items written, timestamp. | + +--- + +## 3. BDD scenarios (Gherkin) + +```gherkin +Feature: Capture session value uniformly across harnesses + As an operator working across many AI harnesses + I want one uniform command to persist insights and file tickets + So that valuable session output is never lost and is always auditable + + Background: + Given a brain store, a Gitea issue tracker, and an ai-sessions summary writer + And the caller is authenticated with a principal + And the session context declares a harness, a fidelity, and a data classification + + # --- Core happy path --- + Scenario: Capture insights and tickets from a non-confidential session + Given a session classified as "internal" + And the capture input has 2 insights and 1 ticket to create + When capture is invoked + Then both insights are written to the brain and their ids and content hashes are returned + And the ticket is created in the named repo under owner "mathias" + And an audit record is emitted naming the principal, harness, and items written + And the receipt reports every item as ok + + # --- I1: sovereignty gate (the load-bearing refusal) --- + Scenario: Refuse capture of a confidential session through a us-nexus harness + Given a session classified as "confidential" + And the calling harness is a us-nexus surface + When capture is invoked + Then the capture is refused before any write + And no insight, ticket, or summary is persisted + And the refusal names the sovereignty invariant as the reason + + Scenario: Allow capture of a confidential session through a sovereign harness + Given a session classified as "confidential" + And the calling harness is a sovereign-soil surface + When capture is invoked + Then the capture proceeds and persists normally + + # --- Supersession + staleness discipline (reuses #45 / #47 resolution) --- + Scenario: Supersede a prior insight rather than duplicating it + Given an insight whose context names an existing note to supersede + When capture is invoked + Then the existing note is updated in place, not duplicated + And the prior content hash is recorded in the superseding note + And read-after-write confirmation uses a direct fetch, never a semantic query + + # --- Validation: fail-closed --- + Scenario: Reject a malformed request before any write + Given a capture input with an invalid wing/hall or unknown repo + When capture is invoked + Then the request is rejected with a validation error + And nothing is written to the brain, Gitea, or ai-sessions + + # --- Partial failure: best-effort + honest receipt --- + Scenario: Report partial success when one item fails mid-capture + Given a capture input with 2 insights and 1 ticket + And the second insight write will fail + When capture is invoked + Then the first insight and the ticket are persisted + And the second insight is reported as failed in the receipt + And no rollback is attempted + And the audit record reflects exactly what landed + + # --- Dry run --- + Scenario: Preview a capture without writing + Given a valid capture input with dry_run true + When capture is invoked + Then the would-be receipt is returned + And nothing is written anywhere + + # --- I5: auditability is non-optional --- + Scenario: Capture is unavailable if the audit sink is unreachable + Given the audit substrate cannot be written to + When capture is invoked + Then the capture is refused + And the reason names the auditability invariant + # A privileged write path that cannot be audited must not run silently. + + # --- Summary fidelity (collision rule from the retro work) --- + Scenario: A richer-fidelity summary supersedes a thinner one for the same session + Given a summary already exists for session_ref X at fidelity "live-capture" + And a new summary arrives for session_ref X at fidelity "transcript-parse" + When capture is invoked + Then the transcript-parse summary supersedes the live-capture one + And the live-capture summary is not left as a contradicting duplicate +``` + +--- + +## 4. Open questions for review (deliberately unresolved here) + +1. **Classification source.** How does `context.classification` get set, and is it trusted? A + harness self-declaring "internal" to bypass the I1 gate is an attack surface. Options: caller + declares + server cross-checks against a project/repo allow-list; or classification derived from + the target wing/repo rather than caller-asserted. **This is the highest-risk open question** — + the whole I1 gate is only as strong as the classification it reads. +2. **Sovereign-harness determination.** Is "is this harness us-nexus?" derived from the + authenticated principal/origin (server-side, trustworthy) or from `context.harness` + (caller-asserted, spoofable)? Must be server-derived to be load-bearing. +3. **Central relay: build or defer?** The distributed-library form is admissible without a ledger + entry. The fallback relay needs the I2 acceptance entry. Decide whether the relay ships in v1 or + whether Crush/Pi/headless are deferred until it's ledgered. +4. **Audit-sink-down behaviour.** Spec above says "refuse if unauditable." Confirm that's the + desired posture vs. degrade-and-warn — it's a real availability/assurance trade.