AUDIT-WP-0009-T11 — register informed-decision, and answer GH-DEC-2026-014
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

informed-decision is the browser-facing approver surface; GH-DEC-2026-012
limit 3 makes its evidence copy the one that must reach audit-core
independently of the emitter, because there the actor being audited and the
evidence source are the same component.

Registration accepted on every proposed field — exact source,
["tenant:platform"], write true, read false, load-bearing, secret_policy
redact. Prepared and inert: the scope overlay applies only to a sender the
Secret already carries, asserted by test rather than by reading. Ingress ANDs
namespace and pod label in one peer, following approval-engine rather than
user-engine's older breadth.

Gate House asked whether the record shape can carry a source-held-content
declaration with a retrieval expectation, and asked for a straight answer
rather than a rule the storage cannot meet. Both halves, which must travel
together:

  It CAN carry the declaration. data is stored verbatim into details.data and
  hash-chained, so content_exists and custody need no schema change and become
  as tamper-evident as the commitment they accompany.

  It CANNOT detect non-production. audit-core performs no retrieval and its
  egress permits Postgres and DNS only. Detection happens at retrieval, by the
  reviewer; the stored declaration is what turns a blank into a failure
  attributable to the named custodian.

Residual stated rather than left to be found: a custodian that never held the
content can emit a false content_exists. audit-core validates the declaration's
shape, never its truth — the same class as omission at source, and not closed
by the chain, by attestation, or by T04/T06. A test asserts no egress to the
emitter exists, because that claim silently stops being true if one appears.

Cadence: reconciliation plus heartbeat is right for a mixed-volume source, with
both scoped per class rather than per source — a per-source heartbeat is
satisfied by the high-volume presentation stream and says nothing about a quiet
month of dispositions. Bound: a compromised emitter suppresses the event and
its own count together.

Also recorded: commitment-only satisfies non-alteration and never
reconstructability, in this repo's documents as in theirs; and tenant
provenance under GH-DEC-2026-013 lands in the registration record, not the
envelope, since audit-core checks a value the credential may write rather than
resolving an identity claim.

No secret was created and no production manifest applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
This commit is contained in:
tegwick 2026-09-10 15:15:35 +02:00
parent 565e7e38d4
commit c4016a70d5
8 changed files with 486 additions and 4 deletions

View file

@ -93,6 +93,36 @@ spec:
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: audit-core-informed-decision-ingress
namespace: audit-core
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: audit-core
policyTypes: [Ingress]
ingress:
# AUDIT-WP-0009-T11 / AUDIT-IN-0003. Load-bearing presentation evidence
# under GH-DEC-2026-012 limit 3 and GH-DEC-2026-014. Both selectors belong
# to one peer and are therefore ANDed, following the approval-engine rule
# rather than user-engine's older namespace-only breadth.
#
# Note what this rule does NOT create: no egress from audit-core to
# informed-decision. The commitment-only record's custody declaration is
# carried, never dereferenced from here — audit-core makes no retrieval
# call, and the egress policy below is the proof.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: informed-decision
podSelector:
matchLabels:
app.kubernetes.io/name: informed-decision
ports:
- {protocol: TCP, port: 8080}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: audit-core-operator-ingress
namespace: audit-core

View file

@ -21,5 +21,17 @@
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"
},
{
"name": "informed-decision",
"sources": [
"informed-decision"
],
"tenants": [
"tenant:platform"
],
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"
}
]

View file

@ -23,15 +23,35 @@ data:
[
{
"name": "user-engine",
"sources": ["user-engine"],
"tenants": ["*"],
"sources": [
"user-engine"
],
"tenants": [
"*"
],
"may_write": true,
"may_read": false
},
{
"name": "approval-engine",
"sources": ["approval-engine"],
"tenants": ["tenant:platform"],
"sources": [
"approval-engine"
],
"tenants": [
"tenant:platform"
],
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"
},
{
"name": "informed-decision",
"sources": [
"informed-decision"
],
"tenants": [
"tenant:platform"
],
"may_write": true,
"may_read": false,
"evidence_kind": "load-bearing"

View file

@ -0,0 +1,198 @@
# informed-decision source registration
`AUDIT-WP-0009-T11` · intake `AUDIT-IN-0003` · statute §9.6 · rulings
`GH-DEC-2026-012` (limit 3), `GH-DEC-2026-014`, `GH-DEC-2026-013`
The registration inputs for `informed-decision` as a distinct audit source: the
browser-facing approver surface that `approval-engine` deliberately does not
contain. Onboarded under `INTENT.md` principle 4 — declared ownership,
retention, access, export and evidence policy, not merely events arriving.
This file is the owner-side record. Everything in it is non-secret. No sender
token appears here, in Git, in State Hub, or in a workplan.
## Declaration
| Field | Value |
| --- | --- |
| Sender name | `informed-decision` |
| Permitted `source` | `informed-decision` (exact; no wildcard) |
| `evidence_kind` | **`load-bearing`** (§9.6 — GH-DEC-2026-012 limit 3 makes the presentation record the independent evidence of a binding act) |
| `completeness_trade` | none, and none is permitted — §9.6 requires emission atomicity of a load-bearing source |
| `may_write` | true |
| `may_read` | false — a source does not gain a read surface by emitting. See the reconciliation note below, which is where the temptation to grant one will come from |
| `secret_policy` | `redact` — accepted as proposed |
| `tenants` | `["tenant:platform"]` — accepted as proposed, with the provenance obligation below |
| Retention | no expiry set; recoverable history is the platform `data.backup` window, 30 days, `measured` |
| Custody class | `operational` — never `archive`, never WORM |
Accepted as proposed on every field. The proposal was correct; the substance of
this record is in what follows it.
## Event classes — one source, not two
Asked directly: is commitment-only content one event class or two, and does it
belong in the same class as other evidence at all.
**One source, distinct `type` values per class.** This is the same shape
`approval-engine` carries for its four §9.4 classes: audit-core stores classes
as actions on one source and derives nothing from them.
| Class | Volume | Why it matters |
| --- | --- | --- |
| presentation | high — one per render | Carries the commitment. The only class carrying a custody declaration |
| disposition | low | **security-relevant** — the binding verb |
| stance application | low | **security-relevant** — what was applied to the act |
A separate sender for the commitment-bearing class was considered and refused.
Two senders would mean two credentials, two tenant scopes and two ingress rules
for one component whose defining property is that the actor and the evidence
source are the same — splitting them makes that single residual look like two
smaller ones. The distinction that matters is per class, and `type` carries it.
Commitment-only content does belong in the same store as other evidence,
because the custody guarantee it receives is the same one and no stronger. What
differs is what may be claimed of it, and that is stated below rather than
encoded in a separate class.
## GH-DEC-2026-014 limit 3: can this fabric carry it?
Gate House asked whether a source-held-content declaration with a retrieval
expectation is a thing the record shape can carry today, and asked for a
straight answer rather than a rule nobody can meet.
**It can carry the declaration. It cannot itself detect non-production.** Both
halves are load-bearing; neither should be read without the other.
**What is carried.** `data` is an arbitrary structured object stored verbatim,
normalized into the envelope's `details.data`, appended under the trigger and
linked into the hash chain like every other field. `content_exists` and
`custody` therefore need no schema change, no new column and no new surface.
Once stored, the chain detects any later alteration or truncation of them
exactly as it does for the commitment digest itself. So the declaration is as
tamper-evident as the thing it accompanies, which is the property the condition
needs — a custodian cannot quietly retract the assertion that content existed.
**What is not delivered, and must not be inferred.** audit-core performs no
retrieval. It holds no client for `informed-decision`, and its egress policy
permits Postgres and DNS and nothing else (`deploy/networkpolicies.yaml`) — the
new ingress rule adds no egress. So audit-core never learns whether a custodian
would in fact produce the content, and raises no finding when it would not.
Non-production becomes detectable **at retrieval, by the reviewer**, and the
stored declaration is what makes it a finding rather than a blank: the reviewer
holds a chained, timestamped statement that content existed and where custody
sat, so failure to produce is attributable to the named custodian. That is the
condition met in the form audit-core can honestly meet it.
**The residual, stated rather than left to be found.** A custodian that never
held the content can emit a false `content_exists`. audit-core validates the
declaration's shape, never its truth. This is the same class of defect as
omission at source — the archive cannot retrofit a property the boundary did
not have — and it is not closed by the chain, by attestation, or by T04/T06. It
narrows to this: the declaration converts an unattributable absence into an
attributable false statement, which is strictly better and is not the same as
proof.
**One design consequence for the emitter.** `secret_policy: redact` scans
`data`. If the `custody` locator is ever secret-shaped — a token in a URL, a
path that trips the scanner — it will be redacted out and the declaration will
reach the store without its pointer. The redaction is recorded in
`details.redaction.paths`, so this fails visibly rather than silently, but the
locator should be a stable non-secret identifier resolved by the custodian, not
a credentialed URL. Under commitment-only the exposure is small by
construction, as informed-decision observed; the custody field is the one place
that stops being true.
## Cadence: the shape for a mixed-volume source
Asked whether reconciliation per class as primary, plus heartbeat for the
low-volume classes, is right. **Yes, with one refinement and one bound.**
**Refinement — both are per class, not per source.** A per-source heartbeat
from a component emitting one presentation per render is nearly worthless: it
is satisfied by the high-volume class and says nothing about a quiet month of
dispositions. Heartbeat must be scoped to the low-volume classes individually,
carrying a *nothing to report* assertion for that class. Likewise reconciliation
counts must be keyed `(source, class, window)`, which is the shape
`AUDIT-WP-0009-T06` is being built to.
The high-volume class needs reconciliation too, and rate alone is not a
substitute for it. Rate monitoring detects a stream stopping; it does not detect
a stream missing the particular renders that mattered.
**The bound, which matters more here than for approval-engine.** Reconciliation
compares audit-core's counts against counts the source computes from its own
state. Where the threat is a compromised emitter — and GH-DEC-2026-012 limit 3
exists precisely because here the actor and the evidence source are one
component — a compromised `informed-decision` suppresses the event and its own
count together, and reconciliation agrees. Heartbeat inherits the same weakness:
a compromised source emits a truthful-looking *nothing to report*.
So reconciliation and heartbeat cover loss, accident, outage and drain failure,
which is most of what actually goes wrong. They do not cover the residual
informed-decision already declares and should not be described as covering it.
Nothing audit-core can build closes that; it needs an observer independent of
the emitter, which is out of scope here (§16).
**Sequencing.** T04 and T06 are open. informed-decision may declare this cadence
now and must not describe it as operating until they land — which is exactly
what the intake proposed, and is the right handling.
## Tenant provenance
`tenant:platform` is accepted. informed-decision notes that under
`GH-DEC-2026-013` the value arrives by the declared-gap route
(registration-bound rather than directory-sourced) and asks whether the
registration or envelope inherits the obligation to record which route it came
by.
**Not in this envelope, and here is why.** audit-core's `tenant` is not an
asserted identity claim it resolves; it is a value the credential is *permitted
to write*, checked by exact string equality against this registration. The
provenance question — how a principal came to hold `tenant:platform` — is a
property of the token issuer's claim, and recording a route in the audit event
would be audit-core restating something it did not observe. That is the same
error as claiming an event occurred.
Where it does land is here, in the registration record, which is the document
that says why this sender may write this tenant. Recorded: the scope is exact,
single-valued, and its authority is `GH-DEC-2026-013`'s bounded gap rather than
a populated directory record. If the gap closes and the terminal
directory-sourced shape lands, this entry should be revisited rather than
assumed still correct.
## Applied in this repository
| Input | Where |
| --- | --- |
| Non-secret scope | `deploy/senders-scope.json` + `deploy/senders-scope.yaml` (lockstep) |
| Receiver ingress | `deploy/networkpolicies.yaml`, `audit-core-informed-decision-ingress` |
| Evidence kind in the model | `audit_core/senders.py`, `SenderIdentity.evidence_kind` |
The scope overlay only applies to a sender the Secret already carries, so this
entry is **inert until the token exists** — asserted by test, not by reading.
The ingress rule ANDs namespace and pod label in one `from` peer.
`user-engine`'s and `approval-engine`'s policies are unchanged.
## Still owed by others
| Input | Owner | Note |
| --- | --- | --- |
| Sender token, custody path via `warden route` | OpenBao (`railiance-platform`) | audit-core never holds or transports it |
| Protected registry entry with explicit `secret_policy: redact` | `railiance-platform` | The overlay deliberately does not override `secret_policy`; provisioning must set it |
| Applying the manifests to `railiance01` | operator | audit-core does not apply cluster changes unprompted |
| The `content_exists` / `custody` fields on the emitted payload | `informed-decision` | audit-core carries them; it does not require, validate or dereference them |
## What Audit Core will and will not claim about this stream
**Will:** every event it accepted is stored append-only, and the hash chain
detects alteration or truncation of what it received — including of the
`content_exists` and `custody` declarations.
**Will not:** that the archive proves a presentation occurred, or that the
absence of one proves it did not. And specifically, per GH-DEC-2026-014:
audit-core will not describe a commitment-only record as satisfying
**reconstructability**. It satisfies non-alteration of the commitment. About
what was committed to it establishes nothing, except conditionally — if a
document is later produced, whether it is the one. No conformance claim, ITC-CAP
case, or `docs/integrity.md` statement may say otherwise.

View file

@ -1,5 +1,74 @@
# Intake records
## AUDIT-IN-0003 — Register informed-decision as a load-bearing sender; commitment-only payload doctrine
```yaml
id: AUDIT-IN-0003
kind: intake
title: 'Register informed-decision as a load-bearing sender, and answer GH-DEC-2026-014
limit 3'
status: closed
origin: cross-repo
origin_ref: INFD-WP-0001-T08 / GH-DEC-2026-012 / GH-DEC-2026-014
priority: high
owner: audit-core
requested_by: informed-decision
description: >
informed-decision is the browser-facing approver surface approval-engine
deliberately does not contain, ruled PEP-shaped by GH-DEC-2026-012. Limit 3
of that ruling requires its evidence copy to reach audit-core independently
of the emitter, because the actor being audited and the evidence source are
the same component.
Requested: (1) sender registration — source informed-decision exact, tenants
[tenant:platform], write true, read false, evidence_kind load-bearing,
secret_policy redact; (2) whether reconciliation per class as primary plus
heartbeat for the low-volume classes is the right cadence form for a
mixed-volume source; (3) whether commitment-only content is one event class
or two, and whether it belongs in the same class as other evidence.
GH-DEC-2026-014 attached a condition addressed to audit-core: non-production
of committed content must be detectable as a FINDING rather than present as
an absence, so the record must carry an assertion that committed content
exists and where custody sits. Gate House asked audit-core to say plainly
whether the record shape can express that, rather than have a rule written
that the storage cannot meet.
created: '2026-09-09'
updated: '2026-09-10'
outcome: registered-with-a-stated-bound
closed: '2026-09-10'
resolution: 'Registration accepted on every proposed field; the proposal was
correct and the substance is in the bounds. Prepared and inert pending token,
protected-registry entry and operator apply —
docs/informed-decision-source-registration.md, deploy/senders-scope.{json,yaml},
audit-core-informed-decision-ingress. (1) GH-DEC-2026-014 limit 3: the fabric
CAN carry the declaration — data is stored verbatim into details.data and
hash-chained, so content_exists and custody need no schema change and become
as tamper-evident as the commitment they accompany. It CANNOT detect
non-production: audit-core performs no retrieval and its egress permits
Postgres and DNS only. Detection happens at retrieval by the reviewer, and the
stored declaration is what makes non-production attributable rather than a
blank. Residual: a custodian that never held the content can emit a false
content_exists; audit-core validates the declaration shape, never its truth —
same class as omission at source, not closed by chain, attestation or T04/T06.
(2) Cadence: reconciliation plus heartbeat is right, with both scoped PER
CLASS rather than per source — a per-source heartbeat is satisfied by the
high-volume presentation stream and says nothing about a quiet month of
dispositions. Bound: a compromised emitter suppresses the event and its own
count together, so neither control covers the limit-3 residual. Depends on
T04/T06; declare now, do not describe as operating. (3) One source, distinct
type values per class, following approval-engine four-class shape; a second
sender would split one residual into two smaller-looking ones. (4) Tenant
provenance is recorded in the registration document, not the envelope:
audit-core does not resolve the tenant claim, it checks a value the credential
is permitted to write, and restating an unobserved route in an event is the
same error as claiming an event occurred. (5) Stated for all documents:
commitment-only satisfies non-alteration and never reconstructability, and no
audit-core document or conformance claim may describe it otherwise.'
recorded_in: docs/informed-decision-source-registration.md
work: AUDIT-WP-0009-T11
```
## AUDIT-IN-0002 — Register tenant-engine as an attributive audit-core sender
```yaml

View file

@ -59,3 +59,41 @@ def test_user_engine_sender_ingress_is_unchanged_by_the_new_sender():
)
assert "kubernetes.io/metadata.name: user-engine" in policy
assert "approval-engine" not in policy
def test_informed_decision_ingress_is_bound_to_namespace_and_pod_labels():
"""AUDIT-WP-0009-T11. A second load-bearing source gets the narrow rule too."""
documents = (ROOT / "deploy" / "networkpolicies.yaml").read_text().split("\n---\n")
policy = next(
document
for document in documents
if "name: audit-core-informed-decision-ingress" in document
)
expected_peer = """ - namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: informed-decision
podSelector:
matchLabels:
app.kubernetes.io/name: informed-decision"""
assert expected_peer in policy
assert policy.count(" - namespaceSelector:") == 1
assert " - {protocol: TCP, port: 8080}" in policy
def test_no_egress_to_informed_decision_is_created():
"""The custody declaration is carried, never dereferenced.
GH-DEC-2026-014 limit 3 is met by storing an attributable declaration, not
by audit-core retrieving content. If an egress rule to the emitter ever
appears, the claim in docs/informed-decision-source-registration.md that
audit-core performs no retrieval has silently stopped being true.
"""
documents = (ROOT / "deploy" / "networkpolicies.yaml").read_text().split("\n---\n")
egress = next(
document for document in documents if "name: audit-core-egress" in document
)
assert "informed-decision" not in egress
assert "approval-engine" not in egress
# Postgres and DNS only.
assert egress.count(" - namespaceSelector:") == 2

View file

@ -339,3 +339,62 @@ def test_user_engine_evidence_kind_is_not_asserted_on_its_behalf():
scope = json.loads(SCOPE_FILE.read_text())
entry = next(e for e in scope if e["name"] == "user-engine")
assert "evidence_kind" not in entry
# --- AUDIT-WP-0009-T11: informed-decision registration inputs --------------
def test_informed_decision_is_declared_load_bearing():
scope = json.loads(SCOPE_FILE.read_text())
entry = next(e for e in scope if e["name"] == "informed-decision")
assert entry["evidence_kind"] == "load-bearing"
assert entry["sources"] == ["informed-decision"]
assert entry["tenants"] == ["tenant:platform"]
assert entry["may_read"] is False
# §9.6 permits no completeness trade for a load-bearing source.
assert "completeness_trade" not in entry
assert "tokens" not in entry and "token" not in entry
def test_informed_decision_scope_entry_admits_nothing_without_a_token():
"""Safe to land ahead of the credential, asserted rather than read."""
registry = SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": json.dumps(
[{"name": "user-engine", "tokens": ["live"], "sources": ["user-engine"]}]
),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(SCOPE_FILE),
})
assert "informed-decision" not in [i.name for i in registry.identities]
@pytest.mark.parametrize(
"tenant", ["platform", "tenant:coulomb", "tenant:Platform", "tenant:platform "]
)
def test_informed_decision_scope_restricts_a_stale_wildcard_registration(tenant):
registry = SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": json.dumps([{
"name": "informed-decision", "tokens": ["fixture-only"],
"sources": ["*"], "tenants": ["*"],
"may_read": True, "secret_policy": "redact",
}]),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(SCOPE_FILE),
})
identity = registry.authenticate("Bearer fixture-only")
assert identity.permits_tenant("tenant:platform")
assert not identity.permits_tenant(tenant)
assert identity.permits_source("informed-decision")
assert not identity.permits_source("approval-engine")
assert identity.may_write and not identity.may_read
assert identity.is_load_bearing
def test_a_load_bearing_source_cannot_be_given_a_completeness_trade():
"""informed-decision declares no trade and may not acquire one by overlay."""
with pytest.raises(ValueError, match="completeness_trade"):
SenderIdentity(
name="informed-decision",
tokens=("t",),
sources=frozenset({"informed-decision"}),
evidence_kind="load-bearing",
completeness_trade="emits after commit",
)

View file

@ -287,6 +287,62 @@ intervals for a store another layer reads, which `platform-pg` is. List it and
the State Hub now. Add a conformance script and test in the shape ops-warden
offered estate-wide.
```task
id: AUDIT-WP-0009-T11
status: progress
priority: high
```
Register `informed-decision` as a distinct load-bearing source under
`AUDIT-IN-0003`, and answer the doctrine question `GH-DEC-2026-014` put to
audit-core: whether the record shape can carry a source-held-content
declaration with a retrieval expectation.
`informed-decision` is the browser-facing approver surface. `GH-DEC-2026-012`
limit 3 makes its evidence copy the one that must reach audit-core
independently of the emitter, because there the actor being audited and the
evidence source are the same component. `GH-DEC-2026-014` then granted
commitment-only payloads for Stage 1 under a condition Gate House added: the
record must assert that committed content **exists** and where **custody**
sits, so non-production is a finding attributable to the custodian rather than
an unremarkable blank.
**Done 2026-09-10, prepared and inert:**
`docs/informed-decision-source-registration.md`, scope entries in
`deploy/senders-scope.{json,yaml}`, and
`audit-core-informed-decision-ingress` in `deploy/networkpolicies.yaml`
(namespace AND pod label in one peer, following approval-engine rather than
`user-engine`'s older breadth). Every proposed field accepted as-is: exact
source, `["tenant:platform"]`, write true, read false, `load-bearing`,
`secret_policy: redact`.
**The answer to Gate House's condition, in two halves that must travel
together.** The fabric *can* carry the declaration — `data` is stored verbatim
into `details.data` and chained, so `content_exists` and `custody` need no
schema change and become as tamper-evident as the commitment they accompany. It
*cannot* detect non-production: audit-core performs no retrieval and its egress
policy permits Postgres and DNS only, so it never learns whether a custodian
would produce. Detection happens at retrieval, by the reviewer, and the stored
declaration is what turns a blank into an attributable failure. Residual stated
rather than left to be found: a custodian that never held the content can emit a
false `content_exists`; audit-core validates the declaration's shape, never its
truth. Not a requirement nobody can meet, and not the stronger thing it might be
mistaken for.
**Cadence answered:** reconciliation-per-class plus heartbeat for the low-volume
classes is the right shape for a mixed-volume source, with both scoped **per
class** rather than per source — a per-source heartbeat is satisfied by the
high-volume presentation stream and says nothing about a quiet month of
dispositions. Bound stated: where the emitter itself is compromised, it
suppresses the event and its own count together, so neither control covers the
residual `GH-DEC-2026-012` limit 3 exists for. Depends on T04/T06 for the
detection half; declaring the cadence now is correct, describing it as operating
is not.
Remaining: token custody, the protected registry entry, operator manifest
application, and live ingestion evidence. No secret was created and no
production manifest applied.
## Acceptance
- No custody claim is returned unconditionally where `docs/integrity.md`