Q1 classification trust: model (C) — caller declares, server cross-checks target tag, stricter wins, mismatch logged. Needs a classification taxonomy + per-wing/repo tags (prerequisite, sub-task of #49). Q2 harness origin: server-derived from authenticated principal; context.harness is descriptive-only, never a gate input. Q3 central relay: ships in v1 (needed for claude.ai/Crush/Pi/LLM Council) + I2 security-baseline ledger entry is v1 work. Q4 audit-sink-down: degrade-and-warn + durable local buffer + reconcile on recovery; refuse only if NOTHING can record the audit. Updated the I1 sovereignty + I5 auditability Gherkin scenarios to match; added classification-mismatch and origin-spoofing scenarios.
224 lines
13 KiB
Markdown
224 lines
13 KiB
Markdown
# Capture capability — use-case & BDD specification
|
||
|
||
**Status:** Decisions resolved 2026-06-22 (§4). Ready for implementation scoping. `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 resolves **effective classification** (stricter of caller-declared and target-derived)
|
||
and the **server-derived harness origin** (from the authenticated principal). It checks the
|
||
**sovereignty gate** (I1): if effective classification is confidential AND the origin is a
|
||
non-sovereign (us-nexus) surface, the capture is **refused** before any write.
|
||
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. Harness origin is **server-derived from the authenticated principal** (not caller-asserted). Classification uses **model (C)**: caller declares, server cross-checks the target's tag, **stricter wins**, mismatch logged. See §4.1–4.2. |
|
||
| **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) ---
|
||
# Harness origin is server-derived from the authenticated principal, never from context.harness.
|
||
Scenario: Refuse capture of a confidential session through a us-nexus harness
|
||
Given a session whose effective classification is "confidential"
|
||
And the authenticated principal resolves to a us-nexus harness origin
|
||
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 whose effective classification is "confidential"
|
||
And the authenticated principal resolves to a sovereign-soil harness origin
|
||
When capture is invoked
|
||
Then the capture proceeds and persists normally
|
||
|
||
Scenario: Ignore a caller-asserted harness label and use the server-derived origin
|
||
Given the request context asserts harness "sovereign-soil"
|
||
But the authenticated principal resolves to a us-nexus origin
|
||
And the session classification is "confidential"
|
||
When capture is invoked
|
||
Then the capture is refused
|
||
And the server-derived origin is used, not the asserted label
|
||
And the asserted-vs-derived discrepancy is logged as a security event
|
||
|
||
# --- I1: classification model (C) — stricter of declared vs target-derived wins ---
|
||
Scenario: Take the stricter classification when caller and target disagree
|
||
Given the caller declares classification "internal"
|
||
But the target wing/repo is tagged "confidential"
|
||
When capture is invoked
|
||
Then the effective classification is "confidential"
|
||
And the declared-vs-derived mismatch is logged as a security event
|
||
And the I1 gate is evaluated against "confidential"
|
||
|
||
Scenario: Honour a caller raising sensitivity above the target's tag
|
||
Given the caller declares classification "confidential"
|
||
And the target wing/repo is tagged "internal"
|
||
When capture is invoked
|
||
Then the effective classification is "confidential"
|
||
And the capture is gated as confidential
|
||
|
||
# --- 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, but degrades rather than blocks ---
|
||
Scenario: Capture proceeds under a durable local audit buffer when the central sink is down
|
||
Given the central audit substrate (loki) cannot be written to
|
||
When capture is invoked
|
||
Then the capture proceeds
|
||
And the audit record is written to a durable LOCAL fallback buffer
|
||
And an ntfy alert is emitted naming the degraded audit state
|
||
And the receipt flags that audit was buffered locally, not centrally recorded
|
||
# Never UN-audited: the write is buffered durably and reconciled on recovery.
|
||
|
||
Scenario: Locally buffered audit records reconcile to the central sink on recovery
|
||
Given audit records were buffered locally during a sink outage
|
||
When the central audit substrate becomes reachable again
|
||
Then the buffered records are replayed to the central sink
|
||
And the local buffer is cleared only after confirmed central write
|
||
|
||
Scenario: Capture refuses if even the local audit buffer cannot be written
|
||
Given neither the central sink nor the local fallback buffer can be written
|
||
When capture is invoked
|
||
Then the capture is refused
|
||
And the reason names the auditability invariant
|
||
# Degrade-and-warn has a floor: if NOTHING can record the audit, do not write.
|
||
|
||
# --- 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. Resolved decisions (2026-06-22)
|
||
|
||
These were open questions at draft; resolved in the 2026-06-22 review session. Recorded here as
|
||
binding design decisions for the build.
|
||
|
||
1. **Classification trust — model (C): caller-declares + server-cross-checks, stricter wins.**
|
||
The caller declares `context.classification`; the server **independently derives** the target's
|
||
classification (from the target wing/repo's classification tag) and gates on the **stricter of
|
||
the two**. The caller can voluntarily *raise* sensitivity but can never *lower* it below the
|
||
target's floor. A declared-vs-derived **mismatch is logged as a security event** (I5).
|
||
- **Prerequisite (new build work):** a classification taxonomy (e.g. `public` /
|
||
`internal` / `confidential`) and a per-wing / per-repo classification tag the server can read.
|
||
This must exist before the I1 gate is load-bearing. Tracked as a sub-task of #49.
|
||
- Rationale: composes with decision 2; fails safe; honours a caller flagging something *more*
|
||
sensitive than its destination. Pure caller-trust (A) was rejected — it makes the gate theatre.
|
||
|
||
2. **Sovereign-harness determination — server-derived, not caller-asserted.**
|
||
"Is this harness us-nexus / sovereign?" is derived from the **authenticated principal/origin**
|
||
(the OAuth2 identity), never from `context.harness`. `context.harness` survives only as a
|
||
self-reported label for the audit log — descriptive telemetry, **never a gate input**. A control
|
||
keyed on an attacker-suppliable value is not a control.
|
||
|
||
3. **Central relay — ships in v1, with the I2 ledger entry.**
|
||
The relay is required, not optional: claude.ai (Chat/Cowork/Design), Crush, Pi, and LLM Council
|
||
cannot run the use-case library in-process, and those are primary day-to-day surfaces. Deferring
|
||
the relay would ship a capability that doesn't work from the interfaces actually in use. Because
|
||
the relay is a (thin, no-standing-visibility, audit-only-retention) central node, its cross-harness
|
||
reach **must be entered in `infra/docs/security-baseline.md`** with Why-accepted / Revisit-if
|
||
**before it ships** (I2). That ledger entry is v1 work, not a follow-up.
|
||
|
||
4. **Audit-sink-down — degrade-and-warn, with a durable local buffer + reconcile-on-recovery.**
|
||
If the central audit sink (loki) is unreachable, capture **proceeds** rather than blocking
|
||
(availability), BUT the audit record is written to a **durable local fallback buffer** and an
|
||
**ntfy alert** fires. Buffered records **reconcile to the central sink on recovery**; the local
|
||
buffer clears only after confirmed central write. **Floor:** if *neither* the central sink nor the
|
||
local buffer can be written, capture **refuses** — degrade-and-warn never means *un*-audited.
|
||
- Rationale: keeps capture available during an observability outage while preserving I5 — the
|
||
write is always recorded *somewhere durable*, just not centrally until reconciliation. This is
|
||
the difference between an I5 violation and an I5-compliant degraded mode, and it must be
|
||
presentable as such to a due-diligence client.
|