docs(capture): resolve §4 open questions into binding decisions (#49)
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:
+94
-26
@@ -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` (I1–I5), the admissibility test in
|
**Governed by:** `infra/docs/architecture/01-invariants.md` (I1–I5), 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.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. |
|
| **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.
|
||||||
|
|||||||
Reference in New Issue
Block a user