Decision Receipts
How Veto turns every policy decision into a portable, hash-linked, offline-verifiable veto.receipt/1 record.
A decision receipt is the portable, tamper-evident proof of a single authorization decision. Every time Veto allows, denies, escalates, or resolves a tool call, it can emit a canonical veto.receipt/1 payload that binds what was asked, which policy decided, what was decided, and what happened next into one deterministic record that anyone can verify later — without calling Veto.
If policy evaluation answers "may this action run?", receipts answer the question auditors, incident responders, and other agents ask afterward: "prove what you authorized, and prove the record was not edited."
Why a receipt and not a log line
Application logs are mutable, vendor-specific, and trust the writer. That is the wrong foundation for autonomous agents that move money, change infrastructure, or touch regulated data. A decision receipt is different in three ways:
- Canonical. The payload is serialized with RFC 8785 JSON Canonicalization and hashed with SHA-256, so the same decision always produces the same bytes and the same hash on any machine.
- Chained. Each receipt carries
previous_receipt_hash, linking it to the one before. Editing, reordering, or dropping a receipt breaks the chain. - Portable.
veto.receipt/1is a versioned, vendor-neutral schema. A receipt verifies the same way in your CI, your SIEM, a regulator's laptop, or another agent's runtime — using the openveto-receipt-protocolpackage, not a Veto API call.
What a receipt binds
Each receipt payload commits to the full context of one decision:
| Group | Fields | What it proves |
|---|---|---|
| Subject | organization_id, project_id, agent_id, session_id, client_id, connection_id, upstream_id | Who acted, and through which gateway path |
| Action | tool_name, tool_schema_hash, redacted_arguments, argument_hash | Which tool was called, with which arguments |
| Authority | policy_id, policy_version, policy_hash, decision, reason_code, reason_detail | Which policy version decided, and why |
| Outcome | result_hash, approval_id, approval_hash | What the tool returned, and who approved it |
| Integrity | previous_receipt_hash, merkle_root, timestamp, trace_id | Where it sits in the tamper-evident chain |
The decision field is one of allow, deny, require_approval, approval_approved, or approval_denied.
Redaction without losing provenance
Receipts must be safe to archive and forward, but still prove which arguments were evaluated. Veto stores two fields:
argument_hash— the canonical hash of the raw arguments.redacted_arguments— the same structure with string values replaced by[redacted](numbers, booleans, and nulls are preserved).
So a receipt for a $900 refund keeps { "amount_usd": 900, "customer_email": "[redacted]" } and a hash that still binds the exact email that was checked. You can prove an argument matched a known value by re-hashing it; you cannot read secrets out of the archive.
The chain and the Merkle root
Receipts form an append-only hash chain. The first receipt links to a fixed genesis value — the SHA-256 of the empty byte string (sha256:e3b0c4…b855) — and every later receipt links to its predecessor's hash. Each receipt also carries a protocol merkle_root field; the current verifier checks root continuity rather than claiming full batch-membership recomputation.
Verification re-derives all of this locally:
- Re-canonicalize and re-hash each payload; compare to the recorded hash.
- Confirm each
previous_receipt_hashequals the prior receipt's hash. - Check timestamp ordering and
merkle_rootcontinuity rules.
Any mutation — a changed amount, a swapped policy version, a deleted denial — changes a hash and breaks the chain at a known index.
The lifecycle of a receipt
Receipts track the whole decision, not just the first answer:
- A
POST /v1/validatecall or a guarded MCP tool call writes a receipt for theallow/deny/require_approvaldecision and surfaces a compact summary. - On the MCP allow path the receipt is finalized after the upstream tool runs, binding a
result_hashover the output. - Resolving an approval appends a second, chained receipt (
approval_approved/approval_denied) that binds theapproval_hash.
The result is a continuous chain where a human escalation and its resolution are cryptographically connected to the original request.
Summaries vs. canonical payloads
Hot-path responses return a four-field summary (receipt_id, receipt_hash, previous_receipt_hash, merkle_root) so latency stays low. The full canonical payload stays available for later:
GET /v1/decisions/:id/receipt— one canonical payload.GET /v1/receipts/export?format=ndjson— a stream of payloads for archives, SIEM ingestion, or batch verification.
Where receipts fit
Receipts are the verifiable surface of Veto's broader authority model. The audit hash chain anchors tenant-wide Merkle roots; receipts are the portable, per-decision artifact you can hand to someone outside your trust boundary. They are tamper-evident by construction: verification detects any edit to an exported chain. For external anchoring (publishing a root so you can prove a receipt existed at a point in time), pair exports with your own timestamping or the platform's chain anchoring.
Next steps
| Go to | For |
|---|---|
| Receipts API | Endpoints, payload schema, and field reference |
| Verify decision receipts | Export, verify in CI, and forward to a SIEM |
| SDK reference | veto-receipt-protocol helpers and JSON Schema exports |

