Reconsidered Q4: instead of one global degrade-and-warn, the posture now inherits from effective classification (Q1): - confidential + audit-sink-down -> hard-refuse (no buffer; removes the buffer-integrity question for confidential data) - internal/public + audit-sink-down -> degrade-and-warn + durable local buffer + ntfy + reconcile-on-recovery - floor (all tiers): refuse if nothing can record the audit Updated the I5 Gherkin scenarios + obligations row to match. Couples Q4 to the Q1 classification spine -> one coherent sensitivity model.
243 lines
14 KiB
Markdown
243 lines
14 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 (actor/principal, harness, items written, timestamp) to the alloy/loki substrate. **Classification-aware degradation** (§4.4): confidential + sink-down → hard-refuse; internal/public + sink-down → durable local buffer + ntfy + reconcile. Floor: refuse if nothing can record the audit. |
|
||
|
||
---
|
||
|
||
## 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 classification-aware (confidential fails closed) ---
|
||
Scenario: Confidential capture hard-refuses when the central audit sink is down
|
||
Given the effective classification is "confidential"
|
||
And the central audit substrate (loki) cannot be written to
|
||
When capture is invoked
|
||
Then the capture is refused before any write
|
||
And the reason names the auditability invariant
|
||
# Confidential work must be centrally auditable at write time — no buffered exception.
|
||
|
||
Scenario: Internal capture degrades to a durable local buffer when the sink is down
|
||
Given the effective classification is "internal" or "public"
|
||
And 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
|
||
|
||
Scenario: Locally buffered audit records reconcile to the central sink on recovery
|
||
Given internal-tier 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: Even internal capture refuses if neither sink nor local buffer can be written
|
||
Given the effective classification is "internal" or "public"
|
||
And 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 — classification-aware: confidential fails closed, internal/public degrades.**
|
||
The posture inherits from the effective classification (decision 1), so there is one coherent
|
||
sensitivity model rather than a separate availability policy:
|
||
- **Confidential + central audit sink unreachable → hard-refuse.** No buffer, no proceed.
|
||
Confidential work must be centrally auditable *at write time*; "buffer and reconcile later"
|
||
introduces a buffer-integrity question (can a write tamper with its own pending audit record?)
|
||
that must not exist for confidential data. The simplicity of "refuse" is itself the assurance
|
||
asset — trivially true, nothing to poke holes in.
|
||
- **Internal / public + central sink unreachable → degrade-and-warn** with a durable local buffer
|
||
+ ntfy alert + reconcile-on-recovery (the earlier Q4 design, now scoped to lower tiers). Keeps
|
||
capture available for your own homelab work during an observability outage; negligible risk
|
||
since the buffered record is still durable and the data isn't client-confidential.
|
||
- **Floor (all tiers):** if *nothing* — neither central sink nor (for internal/public) the local
|
||
buffer — can record the audit, capture **refuses**. No tier writes wholly un-audited.
|
||
- Rationale: matches assurance cost to data sensitivity, exactly as the I1/sovereignty model
|
||
does for placement. Presentable to a due-diligence client as "audit posture is
|
||
classification-aware: confidential fails closed, internal degrades gracefully" — which
|
||
demonstrates the judgment, not just a binary. Couples Q4 to Q1's classification machinery
|
||
(being built anyway) and removes the buffer-integrity rabbit hole for the only tier where it
|
||
mattered.
|