From c4016a70d528f1c35fc09b6b7680a7bb1f5b9fd4 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 10 Sep 2026 15:15:35 +0200 Subject: [PATCH] =?UTF-8?q?AUDIT-WP-0009-T11=20=E2=80=94=20register=20info?= =?UTF-8?q?rmed-decision,=20and=20answer=20GH-DEC-2026-014?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- deploy/networkpolicies.yaml | 30 +++ deploy/senders-scope.json | 12 ++ deploy/senders-scope.yaml | 28 ++- docs/informed-decision-source-registration.md | 198 ++++++++++++++++++ intakes/intakes.md | 69 ++++++ tests/test_networkpolicies.py | 38 ++++ tests/test_senders.py | 59 ++++++ ...AUDIT-WP-0009-evidence-role-conformance.md | 56 +++++ 8 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 docs/informed-decision-source-registration.md diff --git a/deploy/networkpolicies.yaml b/deploy/networkpolicies.yaml index bac5835..a618180 100644 --- a/deploy/networkpolicies.yaml +++ b/deploy/networkpolicies.yaml @@ -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 diff --git a/deploy/senders-scope.json b/deploy/senders-scope.json index 3a0ea4d..c904a74 100644 --- a/deploy/senders-scope.json +++ b/deploy/senders-scope.json @@ -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" } ] diff --git a/deploy/senders-scope.yaml b/deploy/senders-scope.yaml index e1a034f..74946e0 100644 --- a/deploy/senders-scope.yaml +++ b/deploy/senders-scope.yaml @@ -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" diff --git a/docs/informed-decision-source-registration.md b/docs/informed-decision-source-registration.md new file mode 100644 index 0000000..17aa3e7 --- /dev/null +++ b/docs/informed-decision-source-registration.md @@ -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. diff --git a/intakes/intakes.md b/intakes/intakes.md index 9bfd744..ecd1fc0 100644 --- a/intakes/intakes.md +++ b/intakes/intakes.md @@ -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 diff --git a/tests/test_networkpolicies.py b/tests/test_networkpolicies.py index cb24bf0..4cb6807 100644 --- a/tests/test_networkpolicies.py +++ b/tests/test_networkpolicies.py @@ -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 diff --git a/tests/test_senders.py b/tests/test_senders.py index 17188f8..8bd4c77 100644 --- a/tests/test_senders.py +++ b/tests/test_senders.py @@ -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", + ) diff --git a/workplans/AUDIT-WP-0009-evidence-role-conformance.md b/workplans/AUDIT-WP-0009-evidence-role-conformance.md index 0a5d2fe..1fe68f3 100644 --- a/workplans/AUDIT-WP-0009-evidence-role-conformance.md +++ b/workplans/AUDIT-WP-0009-evidence-role-conformance.md @@ -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`