Files
hyperguild/specs/capture-bdd-spec.md
T
mathias db638cca11
CI / Lint / Test / Vet (push) Successful in 14s
CI / Mirror to GitHub (push) Successful in 3s
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).
2026-06-22 17:25:03 +00:00

8.3 KiB
Raw Blame History

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 (I1I5), 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)

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.