ai-sessions#13: capture's `summary` field wrote a frontmatter shape (title/harness/fidelity/captured_at/repos_touched) that ai-sessions' own extract/audit pipeline can't parse -- silently invisible to that repo's own audit tooling, and bypassing its redaction + Stage2 completeness gates by construction. Only 5 files ever landed this way over 3 weeks; the summary capability's whole value (speed) fights ai-sessions' whole value (redacted, audited, complete), so kill it rather than build a second parse branch. capture now persists insights -> brain and action items -> Gitea tickets only. Removes Summary/SummaryResult/SummaryWriter and all wiring (service, REST body, MCP tool schema); close-session updated to stop assembling a summary payload. specs/capture-*.md kept as historical record with a superseded note -- the feature shipped and is documented, just no longer current behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPdSHbp9Utb2hPFm9wDG59
15 KiB
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.
Superseded 2026-07-27: the ai-sessions summary write path specified below was removed post-ship
(see specs/capture-implementation-report.md). capture now touches brain + Gitea only.
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):
- Caller assembles capture input (insights, tickets, optional summary) + context (harness, session_ref, fidelity, actor, data-classification).
- System validates the whole request (fail-closed).
- 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.
- System persists insights (write or supersede), tickets (create/close/comment), summary — each best-effort, recording per-item outcome.
- System emits an audit record (I5) of who/what captured what, when, via which principal.
- 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)
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.
-
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.- Implemented (#50): taxonomy
public < internal < confidential(ordered so "stricter wins" ismax) iningestion/internal/classification/. Tags are read from an optionalclassification.yamlat the brain root (wings:/repos:maps); absent entries fall to built-in defaults (client-*→ confidential;hyperguild/homelab→ internal; everything else → confidential, fail-safe).Config.Derive(Target)is the function the use-case calls. See brainwiki/hyperguild/decisions/capture-classification-taxonomy.
- Implemented (#50): taxonomy
- 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.
- Prerequisite (new build work): a classification taxonomy (e.g.
-
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.harnesssurvives 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. -
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.mdwith Why-accepted / Revisit-if before it ships (I2). That ledger entry is v1 work, not a follow-up. -
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.