Acta decision receipts as action-receipt evidence¶
These fixtures are a second action-receipt profile alongside the embodied one in the parent directory. Where that profile evidences a physical action taken by an external controller (a robot arm, a safety monitor), this one evidences a software decision: an AI agent's tool call, evaluated and decided by a local policy gate before it runs. Same verification pattern from spec section 3.3.2, different action class, and the two should not be conflated: nothing here claims a physical outcome.
Full field mapping and rationale: docs/crosswalks/acta-decision-receipts.md.
Every fixture is a draft-farley-acta-signed-receipts-02 envelope (section 2.1): { "payload": {...}, "signature": { "alg", "kid", "sig" } }, with an access-decision payload per section 3.1 (type: "protectmcp:decision"). All six are real: generated by an actual Ed25519 signer over JCS-canonical payload bytes (gen.mjs in this directory), self-checked against expected.json at generation time, and re-verified in CI by tests/test_acta_fixtures.py using this repository's own rfc8785 and cryptography dependencies. Negative cases are committed artifacts, not transient states inside the generator.
Fixture cases¶
Expected results are machine-readable in expected.json.
| File | Signature | Other checks | Interpretation |
|---|---|---|---|
01-valid-accepted.json | pass | all pass | A safe tool call the policy permitted. First receipt in the chain (no previousReceiptHash). |
02-valid-denied.json | pass | all pass | A destructive tool call the policy blocked, evidenced with the same rigor as an allow. A denial is not an absence of evidence, it is its own signed record. previousReceiptHash correctly links to 01 per draft section 5.7. |
03-signature-key-mismatch.json | fail | n/a | Signed by a real but different key (mismatched-signer-public-key.txt, committed) while claiming the signer's kid. Resolving kid to the published signer-public-key.txt deterministically fails verification. The mismatch is in the committed bytes, reproducible by any independent verifier. |
04-broken-chain.json | pass | chain fail | Validly signed by the correct key, but previousReceiptHash carries the envelope hash of 01 when the receipt's actual predecessor in the chain is 02. The signature check passes; recomputing SHA-256(JCS(predecessor envelope)) does not match. This exercises chain verification as its own check: a dishonest or buggy issuer can validly sign a wrong chain pointer, and only recomputation against the actual predecessor catches reordering or omission. |
05-stale-policy-digest.json | pass | freshness fail | Validly signed, but policy_digest names a policy bundle that is no longer the one in force (current_policy_digest in expected.json). Cryptographic validity and authorization freshness are different checks; a verifier that stops at the signature accepts a receipt whose authorization basis has since changed. |
06-session-binding-mismatch.json | pass | binding fail | Validly signed, but session_id belongs to a different session than the one the verifier expects (expected_session_id in expected.json). A genuine receipt replayed from another session passes every cryptographic check and fails only the binding comparison. |
04, 05, and 06 together make one point three ways: signature verification proves integrity and origin, and nothing else. Chain position, policy freshness, and session binding are separate verifier obligations with separately observable failures.
Verifying independently¶
To check any fixture without this repo's tooling, per draft-02 sections 4.1 and 5.6:
- Extract
payloadfrom the envelope. - Serialize it with JCS (RFC 8785): recursively sorted keys, no whitespace.
- Verify
signature.sigas a PureEdDSA (Ed25519) signature directly over those bytes (no pre-hash), against the public key thatsignature.kidresolves to:signer-public-key.txt(hex). - Chain check (draft section 5.7):
previousReceiptHashmust equal the lowercase hex SHA-256 of the JCS bytes of the predecessor's entire envelope, signature included. The predecessor for each chained fixture is declared inexpected.json. - Freshness and binding checks: compare
policy_digestandsession_idagainstcurrent_policy_digestandexpected_session_idinexpected.json.
Expected outcomes per fixture are in expected.json; the CI test asserts exactly these, so fixture or envelope drift fails the build rather than passing silently.
Boundary¶
Same boundary the embodied profile states, generalized: a valid Acta receipt proves a specific policy decision, over a specific tool call, was signed by the operator's gate at a specific time, and that the record has not been altered since. It does not prove the tool call's real-world side effect was safe, correct, or reversible, and (per 04/05/06) signature validity alone does not prove chain position, policy freshness, or session binding.