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).
156 lines
8.3 KiB
Markdown
156 lines
8.3 KiB
Markdown
# 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.
|