Acta Decision Receipts Cross-walk¶
Non-normative. This document is informative only. Nothing here changes TRACE v0.1 schema fields, wire formats, required claims, or conformance requirements. References to "TRACE" mean the TRACE v0.1 Trust Record as defined in
spec/trace-v0.1.md. References to "Acta" mean the receipt format specified in draft-farley-acta-signed-receipts (revision 02); section references of the form "Acta s2.1" are to that draft.
Purpose¶
Spec section 3.3.2 describes action receipts as evidence that sits below a Trust Record: a per-action signed statement, bound to a call or session, that a verifier checks independently of the core record. The embodied-workflow fixtures show one profile of that pattern, for a physical controller (a robot arm, a safety monitor) signing an assertion about a physical action.
This cross-walk describes a second profile of the same pattern: an Acta decision receipt, evidencing a software decision (an AI agent's tool call, decided by a local policy gate before it runs) rather than a physical one. The verification pattern in 3.3.2 is deliberately domain-agnostic; the intent here is to show it composes with an existing, independently specified receipt format without a wire-format change to either side.
This follows the acceptance bar raised on trace-spec#97: carry Acta as external evidence by digest/chain head and issuer/key metadata rather than embedding a second receipt schema into the Trust Record; state exactly which Acta fields satisfy the action-receipt verifier obligations; and keep the physical/software boundary explicit.
What an Acta decision receipt is¶
Per Acta s2.1, a receipt is a two-field envelope: a payload and a signature object. The access-decision payload type (Acta s3.1) records the outcome of a policy evaluation for a tool invocation. Fixture 01-valid-accepted.json in full:
{
"payload": {
"type": "protectmcp:decision",
"tool_name": "run_shell",
"decision": "allow",
"policy_digest": "sha256:bda8d8f60c90afe5c05fc7f3b1b53d4715a0f7ba72ec8d4b82ad7aba70d9609b",
"session_id": "ses_8f31ab",
"issued_at": "2026-07-08T09:00:01.000Z",
"issuer_id": "sb:issuer:QUGuJV1P6e6c"
},
"signature": {
"alg": "EdDSA",
"kid": "sb:issuer:QUGuJV1P6e6c",
"sig": "cf4665db8613b3b8b398fae6c1bacf88e4963d300af63cf709a2be1e8a6bda69..."
}
}
Signature semantics (Acta s5.6): signature.sig is a PureEdDSA (Ed25519) signature directly over the JCS-canonical (RFC 8785) bytes of payload, with no intermediate hash. payload.issuer_id MUST match signature.kid (Acta s2.2); the RECOMMENDED kid format is sb:issuer:<first 12 Base58 characters of the public key> (Acta s2.1.1), and the key is resolved out of band, never carried in the receipt.
Chain semantics (Acta s5.7): previousReceiptHash is the bare lowercase hex SHA-256 of the JCS bytes of the predecessor's entire signed envelope, signature included. Including the signature binds the chain to specific signed bytes, so re-signing an identical payload produces a distinct chain link.
decision is allow, deny, or rate_limit; a deny receipt is signed with the same rigor as an allow, which is the property trace-spec#95's "valid negative controller outcome" case and this profile's 02-valid-denied.json fixture both exercise.
Working fixtures: examples/action-receipts/acta/.
Field mapping¶
Field names on the left are exact TRACE terms as used in spec section 3.3.2 and the embodied fixture shape. Field names on the right are exact Acta draft-02 fields.
| TRACE action-receipt obligation | Acta field(s) | Notes |
|---|---|---|
receipt.issuer | payload.issuer_id | REQUIRED common field (Acta s2.2); MUST match signature.kid, so issuer identity and key identity cannot silently diverge inside one receipt. |
receipt.issuer_key_id | signature.kid | Resolved the same way 3.3.2 describes: through a pinned, manifest-bound, or otherwise trusted key set, not carried in the receipt as a public key. |
receipt.linked_call_id / session_id | payload.session_id | Acta s3.1 binds at session level; session_id is OPTIONAL, and when present MUST NOT contain PII or be correlatable across sessions unless the operator explicitly configures session binding. Acta has no per-call identifier field; a TRACE-composing deployment gets call-level binding by carrying the tool input's digest in payload_digest (below) or by a deployment extension field, and should say which. |
receipt.evidence_type | payload.type | Namespaced receipt type (Acta s2.2), here protectmcp:decision. |
receipt.evidence_hash | payload.payload_digest | OPTIONAL common field (Acta s2.2): {hash, size, preview?} over associated data (tool input/output) too large to embed. Serves exactly the external-evidence-by-digest role 3.3.2 describes. |
receipt.previous_receipt_hash | payload.previousReceiptHash | Bare lowercase hex over the predecessor's full envelope including its signature (Acta s5.7). Note the deliberate difference from formats that hash payload only: re-signing an identical payload yields a distinct chain link. |
receipt.decision | payload.decision (+ payload.reason) | reason (OPTIONAL) names the machine-readable cause, e.g. policy_block, tier_insufficient, beyond the bare outcome. |
receipt.signature | signature.sig (with signature.alg) | EdDSA is mandatory-to-implement; ES256 optional, ML-DSA-65 recommended for new deployments (Acta s5.8). |
| (no TRACE equivalent) | payload.policy_digest | The digest of the policy bundle in force when the decision was made. Signature validity does not imply this digest is still current; see 05-stale-policy-digest.json and the freshness discussion below. |
Referencing an Acta chain from a Trust Record¶
Per the direction set on trace-spec#97 and the closed decision on trace-spec#34, a Trust Record references external evidence by digest plus issuer/key metadata; it does not embed the evidence or its schema. For an Acta chain, the natural reference is the chain head: the same digest construction Acta s5.7 already uses for previousReceiptHash (SHA-256 over the JCS bytes of the latest receipt's entire signed envelope), so a verifier holding the referenced receipts can recompute it with no additional convention. An illustrative (non-normative; field names shown are descriptive, not proposed schema additions) evidence entry for the two-receipt fixture chain in this profile:
{
"evidence_type": "acta/decision-receipt-chain",
"chain_head": "ddf7efb089f7504bbe880a623976b34be04b0bd4a991cdec12288841030229e4",
"receipt_count": 2,
"issuer_key_id": "sb:issuer:QUGuJV1P6e6c"
}
The chain_head value above is real: it is the recomputable s5.7 envelope hash of 02-valid-denied.json, the latest receipt in the fixture chain. A verifier resolves issuer_key_id through its trusted key set (never a key carried in the evidence), fetches or is handed the referenced receipts out of band, walks the chain from the head, and runs the per-receipt checks below. The Trust Record itself stays small and schema-stable regardless of how many receipts the chain contains.
Verifier obligations, restated for this profile¶
Following the same checks the embodied fixture README lists:
- Extract
payloadand compute its JCS-canonical bytes (Acta s4.1). - Resolve
signature.kidthrough a pinned or manifest-bound key set, not a key carried in the receipt, and checkpayload.issuer_idmatchessignature.kid. - Verify
signature.sigas PureEdDSA directly over those bytes (03-signature-key-mismatch.jsonfails exactly here: a real signature by a different, committed key under the same claimedkid). - If the deployment binds
session_id(or a call identifier carried viapayload_digestor extension) to the surrounding TRACE/cMCP context, verify that binding (06-session-binding-mismatch.jsonis validly signed and fails exactly this check). - Chain check, distinct from the signature check: recompute SHA-256 over the JCS bytes of the predecessor's entire envelope and compare to
payload.previousReceiptHash(Acta s5.7).04-broken-chain.jsonis validly signed by the correct key and fails exactly here; a dishonest or buggy issuer can sign a wrong chain pointer, and only recomputation against the actual predecessor detects reordering or omission.
A sixth check this profile adds: compare payload.policy_digest against the policy bundle actually in force at verification time. 05-stale-policy-digest.json is a genuinely, cryptographically valid receipt (step 3 passes) whose policy binding is out of date; a verifier that stops at step 3 accepts an authorization basis that no longer exists. This is the same distinction raised independently in the Composable Trust Evidence Format discussion as policy_digest / policy_or_verdict_id: cryptographic integrity and authorization freshness are different properties, and a conformant verifier checks both.
Boundary¶
An Acta decision receipt proves that a specific policy decision, over a specific proposed tool call, was signed by the issuing gate at a specific time, and that the payload has not been altered since. Composed with TRACE per this cross-walk, it does not prove:
- that the decided tool call's real-world side effect was safe, correct, or reversible (out of scope for both Acta and TRACE);
- that the receipt occupies the chain position it claims, that its policy binding is still current, or that it is being presented under the session it was issued for, if only the signature was checked (fixtures
04,05,06respectively; each is validly signed and fails exactly one non-signature check); - physical completion or functional-safety certification of any kind, which is the boundary spec section 2.4 and the external-execution-evidence decision on trace-spec#34 already state for the embodied case, and which applies identically here.
Conformance fixtures¶
Six real fixtures in examples/action-receipts/acta/, generated by an actual Ed25519 signer (generator committed alongside), covering the negative cases raised in trace-spec#97 and trace-spec#95: valid accepted, valid denied (negative controller-equivalent outcome), signature/key mismatch (mismatched key committed), broken chain (validly signed, wrong predecessor hash), stale policy digest, and mismatched session binding. Expected outcomes are machine-readable in expected.json, and tests/test_acta_fixtures.py re-verifies every fixture in CI against the draft-02 envelope and the declared positive/negative results, using this repository's existing rfc8785 and cryptography dependencies, so fixture or envelope drift fails the build.
References¶
- TRACE v0.1 specification, section 3.3.2:
spec/trace-v0.1.md - TRACE v0.1 specification, section 2.4 (permanent scope boundaries):
spec/trace-v0.1.md - trace-spec#34 (external execution evidence, closed): https://github.com/agentrust-io/trace-spec/issues/34
- trace-spec#66 (verification depth + action_receipts): https://github.com/agentrust-io/trace-spec/issues/66
- trace-spec#95 (action receipt conformance cases): https://github.com/agentrust-io/trace-spec/issues/95
- trace-spec#97 (this cross-walk's origin issue): https://github.com/agentrust-io/trace-spec/issues/97
- draft-farley-acta-signed-receipts (revision 02): https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/
- Shared conformance suite for Acta implementations: https://github.com/ScopeBlind/agent-governance-testvectors