NK-WP-0056-T15: receipt schema v0.1, validator and executor fixtures; audit sender exercise

- canon/schemas/execution-attribution-receipt_v0.1.schema.json: the bounded-executor profile
  of the PCC v0.2 receipt, both audit-core events (started, receipt), every object closed.
- tools/execution-attribution-receipt: EAR-1..EAR-10 validator, fixtures generated from the
  real executor (completed, reverted, completed-on-retry) with a drift check.
- executor: started carries receipt_version, digests, steps_planned and data.window.not_after
  (audit-core AUDIT-WP-0018-T03 request); receipt carries receipt_version and initiator.iss
  (configured approver_issuer, else unknown).
- bounded-executor/exercise_audit_sender.py: real executor and client against audit-core's
  real ingestion and the Git scope overlay: 202, duplicate 200, conflict 409 as a defect,
  wrong source/tenant/token refused and not stored, a receipt-less start flagged by its own
  window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 523928@bnt-lap001
Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
This commit is contained in:
tegwick 2026-11-04 01:39:57 +01:00
parent 1d4b7e84d7
commit d7133e3a04
15 changed files with 1506 additions and 7 deletions

View file

@ -209,6 +209,9 @@ bounded-execution-admission-test: ## Run bounded-execution admission validator t
bounded-execution-admission-check: ## Validate the example admissions (read-only, offline)
python3 tools/bounded-execution-admission/validate.py validate examples/bounded-execution-admission/*.yaml
execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15)
python3 -m pytest tools/execution-attribution-receipt/tests
bounded-executor-test: ## Bounded executor RBAC and declaration tests; the pilot admission inside its declared grants (NK-WP-0056-T03)
python3 -m pytest sso-mfa/k8s/bounded-executor bounded-executor/tests tests/test_executor_lanes.py
python3 tools/service-identity/validate.py tools/service-identity/declarations/bounded-executor.yaml \
@ -395,7 +398,7 @@ security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody a
openbao-init-unseal openbao-init-unseal-dry-run \
iam-profile-conformance-test playbook-contract-test posture-feedback-test \
custody-record-test service-identity-test service-identity-check security-tools \
bounded-execution-admission-test bounded-execution-admission-check bounded-executor-test \
bounded-execution-admission-test bounded-execution-admission-check bounded-executor-test execution-attribution-receipt-test \
emission-cadence-profile-test \
posture-feedback-reference security-scenario-compose-lightweight \
security-bootstrap-console-test security-bootstrap-scripts-syntax \

View file

@ -128,7 +128,8 @@ def build(cfg: dict, logins: list):
alert=alert)
config = X.Config(executor_identity=cfg["executor_identity"], runtime_principal=cfg["runtime_principal"],
tenant=cfg["tenant"], environment=cfg["environment"],
approver_subjects=dict(cfg["approver_subjects"]))
approver_subjects=dict(cfg["approver_subjects"]),
approver_issuer=cfg.get("approver_issuer", "unknown"))
return config, deps

View file

@ -43,6 +43,7 @@ _spec.loader.exec_module(bea)
ACTION = "bounded-execution"
STARTED = "execution.run.started"
RECEIPT = "execution.attribution.receipt"
RECEIPT_VERSION = "0.1" # canon/schemas/execution-attribution-receipt_v0.1.schema.json
class Refused(Exception):
@ -81,6 +82,7 @@ class Config:
approver_subjects: dict[str, str] # admission approver name -> carrier subject_id (no guessing)
source_root: pathlib.Path = REPO
clock_source: str = "system-utc"
approver_issuer: str = "unknown" # the approvers' token issuer; recorded as `unknown` unless configured
@dataclasses.dataclass
@ -320,8 +322,9 @@ class Run:
def _receipt(self, outcome: str, attempts: int) -> dict:
end = self.d.wall()
return self._event(RECEIPT, occurred_at=end, body={
"receipt_version": RECEIPT_VERSION,
"run_id": self.run_id,
"initiator": {"sub": self.approver_subject, "role": "approver"},
"initiator": {"iss": self.cfg.approver_issuer, "sub": self.approver_subject, "role": "approver"},
"executing_workload": self.cfg.executor_identity,
"runtime_principal": self.cfg.runtime_principal,
"tenant": self.cfg.tenant, "environment": self.cfg.environment,
@ -358,10 +361,16 @@ class Run:
self.d.alert("consume_unconfirmed", {"admission": self.spec["id"], "run_id": self.run_id})
return {"outcome": "refused", "reason": str(refusal), "run_id": self.run_id, "effects": "consume-only"}
try:
self.d.audit.emit(self._event(STARTED, {"run_id": self.run_id, "admission": self.spec["id"],
"approval": self.approval_id, "consumed_at": self.consumed_at,
"executing_workload": self.cfg.executor_identity},
occurred_at=self.d.wall()))
self.d.audit.emit(self._event(STARTED, {
"receipt_version": RECEIPT_VERSION, "run_id": self.run_id, "admission": self.spec["id"],
"admission_digest": bea.admission_digest(self.doc), "plan_digest": self.spec["plan_digest"],
"approval": self.approval_id, "consumed_at": self.consumed_at,
"executing_workload": self.cfg.executor_identity, "runtime_principal": self.cfg.runtime_principal,
"steps_planned": len(self.spec["plan"]["steps"]),
# audit-core holds each run to its own window (AUDIT-WP-0018-T03): a started event without a receipt
# becomes a finding after the earlier of this and the sender's declared bound.
"window": {"not_after": iso(bea.parse_time(self.spec["window"]["not_after"]))}},
occurred_at=self.d.wall()))
except Exception:
self.d.alert("started_not_in_custody", {"admission": self.spec["id"], "run_id": self.run_id})
return {"outcome": "stopped", "reason": "audit_unavailable", "run_id": self.run_id, "effects": "consume-only"}

View file

@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""Executor audit sender against audit-core's real ingestion (NK-WP-0056-T15).
audit-core's source (IngestionApplication, SQLite backend) is served on loopback with its sender registry built the
way production builds it: a Secret-shaped AUDIT_CORE_SENDERS entry plus the Git-declared scope overlay
deploy/senders-scope.json. The executor side is real: executor.Run.execute() on in-memory operations, emitting
through clients.AuditCore over HTTP with a token file.
Checks:
- a completed run and a reverted run: started and receipt accepted (202), and both pairs pass the receipt validator
(tools/execution-attribution-receipt, schema v0.1)
- a retry of the same event is a duplicate (200); the same id with another body is a conflict (409), which the
client reports as a run-id-reuse defect
- wrong source, wrong tenant (`tenant:platform`), no token and a wrong token are refused; nothing is stored for them
- a started event whose admission window has passed, with no receipt, becomes an opening_without_closing finding
held to that window; the closed runs do not
Synthetic tokens only. Writes a value-free receipt. Exit 0 when every check passes.
python3 bounded-executor/exercise_audit_sender.py --receipt FILE [--audit-core-source ~/audit-core]
"""
from __future__ import annotations
import argparse
import copy
import datetime as dt
import hashlib
import importlib.util
import io
import json
import os
import pathlib
import secrets
import subprocess
import sys
import tempfile
import threading
import urllib.error
import urllib.request
import uuid
from wsgiref.simple_server import WSGIRequestHandler, make_server
HERE = pathlib.Path(__file__).resolve().parent
REPO = HERE.parent
TOOL = REPO / "tools/execution-attribution-receipt"
def _load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
G = _load("ear_generate", TOOL / "generate_fixtures.py") # loads executor.py as `executor`
V = _load("ear_validate", TOOL / "validate.py")
X = G.X
C = _load("clients", HERE / "clients.py")
def commit(path: pathlib.Path) -> str:
return subprocess.run(["git", "-C", str(path), "rev-parse", "HEAD"], capture_output=True, text=True,
check=True).stdout.strip()
def private_write(path: pathlib.Path, value: str) -> None:
with path.open("x") as out:
os.chmod(path, 0o600)
out.write(value)
def run_executor(name: str, audit, fail_checks=()) -> list[dict]:
"""One real executor run on the example admission with a window around now; returns the events it emitted."""
doc = G.X.bea.load(REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml")
now = dt.datetime.now(dt.UTC).replace(microsecond=0)
doc["spec"]["id"] = name
doc["spec"]["window"] = {"not_before": (now - dt.timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
"not_after": (now + dt.timedelta(hours=2)).isoformat().replace("+00:00", "Z")}
doc["spec"]["budget"]["attempts"] = 1 # a failed check then ends the run as reverted, not retried
doc["spec"]["plan_digest"] = X.bea.plan_digest(doc)
fake = G.Fake(doc, fail_checks)
seen: list[dict] = []
class Tee:
def emit(self, event):
audit.emit(event) # raises unless audit-core stored it
seen.append(event)
plan = doc["spec"]["plan"]
deps = X.Deps(carrier=fake, operations={s["operation"]: fake.op for s in plan["steps"] + plan["revert_steps"]},
checks={c["id"]: fake.check for c in doc["spec"]["checks"]}, trust=lambda _s: True, audit=Tee(),
alert=lambda *_a: None, sleep=lambda _s: None)
cfg = X.Config(executor_identity=doc["spec"]["executor"]["identity"],
runtime_principal="system:serviceaccount:sso:nk-bind-executor", tenant="platform",
environment="TEST", approver_subjects={"admin": "kc-sub-0f3a"},
approver_issuer="https://kc.coulomb.social")
run = X.Run(doc, G.APPROVAL, cfg, deps)
run.preflight = lambda: run._check_approval(fake.get(G.APPROVAL), consumed_ok=False) # pins are the example's
result = run.execute()
assert result["outcome"] in ("completed", "reverted") and "reason" not in result, result.get("reason")
if G.MARKER in json.dumps(seen):
raise RuntimeError("an operation value reached an event")
return seen
def exercise(args) -> dict:
sys.path.insert(0, str(args.audit_core_source))
from audit_core.ingestion import IngestionApplication
from audit_core.senders import SenderRegistry
from audit_core.sqlite_backend import SQLiteAuditBackend
scope = args.audit_core_source / "deploy/senders-scope.json"
receipt = {
"schema": "netkingdom.bounded-executor.audit-sender-exercise/v1",
"task": "NK-WP-0056-T15",
"target": "disposable local processes; synthetic tokens",
"started_at": dt.datetime.now(dt.UTC).isoformat(timespec="seconds"),
"audit_core_commit": commit(args.audit_core_source),
"senders_scope_sha256": hashlib.sha256(scope.read_bytes()).hexdigest(),
"net_kingdom_commit": commit(REPO),
"executor_source_sha256": {n: hashlib.sha256((HERE / n).read_bytes()).hexdigest()
for n in ("executor.py", "clients.py")},
"receipt_schema_sha256": hashlib.sha256(V.SCHEMA.read_bytes()).hexdigest(),
"limitations": [
"audit-core runs from source in-process (SQLite backend), not the released receiver image",
"the executor's operations are in-memory; the bind rotation itself is T12's synthetic pilot",
"plain HTTP on loopback; the live path is the ESO-delivered bearer over the sso ingress (T16)",
],
"checks": {},
}
checks = receipt["checks"]
tokens = {"executor": secrets.token_urlsafe(32), "operator": secrets.token_urlsafe(32),
"other": secrets.token_urlsafe(32)}
with tempfile.TemporaryDirectory(prefix="nk56-t15-") as temporary:
root = pathlib.Path(temporary)
registry = SenderRegistry.from_env({
# Minted with the defaults on purpose, as audit-core's own test does: the Git overlay must narrow it.
"AUDIT_CORE_SENDERS": json.dumps([
{"name": "nk-bounded-executor", "tokens": [tokens["executor"]], "sources": ["*"], "tenants": ["*"]},
{"name": "user-engine", "tokens": [tokens["other"]], "sources": ["user-engine"]},
{"name": "operator", "tokens": [tokens["operator"]], "sources": ["*"], "tenants": ["*"],
"may_write": False, "may_read": True}]),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(scope)})
backend = SQLiteAuditBackend(str(root / "audit.db"))
app = IngestionApplication(backend, registry)
class Quiet(WSGIRequestHandler):
def log_message(self, *_args):
pass
server = make_server("127.0.0.1", 0, app, handler_class=Quiet)
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{server.server_port}"
try:
for name, value in tokens.items():
private_write(root / f"{name}.token", value + "\n")
audit = C.AuditCore(base, str(root / "executor.token"))
def raw(event, token: str | None = "executor", path="/v1/events", method="POST"):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + (root / f"{token}.token").read_text().strip() \
if token in tokens else "Bearer " + token
if event is not None:
headers["Idempotency-Key"] = event["id"]
req = urllib.request.Request(base + path, method=method, headers=headers,
data=json.dumps(event).encode() if event is not None else None)
try:
with urllib.request.urlopen(req, timeout=5) as response:
return response.status, json.loads(response.read() or b"{}")
except urllib.error.HTTPError as error:
with error:
return error.code, json.loads(error.read() or b"{}")
# ---- real runs through the real client
first_status = []
original = audit.http.call
def recording(method, path, body=None, headers=None, scope=None):
status, data = original(method, path, body, headers, scope)
first_status.append(status)
return status, data
audit.http.call = recording
completed = run_executor("BEA-2026-9201", audit)
reverted = run_executor("BEA-2026-9202", audit, fail_checks=["synthetic-journey"])
audit.http.call = original
assert first_status == [202, 202, 202, 202], first_status
assert completed[1]["data"]["outcome"] == "completed" and reverted[1]["data"]["outcome"] == "reverted"
checks["completed_run_started_and_receipt_accepted_202"] = True
checks["reverted_run_started_and_receipt_accepted_202"] = True
for started, final in (completed, reverted):
assert V.evaluate_pair(started, final) == {"valid": True, "findings": []}
checks["emitted_pairs_pass_receipt_schema_v0_1"] = True
assert raw(completed[0])[0] == 200 and raw(completed[1])[0] == 200
audit.emit(completed[1]) # the client's own retry path counts 200 as stored
checks["retry_is_duplicate_200"] = True
changed = copy.deepcopy(completed[1])
changed["data"]["outcome"] = "stopped"
assert raw(changed)[0] == 409
try:
audit.emit(changed)
except C.ClientError as error:
assert "409" in str(error) and "run id reused" in str(error)
else:
raise AssertionError("conflict accepted")
checks["same_id_other_body_conflict_409_reported_as_defect"] = True
def fresh(event, **over):
e = copy.deepcopy(event)
rid = str(uuid.uuid4())
e.update(id=f"run:{rid}:started", correlation_id=rid, **over)
e["data"]["run_id"] = rid
return e
refused = {
"wrong_source": (fresh(completed[0], source="user-engine"), "executor"),
"wrong_tenant": (fresh(completed[0], tenant="tenant:platform"), "executor"),
"other_sender_token": (fresh(completed[0]), "other"),
"no_token": (fresh(completed[0]), None),
"unknown_token": (fresh(completed[0]), "synthetic-unknown-token"),
}
for label, (event, token) in refused.items():
status, _ = raw(event, token)
assert status in (400, 401, 403), (label, status)
for event, _ in refused.values(): # nothing stored under a refused event's run id
status, body = raw(None, "operator", "/v1/events?correlation_id=" + event["correlation_id"], "GET")
assert status == 200 and body["events"] == [], status
status, body = raw(None, "operator", "/v1/events?correlation_id=" + completed[0]["correlation_id"], "GET")
assert status == 200 and len(body["events"]) == 2, status # while the accepted run's events are held
checks["wrong_source_tenant_or_token_refused_and_not_stored"] = True
# ---- a started event without a receipt, whose admission window closed two hours ago
orphan = fresh(completed[0])
orphan["data"]["window"]["not_after"] = (dt.datetime.now(dt.UTC) - dt.timedelta(hours=2)) \
.isoformat(timespec="seconds").replace("+00:00", "Z")
audit.emit(orphan)
status, body = raw(None, "operator", "/v1/stream-findings", "GET")
assert status == 200
opened = [f for f in body.get("stream_findings", []) if f.get("kind") == "opening_without_closing"
and f.get("class") == "execution.run.started"]
flagged = json.dumps(opened)
mine = [f for f in opened if f.get("correlation_id") == orphan["correlation_id"]]
assert len(mine) == 1 and mine[0]["bound"] == "window.not_after", "orphan not flagged by its window"
assert completed[0]["correlation_id"] not in flagged and reverted[0]["correlation_id"] not in flagged
checks["started_without_receipt_flagged_by_its_own_window"] = True
checks["closed_runs_not_flagged"] = True
finally:
server.shutdown()
server.server_close()
receipt.update(status="passed", cleanup_complete=True,
finished_at=dt.datetime.now(dt.UTC).isoformat(timespec="seconds"))
if any(v in json.dumps(receipt) for v in tokens.values()):
raise RuntimeError("receipt would carry a generated value")
return receipt
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("--audit-core-source", type=pathlib.Path, default=pathlib.Path.home() / "audit-core")
parser.add_argument("--receipt", required=True, type=pathlib.Path)
parser.add_argument("--trace", action="store_true", help="print the failing line (no values)")
args = parser.parse_args(argv)
if args.receipt.exists():
raise SystemExit("receipt path must be new")
try:
receipt = exercise(args)
except Exception as error:
where = ""
if args.trace:
tb, line = error.__traceback__, None
while tb:
if tb.tb_frame.f_code.co_filename == __file__:
line = tb.tb_lineno
tb = tb.tb_next
detail = str(error) if isinstance(error, AssertionError) else "" # own messages: statuses, labels
where = f" at line {line} {detail}".rstrip()
print(f"Audit sender exercise failed: {type(error).__name__}{where}", file=sys.stderr)
return 1
private_write(args.receipt, json.dumps(receipt, indent=2) + "\n")
print(f"Audit sender exercise passed: {len(receipt['checks'])} checks; temporary resources removed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,152 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://netkingdom.io/schemas/execution-attribution-receipt_v0.1.schema.json",
"title": "NetKingdom Execution Attribution Receipt v0.1 (bounded-executor profile)",
"description": "The two audit-core events of one bounded-executor run: execution.run.started and execution.attribution.receipt, in audit-core's eight-field ingest envelope (audit-core docs/event-envelope.md, docs/bounded-executor-receipts.md). Fields follow Playbook Capability Contract v0.2 'Execution Attribution Receipt' and BEA v0.1 section 6. Structure only; the cross-field rules (EAR-1..EAR-10) are in tools/execution-attribution-receipt/validate.py. Values handled by operations never appear: steps carry identifiers, outcomes, check results and error types only.",
"oneOf": [
{"$ref": "#/$defs/started_event"},
{"$ref": "#/$defs/receipt_event"}
],
"$defs": {
"run_id": {"type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"},
"digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
"admission_id": {"type": "string", "pattern": "^BEA-[0-9]{4}-[0-9]{4}$"},
"time": {"type": "string", "format": "date-time", "pattern": "(Z|[+-][0-9]{2}:[0-9]{2})$"},
"identifier": {"type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[A-Za-z0-9][A-Za-z0-9:._/@=,+-]*$"},
"envelope": {
"type": "object",
"required": ["id", "type", "correlation_id", "subject", "source", "tenant", "occurred_at", "data"],
"properties": {
"id": {"type": "string", "pattern": "^run:[0-9a-f-]{36}:(started|receipt)$"},
"correlation_id": {"$ref": "#/$defs/run_id"},
"subject": {"type": "string", "pattern": "^admission:BEA-[0-9]{4}-[0-9]{4}$"},
"source": {"const": "net-kingdom.bounded-executor"},
"tenant": {"$ref": "#/$defs/identifier"},
"occurred_at": {"$ref": "#/$defs/time"}
}
},
"started_event": {
"allOf": [{"$ref": "#/$defs/envelope"}],
"type": "object",
"additionalProperties": false,
"properties": {
"id": true, "correlation_id": true, "subject": true, "source": true, "tenant": true, "occurred_at": true,
"type": {"const": "execution.run.started"},
"data": {
"type": "object",
"additionalProperties": false,
"required": ["receipt_version", "run_id", "admission", "admission_digest", "plan_digest", "approval",
"consumed_at", "executing_workload", "runtime_principal", "steps_planned", "window"],
"properties": {
"receipt_version": {"const": "0.1"},
"run_id": {"$ref": "#/$defs/run_id"},
"admission": {"$ref": "#/$defs/admission_id"},
"admission_digest": {"$ref": "#/$defs/digest"},
"plan_digest": {"$ref": "#/$defs/digest"},
"approval": {"$ref": "#/$defs/identifier"},
"consumed_at": {"anyOf": [{"$ref": "#/$defs/time"}, {"type": "null"}]},
"executing_workload": {"$ref": "#/$defs/identifier"},
"runtime_principal": {"$ref": "#/$defs/identifier"},
"steps_planned": {"type": "integer", "minimum": 1},
"window": {
"type": "object", "additionalProperties": false, "required": ["not_after"],
"properties": {"not_after": {"$ref": "#/$defs/time"}}
}
}
}
}
},
"check_result": {
"type": "object", "additionalProperties": false, "required": ["id", "result"],
"properties": {
"id": {"$ref": "#/$defs/identifier"},
"result": {"enum": ["pass", "fail", "budget"]}
}
},
"step": {
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "operation", "target", "consumer", "outcome", "checks"],
"properties": {
"id": {"$ref": "#/$defs/identifier"},
"kind": {"enum": ["step", "revert"]},
"operation": {"$ref": "#/$defs/identifier"},
"target": {"$ref": "#/$defs/identifier"},
"consumer": {"$ref": "#/$defs/identifier"},
"outcome": {"enum": ["done", "check_failed", "failed", "unknown", "error"]},
"checks": {"type": "array", "items": {"$ref": "#/$defs/check_result"}},
"error_type": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,79}$"}
}
},
"decision_ref": {
"type": "object", "additionalProperties": false, "required": ["label", "ref"],
"properties": {
"label": {"enum": ["admission", "admission_digest", "approval"]},
"ref": {"$ref": "#/$defs/identifier"}
}
},
"budget_values": {
"type": "object", "additionalProperties": false, "required": ["wall_time_seconds", "attempts"],
"properties": {
"wall_time_seconds": {"type": "number", "minimum": 0},
"attempts": {"type": "integer", "minimum": 0}
}
},
"receipt_event": {
"allOf": [{"$ref": "#/$defs/envelope"}],
"type": "object",
"additionalProperties": false,
"properties": {
"id": true, "correlation_id": true, "subject": true, "source": true, "tenant": true, "occurred_at": true,
"type": {"const": "execution.attribution.receipt"},
"data": {
"type": "object",
"additionalProperties": false,
"required": ["receipt_version", "run_id", "initiator", "executing_workload", "runtime_principal", "tenant",
"environment", "source_commit", "artifact_digest", "target", "action", "decision_refs", "pdp",
"time_interval", "outcome", "reverted", "attempts", "budget", "steps"],
"properties": {
"receipt_version": {"const": "0.1"},
"run_id": {"$ref": "#/$defs/run_id"},
"initiator": {
"type": "object", "additionalProperties": false, "required": ["iss", "sub", "role"],
"properties": {
"iss": {"anyOf": [{"const": "unknown"}, {"type": "string", "pattern": "^https://[^\\s]+$"}]},
"sub": {"$ref": "#/$defs/identifier"},
"role": {"const": "approver"}
}
},
"delegation": {"$ref": "#/$defs/identifier"},
"executing_workload": {"$ref": "#/$defs/identifier"},
"runtime_principal": {"$ref": "#/$defs/identifier"},
"tenant": {"$ref": "#/$defs/identifier"},
"environment": {"$ref": "#/$defs/identifier"},
"source_commit": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"},
"artifact_digest": {"$ref": "#/$defs/digest"},
"target": {"type": "string", "pattern": "^admission:BEA-[0-9]{4}-[0-9]{4}$"},
"action": {"const": "bounded-execution"},
"decision_refs": {"type": "array", "minItems": 3, "maxItems": 3, "items": {"$ref": "#/$defs/decision_ref"}},
"pdp": {"enum": ["not-evaluated", "allow"]},
"time_interval": {
"type": "object", "additionalProperties": false, "required": ["start", "end", "clock_source"],
"properties": {
"start": {"$ref": "#/$defs/time"},
"end": {"$ref": "#/$defs/time"},
"clock_source": {"$ref": "#/$defs/identifier"},
"bound": {"type": "number", "exclusiveMinimum": 0}
}
},
"outcome": {"enum": ["completed", "reverted", "revert_failed", "stopped", "unknown", "budget"]},
"reverted": {"type": "boolean"},
"attempts": {"type": "integer", "minimum": 1},
"budget": {
"type": "object", "additionalProperties": false, "required": ["limit", "used"],
"properties": {"limit": {"$ref": "#/$defs/budget_values"}, "used": {"$ref": "#/$defs/budget_values"}}
},
"steps": {"type": "array", "items": {"$ref": "#/$defs/step"}}
}
}
}
}
}
}

View file

@ -0,0 +1,36 @@
# Execution Attribution Receipt validator (NK-WP-0056-T15)
Schema `canon/schemas/execution-attribution-receipt_v0.1.schema.json` covers the bounded-executor profile of the
Playbook Capability Contract v0.2 receipt: the two events one executor run emits to audit-core, in audit-core's ingest
envelope.
- `execution.run.started` is sent after consume, before the first step. It carries `data.window.not_after`, so
audit-core holds the run to its own admission window (AUDIT-WP-0018-T03).
- `execution.attribution.receipt` is one per run, with every step, check result, revert and the budget used.
```bash
python3 tools/execution-attribution-receipt/validate.py validate EVENT.json... # EAR-1..EAR-9 per event
python3 tools/execution-attribution-receipt/validate.py pair STARTED.json RECEIPT.json # plus EAR-10 across the pair
python3 tools/execution-attribution-receipt/generate_fixtures.py [--check] # fixtures from the real executor
make execution-attribution-receipt-test
```
The rules are listed in `validate.py`. Findings name a rule and a location, never a value. The schema closes every
object (`additionalProperties: false`), so no field can carry a value an operation handled. Steps record identifiers,
outcomes, check results and an error *type* only.
`fixtures/valid/` holds three scenarios: `completed`, `reverted` and `completed-on-retry`. They are generated by
running `executor.Run` on in-memory fakes, and a test fails when they drift from what the executor emits. audit-core
can use them for AUDIT-WP-0014-T02 (canonical receipt validation).
Profile decisions (audit-core's open points in `docs/bounded-executor-receipts.md`):
- `target` is `admission:<id>` and `action` is `bounded-execution` at receipt level. Per-step targets stay in
`steps[]`.
- `decision_refs` are exactly `admission`, `admission_digest` and `approval`, as `{label, ref}`.
- `initiator.iss` is the approvers' issuer when configured, else the explicit `unknown`.
- `delegation` is absent: the pilot has no delegated initiator.
- `tenant` is the audit-core tenant (`platform`) and must equal the envelope tenant.
- `pdp` is `not-evaluated` in the pilot.
Not covered: the railiance-infra S1 runner profile (NK-WP-0040-T02), which waits on its own emitter decision.

View file

@ -0,0 +1,229 @@
{
"correlation_id": "2d906c74-5e3f-4b80-8cb2-3d4e5f607182",
"data": {
"action": "bounded-execution",
"artifact_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"attempts": 2,
"budget": {
"limit": {
"attempts": 2,
"wall_time_seconds": 1200
},
"used": {
"attempts": 2,
"wall_time_seconds": 84.0
}
},
"decision_refs": [
{
"label": "admission",
"ref": "BEA-2026-0001"
},
{
"label": "admission_digest",
"ref": "sha256:9bf8633a4ef9d555bf9cc69c8102081332ecc386d28675b1f3beabb953da0c43"
},
{
"label": "approval",
"ref": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10"
}
],
"environment": "PRODUCTION",
"executing_workload": "service:nk-bind-executor",
"initiator": {
"iss": "https://kc.coulomb.social",
"role": "approver",
"sub": "kc-sub-0f3a"
},
"outcome": "completed",
"pdp": "not-evaluated",
"receipt_version": "0.1",
"reverted": true,
"run_id": "2d906c74-5e3f-4b80-8cb2-3d4e5f607182",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"source_commit": "15c9cdbef881",
"steps": [
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "generate",
"kind": "step",
"operation": "openbao.kv.write-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "set-directory",
"kind": "step",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "deliver",
"kind": "step",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "fail"
}
],
"consumer": "authelia",
"id": "restart",
"kind": "step",
"operation": "k8s.rollout-restart",
"outcome": "check_failed",
"target": "k8s:sso/deployment/authelia"
},
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restore-kv",
"kind": "revert",
"operation": "openbao.kv.write-previous-value-as-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restore-directory",
"kind": "revert",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "redeliver",
"kind": "revert",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restart-again",
"kind": "revert",
"operation": "k8s.rollout-restart",
"outcome": "done",
"target": "k8s:sso/deployment/authelia"
},
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "generate",
"kind": "step",
"operation": "openbao.kv.write-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "set-directory",
"kind": "step",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "deliver",
"kind": "step",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restart",
"kind": "step",
"operation": "k8s.rollout-restart",
"outcome": "done",
"target": "k8s:sso/deployment/authelia"
}
],
"target": "admission:BEA-2026-0001",
"tenant": "platform",
"time_interval": {
"clock_source": "system-utc",
"end": "2026-11-03T09:31:24Z",
"start": "2026-11-03T09:30:00Z"
}
},
"id": "run:2d906c74-5e3f-4b80-8cb2-3d4e5f607182:receipt",
"occurred_at": "2026-11-03T09:31:24+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.attribution.receipt"
}

View file

@ -0,0 +1,24 @@
{
"correlation_id": "2d906c74-5e3f-4b80-8cb2-3d4e5f607182",
"data": {
"admission": "BEA-2026-0001",
"admission_digest": "sha256:9bf8633a4ef9d555bf9cc69c8102081332ecc386d28675b1f3beabb953da0c43",
"approval": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10",
"consumed_at": "2026-11-03T09:30:00Z",
"executing_workload": "service:nk-bind-executor",
"plan_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"receipt_version": "0.1",
"run_id": "2d906c74-5e3f-4b80-8cb2-3d4e5f607182",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"steps_planned": 4,
"window": {
"not_after": "2026-11-03T11:00:00Z"
}
},
"id": "run:2d906c74-5e3f-4b80-8cb2-3d4e5f607182:started",
"occurred_at": "2026-11-03T09:30:00+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.run.started"
}

View file

@ -0,0 +1,117 @@
{
"correlation_id": "0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60",
"data": {
"action": "bounded-execution",
"artifact_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"attempts": 1,
"budget": {
"limit": {
"attempts": 1,
"wall_time_seconds": 1200
},
"used": {
"attempts": 1,
"wall_time_seconds": 28.0
}
},
"decision_refs": [
{
"label": "admission",
"ref": "BEA-2026-0001"
},
{
"label": "admission_digest",
"ref": "sha256:bbaa3afb2885cc1dd8a4b00f48f3f7265aaf05d4c2f03479fb935c44e5bf013b"
},
{
"label": "approval",
"ref": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10"
}
],
"environment": "PRODUCTION",
"executing_workload": "service:nk-bind-executor",
"initiator": {
"iss": "https://kc.coulomb.social",
"role": "approver",
"sub": "kc-sub-0f3a"
},
"outcome": "completed",
"pdp": "not-evaluated",
"receipt_version": "0.1",
"reverted": false,
"run_id": "0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"source_commit": "15c9cdbef881",
"steps": [
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "generate",
"kind": "step",
"operation": "openbao.kv.write-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "set-directory",
"kind": "step",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "deliver",
"kind": "step",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restart",
"kind": "step",
"operation": "k8s.rollout-restart",
"outcome": "done",
"target": "k8s:sso/deployment/authelia"
}
],
"target": "admission:BEA-2026-0001",
"tenant": "platform",
"time_interval": {
"clock_source": "system-utc",
"end": "2026-11-03T09:30:28Z",
"start": "2026-11-03T09:30:00Z"
}
},
"id": "run:0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60:receipt",
"occurred_at": "2026-11-03T09:30:28+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.attribution.receipt"
}

View file

@ -0,0 +1,24 @@
{
"correlation_id": "0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60",
"data": {
"admission": "BEA-2026-0001",
"admission_digest": "sha256:bbaa3afb2885cc1dd8a4b00f48f3f7265aaf05d4c2f03479fb935c44e5bf013b",
"approval": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10",
"consumed_at": "2026-11-03T09:30:00Z",
"executing_workload": "service:nk-bind-executor",
"plan_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"receipt_version": "0.1",
"run_id": "0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"steps_planned": 4,
"window": {
"not_after": "2026-11-03T11:00:00Z"
}
},
"id": "run:0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60:started",
"occurred_at": "2026-11-03T09:30:00+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.run.started"
}

View file

@ -0,0 +1,173 @@
{
"correlation_id": "1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071",
"data": {
"action": "bounded-execution",
"artifact_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"attempts": 1,
"budget": {
"limit": {
"attempts": 1,
"wall_time_seconds": 1200
},
"used": {
"attempts": 1,
"wall_time_seconds": 56.0
}
},
"decision_refs": [
{
"label": "admission",
"ref": "BEA-2026-0001"
},
{
"label": "admission_digest",
"ref": "sha256:bbaa3afb2885cc1dd8a4b00f48f3f7265aaf05d4c2f03479fb935c44e5bf013b"
},
{
"label": "approval",
"ref": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10"
}
],
"environment": "PRODUCTION",
"executing_workload": "service:nk-bind-executor",
"initiator": {
"iss": "https://kc.coulomb.social",
"role": "approver",
"sub": "kc-sub-0f3a"
},
"outcome": "reverted",
"pdp": "not-evaluated",
"receipt_version": "0.1",
"reverted": true,
"run_id": "1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"source_commit": "15c9cdbef881",
"steps": [
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "generate",
"kind": "step",
"operation": "openbao.kv.write-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "set-directory",
"kind": "step",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "deliver",
"kind": "step",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "fail"
}
],
"consumer": "authelia",
"id": "restart",
"kind": "step",
"operation": "k8s.rollout-restart",
"outcome": "check_failed",
"target": "k8s:sso/deployment/authelia"
},
{
"checks": [
{
"id": "kv-advanced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restore-kv",
"kind": "revert",
"operation": "openbao.kv.write-previous-value-as-next-version",
"outcome": "done",
"target": "openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia"
},
{
"checks": [
{
"id": "bind-signs-in",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restore-directory",
"kind": "revert",
"operation": "lldap.user.change-own-password-from-kv",
"outcome": "done",
"target": "lldap:svc-authelia"
},
{
"checks": [
{
"id": "eso-synced",
"result": "pass"
}
],
"consumer": "authelia",
"id": "redeliver",
"kind": "revert",
"operation": "eso.force-sync",
"outcome": "done",
"target": "k8s:sso/externalsecret/authelia-secrets"
},
{
"checks": [
{
"id": "synthetic-journey",
"result": "pass"
}
],
"consumer": "authelia",
"id": "restart-again",
"kind": "revert",
"operation": "k8s.rollout-restart",
"outcome": "done",
"target": "k8s:sso/deployment/authelia"
}
],
"target": "admission:BEA-2026-0001",
"tenant": "platform",
"time_interval": {
"clock_source": "system-utc",
"end": "2026-11-03T09:30:56Z",
"start": "2026-11-03T09:30:00Z"
}
},
"id": "run:1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071:receipt",
"occurred_at": "2026-11-03T09:30:56+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.attribution.receipt"
}

View file

@ -0,0 +1,24 @@
{
"correlation_id": "1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071",
"data": {
"admission": "BEA-2026-0001",
"admission_digest": "sha256:bbaa3afb2885cc1dd8a4b00f48f3f7265aaf05d4c2f03479fb935c44e5bf013b",
"approval": "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10",
"consumed_at": "2026-11-03T09:30:00Z",
"executing_workload": "service:nk-bind-executor",
"plan_digest": "sha256:df214e02c24e70c6ac799bee873862f0d24d9bd537bf77cfb845a7b880c7565b",
"receipt_version": "0.1",
"run_id": "1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071",
"runtime_principal": "system:serviceaccount:sso:nk-bind-executor",
"steps_planned": 4,
"window": {
"not_after": "2026-11-03T11:00:00Z"
}
},
"id": "run:1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071:started",
"occurred_at": "2026-11-03T09:30:00+00:00",
"source": "net-kingdom.bounded-executor",
"subject": "admission:BEA-2026-0001",
"tenant": "platform",
"type": "execution.run.started"
}

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Regenerate the valid fixtures from the real executor (bounded-executor/executor.py) on in-memory fakes.
Each scenario runs executor.Run.execute() on the pilot example admission with fixed clocks and a fixed run id, and
writes the events it emitted to fixtures/valid/<scenario>-{started,receipt}.json. The fixtures therefore always
have the executor's exact shape; tests fail when the executor and the schema drift apart. No value an operation
handles can reach them: the fake operation holds a marker value, and the generator refuses if it appears.
python3 tools/execution-attribution-receipt/generate_fixtures.py [--check]
"""
from __future__ import annotations
import argparse
import copy
import datetime as dt
import importlib.util
import json
import pathlib
import sys
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[1]
OUT = HERE / "fixtures/valid"
MARKER = "synthetic-value-never-emitted"
APPROVAL = "6f1c2b9e-0a4d-4e3b-9c51-7d2f8a6b3e10"
NOW = dt.datetime(2026, 11, 3, 9, 30, tzinfo=dt.UTC)
def _executor():
spec = importlib.util.spec_from_file_location("executor", ROOT / "bounded-executor/executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules["executor"] = module
spec.loader.exec_module(module)
return module
X = _executor()
class Fake:
def __init__(self, doc, fail_checks=()):
spec = doc["spec"]
self.t = 0.0
self.fail = list(fail_checks)
self.events = []
self.approval = {
"id": APPROVAL, "status": "approved",
"binding": {"action": X.ACTION, "actor": spec["executor"]["identity"], "principal": "admin",
"purpose": spec["purpose"], "human_control": True, "pdp_path": False,
"target": {"admission_id": spec["id"], "admission_digest": X.bea.admission_digest(doc)}},
"validity": {"not_before": spec["window"]["not_before"], "expires_at": spec["window"]["not_after"]},
"entries": [{"subject_id": "kc-sub-0f3a", "principal_type": "human", "approved_at": "2026-11-03T09:10:00Z",
"assurance": json.dumps({"level": "aal2", "mfa": True, "source": "key-cape"})}]}
def get(self, _id):
return copy.deepcopy(self.approval)
def consume(self, _id, digest, decision_id):
self.approval.update(status="consumed", consumption={"request_digest": digest, "decision_id": decision_id,
"consumed_at": "2026-11-03T09:30:00Z"})
return {"status": "consumed", "request_digest": digest, "decision_id": decision_id,
"consumed_at": "2026-11-03T09:30:00Z", "idempotent": False}
def op(self, _step, ctx):
ctx.data["value"] = MARKER
self.t += 5
def check(self, check, _ctx):
self.t += 2
if self.fail and self.fail[0] == check["id"]:
self.fail.pop(0)
return False
return True
def sleep(self, seconds):
self.t += seconds
def scenario(name: str, fail_checks=(), attempts=1) -> list[dict]:
doc = X.bea.load(ROOT / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml")
doc["spec"]["budget"]["attempts"] = attempts
doc["spec"]["plan_digest"] = X.bea.plan_digest(doc)
fake = Fake(doc, fail_checks)
plan = doc["spec"]["plan"]
deps = X.Deps(carrier=fake, operations={s["operation"]: fake.op for s in plan["steps"] + plan["revert_steps"]},
checks={c["id"]: fake.check for c in doc["spec"]["checks"]}, trust=lambda _s: True,
audit=type("Sink", (), {"emit": lambda _s, e: fake.events.append(e)})(), alert=lambda *_a: None,
wall=lambda: NOW + dt.timedelta(seconds=fake.t), monotonic=lambda: fake.t, sleep=fake.sleep)
cfg = X.Config(executor_identity=doc["spec"]["executor"]["identity"],
runtime_principal="system:serviceaccount:sso:nk-bind-executor", tenant="platform",
environment="PRODUCTION", approver_subjects={"admin": "kc-sub-0f3a"},
approver_issuer="https://kc.coulomb.social")
run = X.Run(doc, APPROVAL, cfg, deps)
run.run_id = {"completed": "0b7e4a52-3c1d-4f6e-8a90-1b2c3d4e5f60",
"reverted": "1c8f5b63-4d2e-4a7f-9ba1-2c3d4e5f6071",
"completed-on-retry": "2d906c74-5e3f-4b80-8cb2-3d4e5f607182"}[name]
run.preflight = lambda: run._check_approval(fake.get(APPROVAL), consumed_ok=False) # pins: example, not repo
result = run.execute()
assert result["outcome"] in ("completed", "reverted"), result
text = json.dumps(fake.events)
if MARKER in text:
raise SystemExit("an operation value reached an event")
return fake.events
SCENARIOS = {
"completed": {},
"reverted": {"fail_checks": ["synthetic-journey"]},
"completed-on-retry": {"fail_checks": ["synthetic-journey"], "attempts": 2},
}
def render() -> dict[str, str]:
files = {}
for name, kwargs in SCENARIOS.items():
started, receipt = scenario(name, **kwargs)
for kind, event in (("started", started), ("receipt", receipt)):
files[f"{name}-{kind}.json"] = json.dumps(event, indent=2, sort_keys=True) + "\n"
return files
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("--check", action="store_true", help="fail if the committed fixtures differ")
args = parser.parse_args(argv)
files = render()
if args.check:
stale = [n for n, text in files.items() if not (OUT / n).is_file() or (OUT / n).read_text() != text]
print(json.dumps({"stale": stale}))
return 1 if stale else 0
OUT.mkdir(parents=True, exist_ok=True)
for name, text in files.items():
(OUT / name).write_text(text)
print(f"wrote {len(files)} fixtures to {OUT.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,132 @@
"""Execution Attribution Receipt validator (NK-WP-0056-T15). Fixtures come from the real executor."""
from __future__ import annotations
import copy
import importlib.util
import json
import pathlib
import sys
import unittest
HERE = pathlib.Path(__file__).resolve().parent
TOOL = HERE.parent
def load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
V = load("ear_validate", TOOL / "validate.py")
G = load("ear_generate", TOOL / "generate_fixtures.py")
VALID = TOOL / "fixtures/valid"
SCENARIOS = ("completed", "reverted", "completed-on-retry")
def fixture(name):
return json.loads((VALID / name).read_text())
class Fixtures(unittest.TestCase):
def test_committed_fixtures_are_what_the_executor_emits(self):
self.assertEqual(G.main(["--check"]), 0)
def test_every_valid_fixture_and_pair_passes(self):
for s in SCENARIOS:
with self.subTest(scenario=s):
started, receipt = fixture(f"{s}-started.json"), fixture(f"{s}-receipt.json")
for event in (started, receipt):
self.assertEqual(V.evaluate(event)["findings"], [])
self.assertEqual(V.evaluate_pair(started, receipt), {"valid": True, "findings": []})
def test_started_carries_the_window_audit_core_asked_for(self):
data = fixture("completed-started.json")["data"]
self.assertEqual(data["window"], {"not_after": "2026-11-03T11:00:00Z"})
class Rules(unittest.TestCase):
def assert_rule(self, event, rule, at=None):
report = V.evaluate(event)
self.assertFalse(report["valid"])
self.assertIn(rule, {f["rule"] for f in report["findings"]}, report)
if at:
self.assertIn(at, {f["at"] for f in report["findings"]})
def receipt(self, name="completed"):
return copy.deepcopy(fixture(f"{name}-receipt.json"))
def test_schema_refusals(self):
cases = {
"extra field (a value could hide there)": lambda e: e["data"].update(bind_password="x"),
"extra step field": lambda e: e["data"]["steps"][0].update(value="x"),
"wrong source": lambda e: e.update(source="someone-else"),
"naive time": lambda e: e.update(occurred_at="2026-11-03T09:31:00"),
"unknown outcome": lambda e: e["data"].update(outcome="fine"),
"issuer not https": lambda e: e["data"]["initiator"].update(iss="http://kc.local"),
"error message instead of type": lambda e: e["data"]["steps"][0].update(error_type="bind failed: x y"),
"missing receipt_version": lambda e: e["data"].pop("receipt_version"),
}
for name, mutate in cases.items():
with self.subTest(case=name):
event = self.receipt()
mutate(event)
self.assert_rule(event, "EAR-1")
def test_cross_field_rules(self):
cases = {
"EAR-2": lambda e: e.update(id="run:" + "0" * 8 + "-0000-4000-8000-" + "0" * 12 + ":receipt"),
"EAR-3": lambda e: e.update(subject="admission:BEA-2026-0002"),
"EAR-4": lambda e: e.update(tenant="tenant:platform"),
"EAR-5": lambda e: e["data"]["decision_refs"][0].update(ref="BEA-2026-0002"),
"EAR-6": lambda e: e.update(occurred_at="2026-11-03T09:00:00Z"),
"EAR-7": lambda e: e["data"].update(reverted=True),
"EAR-8": lambda e: e["data"]["budget"]["used"].update(attempts=2),
"EAR-9": lambda e: e["data"]["steps"][-1].update(outcome="check_failed"),
}
for rule, mutate in cases.items():
with self.subTest(rule=rule):
event = self.receipt()
mutate(event)
self.assert_rule(event, rule)
def test_reverted_needs_a_clean_revert(self):
event = self.receipt("reverted")
next(s for s in event["data"]["steps"] if s["kind"] == "revert").update(outcome="check_failed")
self.assert_rule(event, "EAR-9")
event = self.receipt("completed")
event["data"]["outcome"] = "reverted"
self.assert_rule(event, "EAR-9")
def test_completed_after_revert_needs_a_retry(self):
event = self.receipt("completed-on-retry")
event["data"]["attempts"] = event["data"]["budget"]["used"]["attempts"] = 1
self.assert_rule(event, "EAR-9")
def test_pair_rules(self):
started, receipt = fixture("completed-started.json"), fixture("completed-receipt.json")
cases = {
"window": lambda s, r: s["data"]["window"].update(not_after="2026-11-03T09:30:10Z"),
"approval": lambda s, r: s["data"].update(approval="another-approval"),
"admission_digest": lambda s, r: s["data"].update(admission_digest="sha256:" + "0" * 64),
"order": lambda s, r: s.update(occurred_at="2026-11-03T10:59:00Z"),
}
for at, mutate in cases.items():
with self.subTest(at=at):
s, r = copy.deepcopy(started), copy.deepcopy(receipt)
mutate(s, r)
report = V.evaluate_pair(s, r)
self.assertIn({"rule": "EAR-10", "at": at}, report["findings"])
report = V.evaluate_pair(receipt, started)
self.assertFalse(report["valid"])
def test_findings_never_carry_values(self):
event = self.receipt()
event["data"]["bind_password"] = "synthetic-secret-123"
self.assertNotIn("synthetic-secret-123", json.dumps(V.evaluate(event)))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Execution Attribution Receipt validator, bounded-executor profile (NK-WP-0056-T15, NK-WP-0040-T02 scope).
Offline and deterministic. Checks the two audit-core events of one run against
canon/schemas/execution-attribution-receipt_v0.1.schema.json (EAR-1), then the cross-field rules:
EAR-2 envelope id is run:<correlation_id>:started|receipt, and data.run_id equals correlation_id
EAR-3 subject names the run's admission (started: data.admission; receipt: data.target)
EAR-4 receipt: envelope tenant equals data.tenant
EAR-5 receipt: decision_refs are admission, admission_digest, approval, once each; admission matches the subject
EAR-6 receipt: time_interval.start <= end, and occurred_at equals end
EAR-7 receipt: reverted is true exactly when a revert step was recorded
EAR-8 receipt: attempts equals budget.used.attempts and does not exceed budget.limit.attempts
EAR-9 receipt: outcome agrees with the steps (completed: the final attempt, after the last revert, has every
step done and every check passed, and a revert implies a retry; reverted: a revert ran and every revert
step is done)
EAR-10 pair: started and receipt share run_id, tenant, admission and approval, the started admission_digest is the
receipt's, started precedes the receipt, and the run ended inside the admission window
Findings name a rule and a location, never a value.
python3 tools/execution-attribution-receipt/validate.py validate EVENT.json...
python3 tools/execution-attribution-receipt/validate.py pair STARTED.json RECEIPT.json
"""
from __future__ import annotations
import datetime as dt
import json
import pathlib
import sys
from typing import Any
import jsonschema
ROOT = pathlib.Path(__file__).resolve().parents[2]
SCHEMA = ROOT / "canon/schemas/execution-attribution-receipt_v0.1.schema.json"
STARTED = "execution.run.started"
RECEIPT = "execution.attribution.receipt"
def parse_time(value: str) -> dt.datetime:
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
def _schema() -> dict:
return json.loads(SCHEMA.read_text(encoding="utf-8"))
def evaluate(event: Any, schema: dict | None = None) -> dict:
"""One event. Returns {"valid": bool, "kind": started|receipt|None, "findings": [{"rule", "at"}]}."""
schema = schema or _schema()
findings: list[dict] = []
def fail(rule: str, at: str) -> None:
findings.append({"rule": rule, "at": at})
validator = jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker())
errors = sorted(validator.iter_errors(event), key=lambda e: list(e.absolute_path))
if errors:
for error in errors[:20]:
fail("EAR-1", "/".join(str(p) for p in error.absolute_path) or "(root)")
return {"valid": False, "kind": None, "findings": findings}
kind = "started" if event["type"] == STARTED else "receipt"
data = event["data"]
if event["id"] != f"run:{event['correlation_id']}:{kind}" or data["run_id"] != event["correlation_id"]:
fail("EAR-2", "id")
admission = data["admission"] if kind == "started" else data["target"].removeprefix("admission:")
if event["subject"] != f"admission:{admission}":
fail("EAR-3", "subject")
if kind == "receipt":
if event["tenant"] != data["tenant"]:
fail("EAR-4", "tenant")
refs = {r["label"]: r["ref"] for r in data["decision_refs"]}
if sorted(r["label"] for r in data["decision_refs"]) != ["admission", "admission_digest", "approval"] \
or refs.get("admission") != admission or not refs.get("admission_digest", "").startswith("sha256:"):
fail("EAR-5", "data/decision_refs")
start, end = (parse_time(data["time_interval"][k]) for k in ("start", "end"))
if start > end or parse_time(event["occurred_at"]) != end:
fail("EAR-6", "data/time_interval")
reverts = [s for s in data["steps"] if s["kind"] == "revert"]
if data["reverted"] != bool(reverts):
fail("EAR-7", "data/reverted")
budget = data["budget"]
if data["attempts"] != budget["used"]["attempts"] or data["attempts"] > budget["limit"]["attempts"]:
fail("EAR-8", "data/budget")
if data["outcome"] == "completed":
# A clean revert may be retried (BEA budget.attempts): earlier attempts may hold failures and reverts.
# The final attempt is the steps after the last revert; it must be complete and clean.
last = max((i for i, s in enumerate(data["steps"]) if s["kind"] == "revert"), default=-1)
final = data["steps"][last + 1:]
if (not final or (reverts and data["attempts"] < 2)
or any(s["outcome"] != "done" or any(c["result"] != "pass" for c in s["checks"]) for s in final)):
fail("EAR-9", "data/outcome")
if data["outcome"] == "reverted" and (not reverts or any(s["outcome"] != "done" for s in reverts)):
fail("EAR-9", "data/outcome")
return {"valid": not findings, "kind": kind, "findings": findings}
def evaluate_pair(started: Any, receipt: Any, schema: dict | None = None) -> dict:
schema = schema or _schema()
findings = []
for name, event, want in (("started", started, "started"), ("receipt", receipt, "receipt")):
report = evaluate(event, schema)
findings += [{"rule": f["rule"], "at": f"{name}:{f['at']}"} for f in report["findings"]]
if report["valid"] and report["kind"] != want:
findings.append({"rule": "EAR-10", "at": f"{name}:type"})
if findings:
return {"valid": False, "findings": findings}
s, r = started["data"], receipt["data"]
refs = {x["label"]: x["ref"] for x in r["decision_refs"]}
checks = {
"run_id": s["run_id"] == r["run_id"],
"tenant": started["tenant"] == receipt["tenant"],
"admission": started["subject"] == receipt["subject"],
"admission_digest": s["admission_digest"] == refs["admission_digest"],
"approval": s["approval"] == refs["approval"],
"order": parse_time(started["occurred_at"]) <= parse_time(receipt["occurred_at"]),
"window": parse_time(r["time_interval"]["end"]) <= parse_time(s["window"]["not_after"]),
}
findings = [{"rule": "EAR-10", "at": k} for k, ok in checks.items() if not ok]
return {"valid": not findings, "findings": findings}
def _load(path: str) -> Any:
return json.loads(pathlib.Path(path).read_text(encoding="utf-8"))
def main(argv: list[str] | None = None) -> int:
argv = sys.argv[1:] if argv is None else argv
if len(argv) >= 2 and argv[0] == "validate":
ok = True
for path in argv[1:]:
report = evaluate(_load(path))
print(json.dumps({"file": path, **report}, sort_keys=True))
ok &= report["valid"]
return 0 if ok else 1
if len(argv) == 3 and argv[0] == "pair":
report = evaluate_pair(_load(argv[1]), _load(argv[2]))
print(json.dumps(report, sort_keys=True))
return 0 if report["valid"] else 1
print(__doc__.split("\n\n")[-1], file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())