#!/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::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())