docs(capture): resolve §4 open questions into binding decisions (#49)
CI / Lint / Test / Vet (push) Successful in 14s
CI / Mirror to GitHub (push) Successful in 4s

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.
This commit is contained in:
mathias
2026-06-22 20:17:07 +00:00
parent db638cca11
commit b7a2cc5fdf
+94 -26
View File
@@ -1,6 +1,6 @@
# Capture capability — use-case & BDD specification # Capture capability — use-case & BDD specification
**Status:** Draft for review (pre-build). Spec precedes implementation because `capture` is a **Status:** Decisions resolved 2026-06-22 (§4). Ready for implementation scoping. `capture` is a
privileged cross-harness write path touching brain + Gitea + ai-sessions. privileged cross-harness write path touching brain + Gitea + ai-sessions.
**Tracks:** hyperguild #49. **Tracks:** hyperguild #49.
**Governed by:** `infra/docs/architecture/01-invariants.md` (I1I5), the admissibility test in **Governed by:** `infra/docs/architecture/01-invariants.md` (I1I5), the admissibility test in
@@ -22,8 +22,10 @@ behaviour across harnesses.
1. Caller assembles capture input (insights, tickets, optional summary) + context (harness, 1. Caller assembles capture input (insights, tickets, optional summary) + context (harness,
session_ref, fidelity, actor, **data-classification**). session_ref, fidelity, actor, **data-classification**).
2. System validates the whole request (fail-closed). 2. System validates the whole request (fail-closed).
3. System checks the **sovereignty gate** (I1): if the session is classified confidential AND the 3. System resolves **effective classification** (stricter of caller-declared and target-derived)
caller's harness is a non-sovereign (us-nexus) surface, the capture is **refused** at this layer. 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 4. System persists insights (write or supersede), tickets (create/close/comment), summary — each
best-effort, recording per-item outcome. best-effort, recording per-item outcome.
5. System emits an **audit record** (I5) of who/what captured what, when, via which principal. 5. System emits an **audit record** (I5) of who/what captured what, when, via which principal.
@@ -41,7 +43,7 @@ beyond the I5 audit log.
| Invariant | Obligation on `capture` | | 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. | | **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.14.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. | | **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. | | **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). | | **I4 decisions captured** | The distributed-vs-central decision and the intent-named-verb pattern are recorded (ADR + brain). |
@@ -73,20 +75,46 @@ Feature: Capture session value uniformly across harnesses
And the receipt reports every item as ok And the receipt reports every item as ok
# --- I1: sovereignty gate (the load-bearing refusal) --- # --- 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 Scenario: Refuse capture of a confidential session through a us-nexus harness
Given a session classified as "confidential" Given a session whose effective classification is "confidential"
And the calling harness is a us-nexus surface And the authenticated principal resolves to a us-nexus harness origin
When capture is invoked When capture is invoked
Then the capture is refused before any write Then the capture is refused before any write
And no insight, ticket, or summary is persisted And no insight, ticket, or summary is persisted
And the refusal names the sovereignty invariant as the reason And the refusal names the sovereignty invariant as the reason
Scenario: Allow capture of a confidential session through a sovereign harness Scenario: Allow capture of a confidential session through a sovereign harness
Given a session classified as "confidential" Given a session whose effective classification is "confidential"
And the calling harness is a sovereign-soil surface And the authenticated principal resolves to a sovereign-soil harness origin
When capture is invoked When capture is invoked
Then the capture proceeds and persists normally 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) --- # --- Supersession + staleness discipline (reuses #45 / #47 resolution) ---
Scenario: Supersede a prior insight rather than duplicating it Scenario: Supersede a prior insight rather than duplicating it
Given an insight whose context names an existing note to supersede Given an insight whose context names an existing note to supersede
@@ -119,13 +147,28 @@ Feature: Capture session value uniformly across harnesses
Then the would-be receipt is returned Then the would-be receipt is returned
And nothing is written anywhere And nothing is written anywhere
# --- I5: auditability is non-optional --- # --- I5: auditability is non-optional, but degrades rather than blocks ---
Scenario: Capture is unavailable if the audit sink is unreachable Scenario: Capture proceeds under a durable local audit buffer when the central sink is down
Given the audit substrate cannot be written to 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 When capture is invoked
Then the capture is refused Then the capture is refused
And the reason names the auditability invariant And the reason names the auditability invariant
# A privileged write path that cannot be audited must not run silently. # Degrade-and-warn has a floor: if NOTHING can record the audit, do not write.
# --- Summary fidelity (collision rule from the retro work) --- # --- Summary fidelity (collision rule from the retro work) ---
Scenario: A richer-fidelity summary supersedes a thinner one for the same session Scenario: A richer-fidelity summary supersedes a thinner one for the same session
@@ -138,18 +181,43 @@ Feature: Capture session value uniformly across harnesses
--- ---
## 4. Open questions for review (deliberately unresolved here) ## 4. Resolved decisions (2026-06-22)
1. **Classification source.** How does `context.classification` get set, and is it trusted? A These were open questions at draft; resolved in the 2026-06-22 review session. Recorded here as
harness self-declaring "internal" to bypass the I1 gate is an attack surface. Options: caller binding design decisions for the build.
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** 1. **Classification trust — model (C): caller-declares + server-cross-checks, stricter wins.**
the whole I1 gate is only as strong as the classification it reads. The caller declares `context.classification`; the server **independently derives** the target's
2. **Sovereign-harness determination.** Is "is this harness us-nexus?" derived from the classification (from the target wing/repo's classification tag) and gates on the **stricter of
authenticated principal/origin (server-side, trustworthy) or from `context.harness` the two**. The caller can voluntarily *raise* sensitivity but can never *lower* it below the
(caller-asserted, spoofable)? Must be server-derived to be load-bearing. target's floor. A declared-vs-derived **mismatch is logged as a security event** (I5).
3. **Central relay: build or defer?** The distributed-library form is admissible without a ledger - **Prerequisite (new build work):** a classification taxonomy (e.g. `public` /
entry. The fallback relay needs the I2 acceptance entry. Decide whether the relay ships in v1 or `internal` / `confidential`) and a per-wing / per-repo classification tag the server can read.
whether Crush/Pi/headless are deferred until it's ledgered. This must exist before the I1 gate is load-bearing. Tracked as a sub-task of #49.
4. **Audit-sink-down behaviour.** Spec above says "refuse if unauditable." Confirm that's the - Rationale: composes with decision 2; fails safe; honours a caller flagging something *more*
desired posture vs. degrade-and-warn — it's a real availability/assurance trade. 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.