From 7728fde9b407945452c3b86c859a46e8bd1e3015 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 23:02:44 +0100 Subject: [PATCH] AUDIT-WP-0018-T02 done: nk-bounded-executor sender scope and sso ingress prepared - deploy/senders-scope.{json,yaml}: nk-bounded-executor (source net-kingdom.bounded-executor, tenants [platform], write-only, secret_policy reject, load-bearing); inert until a token is seeded - senders.py: the scope overlay may tighten secret_policy redact->reject, never relax it - networkpolicies.yaml: audit-core-nk-bounded-executor-ingress (sso AND app.kubernetes.io/name=bounded-executor, TCP 8080); not applied - tests on the executor's envelope shape: 202, 200 duplicate, 409, wrong source/tenant, secret-shaped step check 400 - T03 unblocked (window bound = admission window.not_after) - docs: emitter defect: executor AuditCore.emit rejects 202 Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196 --- WORK-RECORDS.md | 4 +- audit_core/senders.py | 30 +- deploy/networkpolicies.yaml | 27 ++ deploy/senders-scope.json | 13 + deploy/senders-scope.yaml | 17 ++ docs/bounded-executor-receipts.md | 43 +++ tests/test_bounded_executor_sender.py | 267 ++++++++++++++++++ tests/test_networkpolicies.py | 35 ++- ...P-0018-bounded-executor-receipt-custody.md | 27 +- 9 files changed, 452 insertions(+), 11 deletions(-) create mode 100644 tests/test_bounded_executor_sender.py diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 97ad798..2a8b2b8 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -113,8 +113,8 @@ | task | AUDIT-WP-0017-T06 | todo | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | | task | AUDIT-WP-0017-T07 | wait | — | workplans/AUDIT-WP-0017-production-audit-sources-and-correlation.md | | task | AUDIT-WP-0018-T01 | done | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | -| task | AUDIT-WP-0018-T02 | wait | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | -| task | AUDIT-WP-0018-T03 | wait | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | +| task | AUDIT-WP-0018-T02 | done | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | +| task | AUDIT-WP-0018-T03 | todo | — | workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md | | intake | AUDIT-IN-0001 | closed | — | intakes/intakes.md | | intake | AUDIT-IN-0002 | closed | — | intakes/intakes.md | | intake | AUDIT-IN-0003 | closed | — | intakes/intakes.md | diff --git a/audit_core/senders.py b/audit_core/senders.py index a400bbb..aecae07 100644 --- a/audit_core/senders.py +++ b/audit_core/senders.py @@ -21,7 +21,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable -from audit_core.redaction import POLICIES, POLICY_REDACT +from audit_core.redaction import POLICIES, POLICY_REDACT, POLICY_REJECT from audit_core.emission_cadence import parse_declaration WILDCARD = "*" @@ -314,7 +314,7 @@ def _apply_scope_overlay( may_read=( bool(extra["may_read"]) if "may_read" in extra else identity.may_read ), - secret_policy=identity.secret_policy, + secret_policy=_overlay_secret_policy(identity, extra), expires_at=identity.expires_at, evidence_kind=_overlay_evidence_kind(identity, extra), completeness_trade=( @@ -349,6 +349,32 @@ def _overlay_evidence_kind(identity: SenderIdentity, extra: dict[str, Any]) -> s return declared +def _overlay_secret_policy(identity: SenderIdentity, extra: dict[str, Any]) -> str: + """Let the overlay tighten redact to reject, never relax reject to redact. + + AUDIT-WP-0018-T02. A sender whose events are kept by hash (the bounded + executor's receipts) needs ``reject``: silent redaction would store a body + that no longer matches the hash the emitter computed. Declaring that in + Git means a Secret entry minted with the default cannot quietly weaken it. + The opposite direction would let a ConfigMap refresh start storing + secret-shaped fields, so it is refused. + """ + if "secret_policy" not in extra: + return identity.secret_policy + declared = str(extra["secret_policy"]) + if declared not in POLICIES: + raise ValueError( + f"sender {identity.name!r}: secret_policy must be one of {POLICIES}, " + f"got {declared!r}" + ) + if identity.secret_policy == POLICY_REJECT and declared == POLICY_REDACT: + raise ValueError( + f"sender {identity.name!r}: the scope overlay may not relax " + "secret_policy from reject to redact" + ) + return declared + + def _overlay_heartbeats( identity: SenderIdentity, extra: dict[str, Any] ) -> tuple[tuple[str, int], ...]: diff --git a/deploy/networkpolicies.yaml b/deploy/networkpolicies.yaml index 1b3aa2a..f12a6d7 100644 --- a/deploy/networkpolicies.yaml +++ b/deploy/networkpolicies.yaml @@ -63,6 +63,33 @@ spec: --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy +metadata: + name: audit-core-nk-bounded-executor-ingress + namespace: audit-core +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: audit-core + policyTypes: [Ingress] + ingress: + # AUDIT-WP-0018-T02. NetKingdom's bounded executor (Job pods run as + # ServiceAccount nk-bind-executor, package rapp-bounded-executor). Both + # selectors belong to one peer and are therefore ANDed. The whole sso + # namespace is NOT admitted: Authelia, LLDAP and KeyCape run there too. + # Prepared, not applied: the rollout is founder-run, together with the + # nk-bounded-executor registry seed. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: sso + podSelector: + matchLabels: + app.kubernetes.io/name: bounded-executor + ports: + - {protocol: TCP, port: 8080} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy metadata: name: audit-core-whitehat-ingress namespace: audit-core diff --git a/deploy/senders-scope.json b/deploy/senders-scope.json index 021bdd6..65e5fd0 100644 --- a/deploy/senders-scope.json +++ b/deploy/senders-scope.json @@ -49,5 +49,18 @@ "heartbeat_classes": { "informed-decision.memo.posed": 86400 } + }, + { + "name": "nk-bounded-executor", + "sources": [ + "net-kingdom.bounded-executor" + ], + "tenants": [ + "platform" + ], + "may_write": true, + "may_read": false, + "secret_policy": "reject", + "evidence_kind": "load-bearing" } ] diff --git a/deploy/senders-scope.yaml b/deploy/senders-scope.yaml index cd812be..236318f 100644 --- a/deploy/senders-scope.yaml +++ b/deploy/senders-scope.yaml @@ -7,6 +7,10 @@ # entry here for a sender with no token yet is inert. That is what makes the # approval-engine entry safe to land ahead of its credential. # +# secret_policy may be tightened here (redact -> reject) but never relaxed +# (AUDIT-WP-0018-T02): nk-bounded-executor receipts are kept by hash, so a +# secret-shaped field must refuse the event rather than be silently masked. +# # evidence_kind may be raised here (attributive -> load-bearing) but never # lowered: a ConfigMap refresh must not be able to drop a source's §9.6 # atomicity and detection obligations without anyone deciding to. @@ -71,5 +75,18 @@ data: "heartbeat_classes": { "informed-decision.memo.posed": 86400 } + }, + { + "name": "nk-bounded-executor", + "sources": [ + "net-kingdom.bounded-executor" + ], + "tenants": [ + "platform" + ], + "may_write": true, + "may_read": false, + "secret_policy": "reject", + "evidence_kind": "load-bearing" } ] diff --git a/docs/bounded-executor-receipts.md b/docs/bounded-executor-receipts.md index 6ad282e..f55d002 100644 --- a/docs/bounded-executor-receipts.md +++ b/docs/bounded-executor-receipts.md @@ -163,3 +163,46 @@ value appears in this repository. entry. - **Founder:** the live registry entry and NetworkPolicy rollout are an approved live change. Audit-core prepares them; the founder runs them. + +## Status 2026-10-31 — T02 prepared after NetKingdom's answers + +NetKingdom answered on hub messages `ed03ba30` and `deea5bf8`. The answers are +based on executor.py at net-kingdom a020758 and rapp-bounded-executor 9826a24. + +- Source `net-kingdom.bounded-executor`. Event types `execution.run.started` and + `execution.attribution.receipt`. Tenant `platform`, which is the exact + string. Note that it is not `tenant:platform`, which approval-engine and + informed-decision use. The `started` event is adopted, so the stream is + `load-bearing`. +- **Registry entry (prepared, not seeded):** `nk-bounded-executor` in + `deploy/senders-scope.json` and `deploy/senders-scope.yaml`. The entry admits + nothing until the Secret carries a token for that name. The overlay may now + tighten `secret_policy` from `redact` to `reject`, but it may never relax it + (`audit_core/senders.py`). Even if the Secret entry is minted with the + default, the result is therefore `reject`. +- **Ingress (prepared, not applied):** `audit-core-nk-bounded-executor-ingress` + in `deploy/networkpolicies.yaml` admits only namespace `sso` combined (AND) + with pod label `app.kubernetes.io/name=bounded-executor`, on TCP 8080. +- **Tests:** `tests/test_bounded_executor_sender.py` runs on the executor's + envelope shape. It checks: 202 accept; 200 duplicate on retry; 409 on a + reused run id with a different body; 400 `source_not_allowed` and + `tenant_not_allowed`; 400 `secret_shaped_field` for a secret-shaped step + check. +- **Defect found in the emitter:** `bounded-executor/clients.py` + `AuditCore.emit` accepts only 200 and 201. Audit-core answers a first accept + with **202**, so the executor would treat a stored `started` event as a + failure and stop with `audit_unavailable`. NetKingdom has to accept 202 + (accepted) and 200 (duplicate). It should treat 409 as a run-id-reuse + defect and must not retry it. +- **T03 window bound:** a `started` event that has no receipt with the same + `correlation_id` becomes a finding after the admission's `window.not_after`. + Pilot windows are 2 h. + +Remaining work: + +- **Founder-run:** mint and seed the Secret entry, then roll out the ConfigMap + and the NetworkPolicy. +- **NetKingdom:** the versioned receipt schema with fixtures (NK-WP-0040-T02) + and the 202 fix. +- **railiance-platform:** custody of the sender bearer (ESO projection to + `sso`). diff --git a/tests/test_bounded_executor_sender.py b/tests/test_bounded_executor_sender.py new file mode 100644 index 0000000..2621062 --- /dev/null +++ b/tests/test_bounded_executor_sender.py @@ -0,0 +1,267 @@ +"""AUDIT-WP-0018-T02. The bounded executor as a registered sender, on fixtures. + +The envelopes below follow net-kingdom ``bounded-executor/executor.py`` +(``Run._event`` / ``Run._receipt``, commit a020758) key for key. Values are +synthetic. The registry is built the way production builds it: a Secret-shaped +``AUDIT_CORE_SENDERS`` entry (here deliberately minted with the defaults) plus +the Git-declared scope overlay in ``deploy/senders-scope.json``, so these tests +also prove that the overlay alone yields the declared posture. + +Canonical receipt schema validation is AUDIT-WP-0014-T02 and waits on +NetKingdom's versioned schema (NK-WP-0040-T02); nothing here validates the +receipt beyond the envelope and the secret-shaped-field scan. +""" + +import copy +import io +import json +from pathlib import Path + +import pytest + +from audit_core.ingestion import IngestionApplication +from audit_core.senders import SenderRegistry +from audit_core.sqlite_backend import SQLiteAuditBackend + +ROOT = Path(__file__).resolve().parents[1] +SCOPE_FILE = ROOT / "deploy" / "senders-scope.json" + +TOKEN = "fixture-only-executor" +RUN_ID = "3f0c2a8e-0000-4000-8000-000000000001" +ADMISSION = "bea-pilot-0001" + + +def started_event(): + return { + "id": f"run:{RUN_ID}:started", + "type": "execution.run.started", + "correlation_id": RUN_ID, + "subject": f"admission:{ADMISSION}", + "source": "net-kingdom.bounded-executor", + "tenant": "platform", + "occurred_at": "2026-10-31T10:00:05+00:00", + "data": { + "run_id": RUN_ID, + "admission": ADMISSION, + "approval": "apr-0001", + "consumed_at": "2026-10-31T10:00:04Z", + "executing_workload": "service:nk-bind-executor", + }, + } + + +def receipt_event(): + return { + "id": f"run:{RUN_ID}:receipt", + "type": "execution.attribution.receipt", + "correlation_id": RUN_ID, + "subject": f"admission:{ADMISSION}", + "source": "net-kingdom.bounded-executor", + "tenant": "platform", + "occurred_at": "2026-10-31T10:03:00+00:00", + "data": { + "run_id": RUN_ID, + "initiator": {"sub": "approver-subject-1", "role": "approver"}, + "executing_workload": "service:nk-bind-executor", + "runtime_principal": "system:serviceaccount:sso:nk-bind-executor", + "tenant": "platform", + "environment": "railiance01", + "source_commit": "a020758", + "artifact_digest": "sha256:" + "a" * 64, + "target": f"admission:{ADMISSION}", + "action": "bounded-execution", + "decision_refs": [ + {"label": "admission", "ref": ADMISSION}, + {"label": "admission_digest", "ref": "sha256:" + "b" * 64}, + {"label": "approval", "ref": "apr-0001"}, + ], + "pdp": "not-evaluated", + "time_interval": { + "start": "2026-10-31T10:00:04Z", + "end": "2026-10-31T10:03:00Z", + "clock_source": "system-utc", + }, + "outcome": "completed", + "reverted": False, + "attempts": 1, + "budget": { + "limit": {"wall_time_seconds": 600, "attempts": 1}, + "used": {"wall_time_seconds": 176.2, "attempts": 1}, + }, + "steps": [ + { + "id": "s1", "kind": "step", "operation": "rollout-restart", + "target": "deployment/authelia", "consumer": "sso", + "outcome": "done", + "checks": [{"id": "c1", "result": "pass"}], + }, + ], + }, + } + + +def registry(): + # Minted with the defaults on purpose (redact, attributive, wildcard + # source/tenant): the Git overlay must narrow and tighten it on its own. + secret = json.dumps([ + {"name": "nk-bounded-executor", "tokens": [TOKEN], "sources": ["*"], "tenants": ["*"]}, + {"name": "operator", "tokens": ["operator"], "sources": ["*"], "tenants": ["*"], + "may_write": False, "may_read": True}, + ]) + return SenderRegistry.from_env({ + "AUDIT_CORE_SENDERS": secret, + "AUDIT_CORE_SENDERS_SCOPE_PATH": str(SCOPE_FILE), + }) + + +@pytest.fixture +def app(tmp_path): + return IngestionApplication(SQLiteAuditBackend(str(tmp_path / "executor.db")), registry()) + + +def post(app, event, *, token=TOKEN): + raw = json.dumps(event).encode() + environ = { + "PATH_INFO": "/v1/events", + "REQUEST_METHOD": "POST", + "CONTENT_LENGTH": str(len(raw)), + "wsgi.input": io.BytesIO(raw), + "HTTP_AUTHORIZATION": f"Bearer {token}", + # The executor sets Idempotency-Key = envelope id (clients.AuditCore.emit). + "HTTP_IDEMPOTENCY_KEY": event["id"], + } + result = {} + out = b"".join(app(environ, lambda status, headers: result.update(status=status))) + return int(result["status"].split()[0]), (json.loads(out) if out else {}) + + +# --- registration ----------------------------------------------------------- + +def test_scope_entry_is_exact_write_only_reject_load_bearing(): + entry = next(e for e in json.loads(SCOPE_FILE.read_text()) if e["name"] == "nk-bounded-executor") + assert entry == { + "name": "nk-bounded-executor", + "sources": ["net-kingdom.bounded-executor"], + "tenants": ["platform"], + "may_write": True, + "may_read": False, + "secret_policy": "reject", + "evidence_kind": "load-bearing", + } + + +def test_overlay_narrows_a_default_minted_secret_entry(): + identity = registry().authenticate(f"Bearer {TOKEN}") + assert identity.name == "nk-bounded-executor" + assert identity.sources == frozenset({"net-kingdom.bounded-executor"}) + assert identity.tenants == frozenset({"platform"}) + assert identity.may_write and not identity.may_read + assert identity.secret_policy == "reject" + assert identity.is_load_bearing + assert identity.completeness_trade is None + + +def test_scope_entry_admits_nothing_without_a_token(): + """Safe to land ahead of the founder-run seed.""" + reg = 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 [i.name for i in reg.identities] == ["user-engine"] + + +def test_overlay_may_not_relax_reject_to_redact(): + with pytest.raises(ValueError, match="may not relax"): + SenderRegistry.from_env({ + "AUDIT_CORE_SENDERS": json.dumps([{ + "name": "nk-bounded-executor", "tokens": ["t"], + "sources": ["net-kingdom.bounded-executor"], "secret_policy": "reject", + }]), + "AUDIT_CORE_SENDERS_SCOPE": json.dumps( + [{"name": "nk-bounded-executor", "secret_policy": "redact"}] + ), + }) + + +def test_overlay_rejects_an_unknown_secret_policy(): + with pytest.raises(ValueError, match="secret_policy must be one of"): + SenderRegistry.from_env({ + "AUDIT_CORE_SENDERS": json.dumps([{"name": "x", "tokens": ["t"], "sources": ["x"]}]), + "AUDIT_CORE_SENDERS_SCOPE": json.dumps([{"name": "x", "secret_policy": "ignore"}]), + }) + + +# --- ingest behaviour on the executor's real envelope shape ----------------- + +@pytest.mark.parametrize("make", [started_event, receipt_event]) +def test_accept_is_202(app, make): + status, body = post(app, make()) + assert status == 202 + assert body["status"] == "accepted" + + +@pytest.mark.parametrize("make", [started_event, receipt_event]) +def test_retry_is_a_200_duplicate(app, make): + assert post(app, make())[0] == 202 + status, body = post(app, make()) + assert status == 200 + assert body["status"] == "duplicate" + + +def test_started_and_receipt_share_the_run_correlation(app): + assert post(app, started_event())[0] == 202 + assert post(app, receipt_event())[0] == 202 + + +def test_reused_run_id_with_a_different_body_is_a_409(app): + assert post(app, receipt_event())[0] == 202 + other = receipt_event() + other["data"]["outcome"] = "reverted" + other["data"]["reverted"] = True + status, body = post(app, other) + assert status == 409 + assert body["error"] == "event_id_conflict" + + +@pytest.mark.parametrize("source", [ + "net-kingdom", "bounded-executor", "net-kingdom.bounded-executor ", "approval-engine", +]) +def test_wrong_source_is_refused(app, source): + event = receipt_event() + event["source"] = source + status, body = post(app, event) + assert status == 400 + assert body["error"] == "source_not_allowed" + + +@pytest.mark.parametrize("tenant", [ + "tenant:platform", "Platform", "platform ", "tenant:friendly:binky", "*", +]) +def test_wrong_tenant_is_refused(app, tenant): + event = receipt_event() + event["tenant"] = tenant + status, body = post(app, event) + assert status == 400 + assert body["error"] == "tenant_not_allowed" + + +@pytest.mark.parametrize("field", ["token", "password", "client_secret", "private_key"]) +def test_secret_shaped_step_check_is_refused_with_400(app, field): + event = receipt_event() + event["data"]["steps"][0]["checks"][0][field] = "synthetic-not-a-secret" + status, body = post(app, event) + assert status == 400 + assert body["error"] == "secret_shaped_field" + # Not stored: the clean receipt for the same run is still accepted afterwards. + assert post(app, receipt_event())[0] == 202 + + +def test_the_real_receipt_shape_carries_no_secret_shaped_key(app): + """If the executor ever adds such a key, its receipts start bouncing; this + pins that the current shape passes the reject policy unmodified.""" + from audit_core.redaction import scan + + assert scan(started_event()["data"]) == [] + assert scan(receipt_event()["data"]) == [] diff --git a/tests/test_networkpolicies.py b/tests/test_networkpolicies.py index 0850fab..df76f08 100644 --- a/tests/test_networkpolicies.py +++ b/tests/test_networkpolicies.py @@ -129,9 +129,42 @@ def test_every_sender_has_exactly_one_ingress_rule(): scope = json.loads((ROOT / "deploy" / "senders-scope.json").read_text()) senders = {entry["name"] for entry in scope} assert senders == { - "user-engine", "approval-engine", "informed-decision", "tenant-engine" + "user-engine", "approval-engine", "informed-decision", "tenant-engine", + "nk-bounded-executor", } # user-engine's rule predates the per-sender naming convention. assert "name: audit-core-sender-ingress" in text for sender in senders - {"user-engine"}: assert f"name: audit-core-{sender}-ingress" in text + + +def test_bounded_executor_ingress_is_bound_to_sso_and_executor_pod_label(): + """AUDIT-WP-0018-T02. Only the executor's pods, not the whole sso namespace. + + Authelia, LLDAP and KeyCape run in sso too; a namespace-only rule would + let any of them reach the receiver's write port. + """ + documents = (ROOT / "deploy" / "networkpolicies.yaml").read_text().split("\n---\n") + policy = next( + document + for document in documents + if "name: audit-core-nk-bounded-executor-ingress" in document + ) + expected_peer = """ - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: sso + podSelector: + matchLabels: + app.kubernetes.io/name: bounded-executor""" + assert expected_peer in policy + assert policy.count(" - namespaceSelector:") == 1 + assert policy.count(" - {protocol: TCP, port:") == 1 + assert " - {protocol: TCP, port: 8080}" in policy + + +def test_no_egress_to_sso_is_created(): + """The receiver answers the executor; it never calls into sso.""" + documents = (ROOT / "deploy" / "networkpolicies.yaml").read_text().split("\n---\n") + for document in documents: + if "Egress" in document and "egress:" in document: + assert "kubernetes.io/metadata.name: sso" not in document diff --git a/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md b/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md index 0ea15eb..a353851 100644 --- a/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md +++ b/workplans/AUDIT-WP-0018-bounded-executor-receipt-custody.md @@ -46,10 +46,8 @@ targets and actions, and has to make `decision_refs` typed. ```task id: AUDIT-WP-0018-T02 -status: wait +status: done priority: high -blocking_reason: "NetKingdom must confirm source, event types, tenant binding and the started event; the rapp-* package must name pod labels and ServiceAccount; railiance-platform must route the ESO-projected bearer." -blocked_on: message-from:net-kingdom state_hub_task_id: "3f251d30-e62d-5849-88d9-d033097b4f7d" ``` @@ -70,14 +68,27 @@ Then test on synthetic fixtures: The live registry seed and the rollout are founder-run. +Done 2026-10-31, from NetKingdom hub messages ed03ba30 and deea5bf8. Scope +entry: `deploy/senders-scope.{json,yaml}`. The overlay may now tighten +`secret_policy` but never relax it. Ingress: +`audit-core-nk-bounded-executor-ingress`. Tests: +`tests/test_bounded_executor_sender.py`. Nothing is applied. + +- **Founder-run:** mint the Secret entry, seed it, and roll out the ConfigMap + and the NetworkPolicy. +- **Still owed by NetKingdom:** NK-WP-0040-T02 (schema and fixtures). Also the + executor must accept 202: `clients.AuditCore.emit` accepts only 200 and 201 + today. +- **Still owed by railiance-platform:** bearer custody. + +See `docs/bounded-executor-receipts.md`, "Status 2026-10-31". + ## Detect runs that started without a receipt ```task id: AUDIT-WP-0018-T03 -status: wait +status: todo priority: medium -blocking_reason: "Depends on NetKingdom adopting the execution.run.started event and stating the admission window bound after which a missing receipt is a finding." -blocked_on: message-from:net-kingdom state_hub_task_id: "ec2cd9b7-be65-5792-b546-ffd0b3937856" ``` @@ -88,6 +99,10 @@ declared window. Reuse the existing stream-completeness/cadence surface adding a new monitor. The report is a finding for the verifier and the founder. Audit-core does not infer what the run did. +Unblocked 2026-10-31. NetKingdom adopted the started event and set the +bound: the admission's `window.not_after`. Pilot windows are 2 h. The receipt +also carries `time_interval.end`. + ## Founder decision 2026-10-31 Approved by the founder (`proposed` -> `ready`) after the NK-WP-0056-T02 owner answers (hub decision 19afd15e). Founder adopted the `execution.run.started` event plus one receipt per run with an admission-level target/action, `steps[]` detail and labelled `decision_refs`.