From e102a86a37c1a3c9f88f57eb49af4ea31bb68ae3 Mon Sep 17 00:00:00 2001 From: Proof Pack Bot Date: Thu, 14 May 2026 03:43:42 +0000 Subject: [PATCH] docs/demo: add proof pack v0.1 for DecisionRecord authority-before-mutation --- demo/expired_authority_refusal.json | 14 + demo/replayed_nonce_refusal.json | 14 + demo/scope_mismatch_refusal.json | 14 + demo/valid_decision_record.json | 14 + docs/PROOF_PACK_v0.1.md | 94 ++++++ receipts/examples/allow_receipt.json | 37 +++ receipts/examples/deny_expired_receipt.json | 37 +++ receipts/examples/deny_replay_receipt.json | 37 +++ receipts/examples/deny_scope_receipt.json | 37 +++ scripts/run_proof_pack.py | 310 ++++++++++++++++++++ scripts/verify_receipt.py | 168 +++++++++++ 11 files changed, 776 insertions(+) create mode 100644 demo/expired_authority_refusal.json create mode 100644 demo/replayed_nonce_refusal.json create mode 100644 demo/scope_mismatch_refusal.json create mode 100644 demo/valid_decision_record.json create mode 100644 docs/PROOF_PACK_v0.1.md create mode 100644 receipts/examples/allow_receipt.json create mode 100644 receipts/examples/deny_expired_receipt.json create mode 100644 receipts/examples/deny_replay_receipt.json create mode 100644 receipts/examples/deny_scope_receipt.json create mode 100644 scripts/run_proof_pack.py create mode 100644 scripts/verify_receipt.py diff --git a/demo/expired_authority_refusal.json b/demo/expired_authority_refusal.json new file mode 100644 index 0000000..d547d3b --- /dev/null +++ b/demo/expired_authority_refusal.json @@ -0,0 +1,14 @@ +{ + "decision_id": "dr_expired_001", + "actor_id": "agent_17", + "action": "approve_invoice", + "object_id": "invoice_778", + "environment": "prod", + "commit_hash": "sha256:abc123", + "verdict": "ALLOW", + "policy_version": "2026-04-27.1", + "issued_at": "2026-04-27T04:00:00Z", + "expires_at": "2026-04-27T04:05:00Z", + "nonce": "nonce_expired_001", + "signature": "sig_valid" +} diff --git a/demo/replayed_nonce_refusal.json b/demo/replayed_nonce_refusal.json new file mode 100644 index 0000000..9fb28a1 --- /dev/null +++ b/demo/replayed_nonce_refusal.json @@ -0,0 +1,14 @@ +{ + "decision_id": "dr_replay_001", + "actor_id": "agent_17", + "action": "approve_invoice", + "object_id": "invoice_778", + "environment": "prod", + "commit_hash": "sha256:abc123", + "verdict": "ALLOW", + "policy_version": "2026-04-27.1", + "issued_at": "2026-04-27T05:00:00Z", + "expires_at": "2026-04-27T05:05:00Z", + "nonce": "nonce_already_used_001", + "signature": "sig_valid" +} diff --git a/demo/scope_mismatch_refusal.json b/demo/scope_mismatch_refusal.json new file mode 100644 index 0000000..259cb43 --- /dev/null +++ b/demo/scope_mismatch_refusal.json @@ -0,0 +1,14 @@ +{ + "decision_id": "dr_scope_001", + "actor_id": "agent_17", + "action": "approve_invoice", + "object_id": "invoice_OTHER", + "environment": "prod", + "commit_hash": "sha256:abc123", + "verdict": "ALLOW", + "policy_version": "2026-04-27.1", + "issued_at": "2026-04-27T05:00:00Z", + "expires_at": "2026-04-27T05:05:00Z", + "nonce": "nonce_scope_001", + "signature": "sig_valid" +} diff --git a/demo/valid_decision_record.json b/demo/valid_decision_record.json new file mode 100644 index 0000000..aa6677e --- /dev/null +++ b/demo/valid_decision_record.json @@ -0,0 +1,14 @@ +{ + "decision_id": "dr_valid_001", + "actor_id": "agent_17", + "action": "approve_invoice", + "object_id": "invoice_778", + "environment": "prod", + "commit_hash": "sha256:abc123", + "verdict": "ALLOW", + "policy_version": "2026-04-27.1", + "issued_at": "2026-04-27T05:00:00Z", + "expires_at": "2026-04-27T05:05:00Z", + "nonce": "nonce_valid_001", + "signature": "sig_valid" +} diff --git a/docs/PROOF_PACK_v0.1.md b/docs/PROOF_PACK_v0.1.md new file mode 100644 index 0000000..c332dac --- /dev/null +++ b/docs/PROOF_PACK_v0.1.md @@ -0,0 +1,94 @@ +# Proof Pack v0.1 — Authority-Before-Mutation + +Bounded public proof that, on the demonstrated CommitGate path, state +mutation is refused unless the attached `DecisionRecord` is valid, +scoped, unexpired, signed, and unreplayed — and that every refusal +produces an inspectable receipt. + +## How to run in 60 seconds + +```bash +git clone https://github.com/LalaSkye/commit-gate-core.git +cd commit-gate-core +python3 scripts/run_proof_pack.py +python3 scripts/verify_receipt.py +``` + +No install step. Stdlib only. The runner exercises four cases through +the existing kernel at `src/commit_gate_core/gate.py`: + +| Case | Demo fixture | Expected | Receipt | +| --- | --- | --- | --- | +| valid DecisionRecord | `demo/valid_decision_record.json` | ALLOW | `receipts/examples/allow_receipt.json` | +| expired authority | `demo/expired_authority_refusal.json` | DENY_EXPIRED | `receipts/examples/deny_expired_receipt.json` | +| scope mismatch | `demo/scope_mismatch_refusal.json` | DENY_SCOPE | `receipts/examples/deny_scope_receipt.json` | +| replayed nonce | `demo/replayed_nonce_refusal.json` | DENY_REPLAY | `receipts/examples/deny_replay_receipt.json` | + +## Expected output + +`scripts/run_proof_pack.py` prints, for each case: case name, expected +result, actual result, receipt path, receipt hash, mutation occurred +(`YES` / `NO`). The run ends with: + +```text +All four cases pass: YES +``` + +`scripts/verify_receipt.py` then checks each receipt in +`receipts/examples/` against five gates: + +1. `receipt_hash_integrity` — sha256 over the receipt minus + `receipt_hash` matches the stored value +2. `input_hash` — the hash of the input DecisionRecord is present and + well-formed +3. `decision_result` — `actual_result` matches `expected_result` +4. `refusal_reason` — present and non-empty on DENY receipts, `null` + on ALLOW receipts +5. `no_execution_marker` — `no_execution_marker` is the inverse of + `mutation_occurred`; DENY receipts must show `mutation_occurred=false` + +A clean run ends with: + +```text +All receipts verified: YES +``` + +## What this proves + +On the demonstrated CommitGate path: + +- A signed, scoped, unexpired, unreplayed `DecisionRecord` is a hard + precondition for the mutation callback to run. +- Each of the four failure modes — `DECISION_EXPIRED`, + `SCOPE_MISMATCH:object_id`, `NONCE_REPLAYED`, and the ALLOW happy + path — flows through the kernel in `src/commit_gate_core/gate.py` + and produces a distinct, content-addressed receipt with an explicit + no-execution marker. +- Refusal receipts can be inspected independently by + `scripts/verify_receipt.py` without re-running the gate. + +The receipts and DecisionRecord fixtures live in version control, so +the evidence object is reproducible byte-for-byte. + +## What this does not prove + +- Production readiness, certification, or compliance. +- Adoption, deployment, or coverage outside this repository. +- Universal runtime governance, path-universal enforcement, or + non-bypassability outside the demonstrated path. +- Real cryptographic signature verification — the bundled + `AcceptingSignatureVerifier` is synthetic and treats + `signature == "sig_valid"` as signed for the purpose of the bounded + surface. +- Real persistent nonce ledgers, atomic commit across systems, or + downstream side-effect prevention beyond the in-process callback. + +## Claim boundary + +This proof pack demonstrates that, on the shown path, state mutation +is refused unless a DecisionRecord is valid, scoped, unexpired, signed, +and unreplayed; each refusal produces an inspectable receipt. + +This is not production infrastructure, certification, adoption +evidence, or universal runtime governance. It is a bounded proof +surface for authority-before-mutation. diff --git a/receipts/examples/allow_receipt.json b/receipts/examples/allow_receipt.json new file mode 100644 index 0000000..ee464ae --- /dev/null +++ b/receipts/examples/allow_receipt.json @@ -0,0 +1,37 @@ +{ + "actual_result": "ALLOW", + "case_name": "valid_decision_record", + "claim_boundary": "bounded proof surface for authority-before-mutation on the demonstrated CommitGate path; not production, not certification, not universal runtime governance", + "decision_id": "dr_valid_001", + "expected_result": "ALLOW", + "gate_audit_event": { + "allowed": true, + "attempted": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778" + }, + "code": "ALLOW", + "decision_id": "dr_valid_001", + "event_type": "GATE_EVALUATION", + "record_scope": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778", + "policy_version": "2026-04-27.1" + }, + "timestamp": "2026-04-27T05:01:00Z" + }, + "input_hash": "sha256:d2d4ced3301d5710dbc556a3288b4291a385667cdb2c1242f37f21617d5d30d8", + "mutation_occurred": true, + "no_execution_marker": false, + "receipt_hash": "sha256:62bcaa6694c5b622c96005567d9f498e6307347caeb11533c88d71be20011a01", + "receipt_id": "RCP-PP-valid_decision_record", + "refusal_reason": null, + "schema_version": "proof-pack-v0.1", + "timestamp": "2026-04-27T05:01:00Z" +} diff --git a/receipts/examples/deny_expired_receipt.json b/receipts/examples/deny_expired_receipt.json new file mode 100644 index 0000000..1aa5153 --- /dev/null +++ b/receipts/examples/deny_expired_receipt.json @@ -0,0 +1,37 @@ +{ + "actual_result": "DENY", + "case_name": "expired_authority", + "claim_boundary": "bounded proof surface for authority-before-mutation on the demonstrated CommitGate path; not production, not certification, not universal runtime governance", + "decision_id": "dr_expired_001", + "expected_result": "DENY_EXPIRED", + "gate_audit_event": { + "allowed": false, + "attempted": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778" + }, + "code": "DENY:DECISION_EXPIRED", + "decision_id": "dr_expired_001", + "event_type": "GATE_EVALUATION", + "record_scope": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778", + "policy_version": "2026-04-27.1" + }, + "timestamp": "2026-04-27T05:01:00Z" + }, + "input_hash": "sha256:d7cbe91680199adbee2f80c3a8a1f444c4f0062f1ac602d31f74ef3d78fcec1c", + "mutation_occurred": false, + "no_execution_marker": true, + "receipt_hash": "sha256:d2aeafaeb1c19747b26535226fdff51a655b9cec1e651f452ad5569e53b922b8", + "receipt_id": "RCP-PP-expired_authority", + "refusal_reason": "DENY:DECISION_EXPIRED", + "schema_version": "proof-pack-v0.1", + "timestamp": "2026-04-27T05:01:00Z" +} diff --git a/receipts/examples/deny_replay_receipt.json b/receipts/examples/deny_replay_receipt.json new file mode 100644 index 0000000..eca5382 --- /dev/null +++ b/receipts/examples/deny_replay_receipt.json @@ -0,0 +1,37 @@ +{ + "actual_result": "DENY", + "case_name": "replayed_nonce", + "claim_boundary": "bounded proof surface for authority-before-mutation on the demonstrated CommitGate path; not production, not certification, not universal runtime governance", + "decision_id": "dr_replay_001", + "expected_result": "DENY_REPLAY", + "gate_audit_event": { + "allowed": false, + "attempted": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778" + }, + "code": "DENY:NONCE_REPLAYED", + "decision_id": "dr_replay_001", + "event_type": "GATE_EVALUATION", + "record_scope": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778", + "policy_version": "2026-04-27.1" + }, + "timestamp": "2026-04-27T05:01:00Z" + }, + "input_hash": "sha256:058e73ce7516134fe86b8a33a9307ca3d290ecfc9175828a071c29c065e909c5", + "mutation_occurred": false, + "no_execution_marker": true, + "receipt_hash": "sha256:026f1d52a19772a295e421e671763100afb9d65dffb2ecac02cd6f583fcf8703", + "receipt_id": "RCP-PP-replayed_nonce", + "refusal_reason": "DENY:NONCE_REPLAYED", + "schema_version": "proof-pack-v0.1", + "timestamp": "2026-04-27T05:01:00Z" +} diff --git a/receipts/examples/deny_scope_receipt.json b/receipts/examples/deny_scope_receipt.json new file mode 100644 index 0000000..53ad9f7 --- /dev/null +++ b/receipts/examples/deny_scope_receipt.json @@ -0,0 +1,37 @@ +{ + "actual_result": "DENY", + "case_name": "scope_mismatch", + "claim_boundary": "bounded proof surface for authority-before-mutation on the demonstrated CommitGate path; not production, not certification, not universal runtime governance", + "decision_id": "dr_scope_001", + "expected_result": "DENY_SCOPE", + "gate_audit_event": { + "allowed": false, + "attempted": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_778" + }, + "code": "DENY:SCOPE_MISMATCH:object_id", + "decision_id": "dr_scope_001", + "event_type": "GATE_EVALUATION", + "record_scope": { + "action": "approve_invoice", + "actor_id": "agent_17", + "commit_hash": "sha256:abc123", + "environment": "prod", + "object_id": "invoice_OTHER", + "policy_version": "2026-04-27.1" + }, + "timestamp": "2026-04-27T05:01:00Z" + }, + "input_hash": "sha256:5119a21c49e3c1d631689ff174241af42b5439f3f266ef7cc5302d02359fd346", + "mutation_occurred": false, + "no_execution_marker": true, + "receipt_hash": "sha256:7a2404f44917d8af894613a1208510ce98970657d90b2a64ff43d292945bd6ab", + "receipt_id": "RCP-PP-scope_mismatch", + "refusal_reason": "DENY:SCOPE_MISMATCH:object_id", + "schema_version": "proof-pack-v0.1", + "timestamp": "2026-04-27T05:01:00Z" +} diff --git a/scripts/run_proof_pack.py b/scripts/run_proof_pack.py new file mode 100644 index 0000000..cc9a182 --- /dev/null +++ b/scripts/run_proof_pack.py @@ -0,0 +1,310 @@ +"""Proof Pack v0.1 runner. + +Runs four authority-before-mutation cases through the existing CommitGate +kernel in src/commit_gate_core/gate.py: + + 1. valid DecisionRecord -> ALLOW, mutation runs once + 2. expired authority -> DENY:DECISION_EXPIRED, no mutation + 3. scope mismatch -> DENY:SCOPE_MISMATCH:object_id, no mutation + 4. replayed nonce -> DENY:NONCE_REPLAYED, no mutation + +For each case, writes a receipt JSON to receipts/ and prints: + case name, expected result, actual result, receipt path, + receipt hash, mutation occurred (YES/NO). + +Stdlib only. No install step. + +Claim boundary: + This is a bounded proof surface for authority-before-mutation on the + demonstrated CommitGate path. Not production infrastructure, not + certification, not adoption evidence, not universal runtime governance. +""" +from __future__ import annotations + +import hashlib +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Mapping, Optional + + +_REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_REPO_ROOT / "src")) + +from commit_gate_core.gate import CommitGate, GateResult # noqa: E402 + + +DEMO_DIR = _REPO_ROOT / "demo" +RECEIPTS_DIR = _REPO_ROOT / "receipts" / "examples" + + +def stable_hash(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return "sha256:" + hashlib.sha256(encoded).hexdigest() + + +class FixedClock: + """Deterministic clock pinned just after the valid record's issued_at.""" + + def __init__(self, now: datetime) -> None: + self._now = now + + def now(self) -> datetime: + return self._now + + +class AcceptingSignatureVerifier: + """Treats any record with signature == 'sig_valid' as signed. + + Synthetic: the bounded proof does not exercise real crypto. + """ + + def verify(self, record: Mapping[str, Any]) -> bool: + return record.get("signature") == "sig_valid" + + +class InMemoryNonceLedger: + def __init__(self, preloaded: Optional[set[str]] = None) -> None: + self.used: set[str] = set(preloaded or set()) + + def contains(self, nonce: str) -> bool: + return nonce in self.used + + def consume(self, nonce: str, decision_id: str) -> None: + if nonce in self.used: + raise RuntimeError("nonce already consumed") + self.used.add(nonce) + + def rollback(self, nonce: str, decision_id: str) -> None: + self.used.discard(nonce) + + +class RecordingAuditSink: + def __init__(self) -> None: + self.events: list[dict[str, Any]] = [] + + def append(self, event: Mapping[str, Any]) -> None: + self.events.append(dict(event)) + + +class MutationCounter: + def __init__(self) -> None: + self.calls: list[Mapping[str, Any]] = [] + + def __call__(self, record: Mapping[str, Any]) -> None: + self.calls.append(dict(record)) + + +def load_record(name: str) -> dict[str, Any]: + return json.loads((DEMO_DIR / name).read_text(encoding="utf-8")) + + +def build_receipt( + *, + case_name: str, + expected_result: str, + record: Mapping[str, Any], + gate_result: GateResult, + audit_event: Mapping[str, Any], + mutation_occurred: bool, +) -> dict[str, Any]: + """Build an inspectable receipt for one case. + + Fields: + receipt_id, case_name, expected_result, actual_result, + input_hash, mutation_occurred, no_execution_marker, + refusal_reason, gate_audit_event, schema_version, receipt_hash + """ + actual_result = "ALLOW" if gate_result.allowed else "DENY" + refusal_reason = None if gate_result.allowed else gate_result.code + + receipt: dict[str, Any] = { + "schema_version": "proof-pack-v0.1", + "receipt_id": f"RCP-PP-{case_name}", + "case_name": case_name, + "expected_result": expected_result, + "actual_result": actual_result, + "decision_id": gate_result.decision_id, + "timestamp": gate_result.timestamp, + "input_hash": stable_hash(dict(record)), + "mutation_occurred": mutation_occurred, + "no_execution_marker": (not mutation_occurred), + "refusal_reason": refusal_reason, + "gate_audit_event": dict(audit_event), + "claim_boundary": ( + "bounded proof surface for authority-before-mutation on the " + "demonstrated CommitGate path; not production, not certification, " + "not universal runtime governance" + ), + } + receipt["receipt_hash"] = stable_hash( + {k: v for k, v in receipt.items() if k != "receipt_hash"} + ) + return receipt + + +def write_receipt(receipt: Mapping[str, Any], filename: str) -> Path: + RECEIPTS_DIR.mkdir(parents=True, exist_ok=True) + path = RECEIPTS_DIR / filename + path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return path + + +def make_gate( + *, + preloaded_nonces: Optional[set[str]] = None, + now: datetime, +) -> tuple[CommitGate, RecordingAuditSink, MutationCounter]: + audit = RecordingAuditSink() + mutation = MutationCounter() + gate = CommitGate( + verifier=AcceptingSignatureVerifier(), + nonce_ledger=InMemoryNonceLedger(preloaded=preloaded_nonces), + audit=audit, + mutation_callback=mutation, + accepted_policy_versions=("2026-04-27.1",), + clock=FixedClock(now), + ) + return gate, audit, mutation + + +def run_case( + *, + case_name: str, + record_file: str, + expected_result: str, + receipt_filename: str, + preloaded_nonces: Optional[set[str]] = None, + now: datetime, + scope_override: Optional[Mapping[str, str]] = None, +) -> dict[str, Any]: + record = load_record(record_file) + gate, audit, mutation = make_gate(preloaded_nonces=preloaded_nonces, now=now) + + # Caller's claimed scope: always the canonical scope. The record may diverge + # (scope_override is unused — we keep caller scope fixed and let the record + # disagree to drive scope_mismatch). + scope = { + "actor_id": "agent_17", + "action": "approve_invoice", + "object_id": "invoice_778", + "environment": "prod", + "commit_hash": "sha256:abc123", + } + if scope_override is not None: + scope.update(scope_override) + + gate_result = gate.execute(record=record, **scope) + audit_event = audit.events[-1] if audit.events else {} + mutation_occurred = len(mutation.calls) > 0 + + receipt = build_receipt( + case_name=case_name, + expected_result=expected_result, + record=record, + gate_result=gate_result, + audit_event=audit_event, + mutation_occurred=mutation_occurred, + ) + receipt_path = write_receipt(receipt, receipt_filename) + + return { + "case_name": case_name, + "expected_result": expected_result, + "actual_result": receipt["actual_result"], + "receipt_path": str(receipt_path.relative_to(_REPO_ROOT)), + "receipt_hash": receipt["receipt_hash"], + "mutation_occurred": mutation_occurred, + "gate_code": gate_result.code, + } + + +def main() -> int: + now = datetime(2026, 4, 27, 5, 1, tzinfo=timezone.utc) + + cases = [] + + cases.append( + run_case( + case_name="valid_decision_record", + record_file="valid_decision_record.json", + expected_result="ALLOW", + receipt_filename="allow_receipt.json", + preloaded_nonces=None, + now=now, + ) + ) + + cases.append( + run_case( + case_name="expired_authority", + record_file="expired_authority_refusal.json", + expected_result="DENY_EXPIRED", + receipt_filename="deny_expired_receipt.json", + preloaded_nonces=None, + now=now, + ) + ) + + cases.append( + run_case( + case_name="scope_mismatch", + record_file="scope_mismatch_refusal.json", + expected_result="DENY_SCOPE", + receipt_filename="deny_scope_receipt.json", + preloaded_nonces=None, + now=now, + ) + ) + + cases.append( + run_case( + case_name="replayed_nonce", + record_file="replayed_nonce_refusal.json", + expected_result="DENY_REPLAY", + receipt_filename="deny_replay_receipt.json", + preloaded_nonces={"nonce_already_used_001"}, + now=now, + ) + ) + + expected_codes = { + "valid_decision_record": "ALLOW", + "expired_authority": "DENY:DECISION_EXPIRED", + "scope_mismatch": "DENY:SCOPE_MISMATCH:object_id", + "replayed_nonce": "DENY:NONCE_REPLAYED", + } + + all_pass = True + print("=" * 72) + print("Proof Pack v0.1 — authority-before-mutation on CommitGate path") + print("=" * 72) + for case in cases: + expected_code = expected_codes[case["case_name"]] + code_ok = case["gate_code"] == expected_code + if case["case_name"] == "valid_decision_record": + mutation_ok = case["mutation_occurred"] is True + else: + mutation_ok = case["mutation_occurred"] is False + case_pass = code_ok and mutation_ok + all_pass = all_pass and case_pass + + print() + print(f"Case: {case['case_name']}") + print(f"Expected result: {case['expected_result']}") + print(f"Actual result: {case['actual_result']} ({case['gate_code']})") + print(f"Receipt path: {case['receipt_path']}") + print(f"Receipt hash: {case['receipt_hash']}") + print(f"Mutation occurred: {'YES' if case['mutation_occurred'] else 'NO'}") + print(f"Case pass: {'YES' if case_pass else 'NO'}") + + print() + print("=" * 72) + print(f"All four cases pass: {'YES' if all_pass else 'NO'}") + print("=" * 72) + return 0 if all_pass else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_receipt.py b/scripts/verify_receipt.py new file mode 100644 index 0000000..63332c3 --- /dev/null +++ b/scripts/verify_receipt.py @@ -0,0 +1,168 @@ +"""Proof Pack v0.1 receipt verifier. + +Checks for every receipt file passed on the command line: + + 1. receipt hash integrity (recompute sha256 over all fields except + receipt_hash and compare) + 2. input hash (present, well-formed sha256:) + 3. decision result (actual_result matches expected_result, with + DENY_* expected mapping to actual DENY) + 4. refusal reason (present and non-empty for DENY; null for ALLOW) + 5. no-execution marker (mutation_occurred is the inverse of + no_execution_marker; for DENY cases, + no_execution_marker must be True and + mutation_occurred must be False) + +Exit status: + 0 — all receipts verified + 1 — at least one check failed + +If no paths are given, verifies every JSON file in receipts/examples/. +""" +from __future__ import annotations + +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any, Mapping + + +_REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_DIR = _REPO_ROOT / "receipts" / "examples" + +SHA256_HEX = re.compile(r"^sha256:[0-9a-f]{64}$") + + +def stable_hash(value: Any) -> str: + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return "sha256:" + hashlib.sha256(encoded).hexdigest() + + +def _check_hash_integrity(receipt: Mapping[str, Any]) -> tuple[bool, str]: + stored = receipt.get("receipt_hash") + if not isinstance(stored, str) or not SHA256_HEX.match(stored): + return False, "receipt_hash missing or malformed" + recomputed = stable_hash({k: v for k, v in receipt.items() if k != "receipt_hash"}) + if recomputed != stored: + return False, f"receipt_hash mismatch: stored={stored} recomputed={recomputed}" + return True, "ok" + + +def _check_input_hash(receipt: Mapping[str, Any]) -> tuple[bool, str]: + input_hash = receipt.get("input_hash") + if not isinstance(input_hash, str) or not SHA256_HEX.match(input_hash): + return False, "input_hash missing or malformed" + return True, "ok" + + +def _check_decision_result(receipt: Mapping[str, Any]) -> tuple[bool, str]: + expected = receipt.get("expected_result") + actual = receipt.get("actual_result") + if not isinstance(expected, str) or not isinstance(actual, str): + return False, "expected_result or actual_result missing" + if expected == "ALLOW": + ok = actual == "ALLOW" + elif expected.startswith("DENY"): + ok = actual == "DENY" + else: + return False, f"unknown expected_result: {expected}" + if not ok: + return False, f"expected {expected} but actual is {actual}" + return True, "ok" + + +def _check_refusal_reason(receipt: Mapping[str, Any]) -> tuple[bool, str]: + actual = receipt.get("actual_result") + reason = receipt.get("refusal_reason") + if actual == "ALLOW": + if reason is not None: + return False, f"ALLOW receipt must have null refusal_reason, got {reason!r}" + return True, "ok" + if actual == "DENY": + if not isinstance(reason, str) or not reason: + return False, "DENY receipt must have non-empty refusal_reason" + return True, "ok" + return False, f"unknown actual_result: {actual}" + + +def _check_no_execution_marker(receipt: Mapping[str, Any]) -> tuple[bool, str]: + mutation = receipt.get("mutation_occurred") + marker = receipt.get("no_execution_marker") + actual = receipt.get("actual_result") + if not isinstance(mutation, bool) or not isinstance(marker, bool): + return False, "mutation_occurred / no_execution_marker must be booleans" + if marker == mutation: + return False, "no_execution_marker must be the inverse of mutation_occurred" + if actual == "DENY": + if mutation is not False or marker is not True: + return ( + False, + "DENY receipt must have mutation_occurred=False and no_execution_marker=True", + ) + if actual == "ALLOW": + if mutation is not True or marker is not False: + return ( + False, + "ALLOW receipt must have mutation_occurred=True and no_execution_marker=False", + ) + return True, "ok" + + +CHECKS = ( + ("receipt_hash_integrity", _check_hash_integrity), + ("input_hash", _check_input_hash), + ("decision_result", _check_decision_result), + ("refusal_reason", _check_refusal_reason), + ("no_execution_marker", _check_no_execution_marker), +) + + +def verify_file(path: Path) -> bool: + print(f"Verifying: {path.relative_to(_REPO_ROOT) if path.is_relative_to(_REPO_ROOT) else path}") + try: + receipt = json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: + print(f" ERROR: could not parse JSON: {exc}") + return False + if not isinstance(receipt, dict): + print(" ERROR: receipt root must be an object") + return False + + file_ok = True + for name, check in CHECKS: + ok, detail = check(receipt) + status = "PASS" if ok else "FAIL" + print(f" [{status}] {name}: {detail}") + if not ok: + file_ok = False + return file_ok + + +def main(argv: list[str]) -> int: + if argv: + paths = [Path(arg) for arg in argv] + else: + if not DEFAULT_DIR.is_dir(): + print(f"No paths given and {DEFAULT_DIR} does not exist") + return 1 + paths = sorted(p for p in DEFAULT_DIR.iterdir() if p.suffix == ".json") + if not paths: + print(f"No JSON receipts found in {DEFAULT_DIR}") + return 1 + + all_ok = True + for path in paths: + ok = verify_file(path) + all_ok = all_ok and ok + print() + + print("=" * 60) + print(f"All receipts verified: {'YES' if all_ok else 'NO'}") + print("=" * 60) + return 0 if all_ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))