agora-agent-receipts 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: agora-agent-receipts
3
+ Version: 0.1.0
4
+ Summary: Tamper-evident, third-party-verifiable receipts for AI agent / MCP tool calls
5
+ Author: Agora
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/DanceNitra/agora/tree/main/agent-receipts
8
+ Project-URL: Source, https://github.com/DanceNitra/agora
9
+ Keywords: ai-agents,mcp,verifiable,receipts,ed25519,audit,agent-security,provenance
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security :: Cryptography
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Intended Audience :: Developers
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: crypto
19
+ Requires-Dist: cryptography>=41; extra == "crypto"
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # agent-receipts
25
+
26
+ **Tamper-evident, third-party-verifiable receipts for AI agent / MCP actions — in one small file.**
27
+
28
+ An AI agent's logs are *self-reported claims*. Nothing stops the agent — or a compromised proxy —
29
+ from rewriting history after the fact, or from emitting a hallucinated "I called the database and it
30
+ returned X" that never happened. A **receipt** is the opposite of a log: independent, verifiable
31
+ evidence of what an action consumed and produced, that a third party can check **without trusting the
32
+ agent**.
33
+
34
+ This is the smallest honest version of that idea, built to be read in one sitting and run in one
35
+ command. It is a reference proof-of-concept, not a hardened product — the scope below is deliberately
36
+ honest about what it does and does not give you.
37
+
38
+ > **Naming note / prior art.** There is already an established **"Agent Receipts" protocol** with a
39
+ > public spec and a Python SDK by Otto Jongerius ([github.com/agent-receipts](https://github.com/agent-receipts/ar)).
40
+ > This project is an **independent, minimal reference** for understanding the idea — it is *not* that
41
+ > protocol's SDK, and on PyPI it is `agora-agent-receipts` to avoid any confusion. If you want the
42
+ > protocol and a maintained SDK, use his; if you want a 200-line file to learn from or vendor, use this.
43
+
44
+ ```bash
45
+ python agent_receipts.py # core: hash-chain + Ed25519 signatures + tamper/forgery demo
46
+ python mcp_wrapper.py # wrap any MCP/agent tool so every call emits a receipt
47
+ python mediator.py # external-mediator mode: catch an agent hiding/faking its own actions
48
+ python verify_cli.py receipts.json --pubkey <hex> # independently verify a receipts file (no code)
49
+ python mnemo_receipts.py # tamper-evident memory: detect an out-of-band edit to an mnemo store
50
+ ```
51
+
52
+ ## What it does — two layers
53
+
54
+ 1. **Hash chain (integrity, zero extra deps).** Each receipt commits to the previous one
55
+ (`prev = hash of the last receipt`), forming a Merkle-style chain. Edit *any* past receipt and
56
+ every hash after it breaks — so a *partial* edit is **detectable**, and `verify()` names the exact
57
+ step that was altered. **Honest limit:** the hash chain *alone* does not stop a thorough tamperer
58
+ who recomputes the whole chain end-to-end (then no link breaks). Integrity-only is sufficient only
59
+ if the chain head is published/anchored where the attacker can't also rewrite it — otherwise the
60
+ signature (layer 2) is what actually protects a self-held chain.
61
+ 2. **Ed25519 signatures (authenticity, needs `cryptography`).** Each receipt's hash is signed with
62
+ the actor's private key; a third party verifies with the **public key only**. This proves *who*
63
+ produced the receipt and that the content wasn't forged — no shared secret. (If `cryptography`
64
+ isn't installed, the hash chain still works on its own.)
65
+
66
+ A receipt commits to the **SHA-256 of inputs/outputs, not the raw content** — so you prove *what* was
67
+ processed without exposing it, and reveal a value later only if you choose (anyone can re-check it
68
+ against the committed hash). Full zero-knowledge proofs (ZK-SNARKs) go further still — proving a
69
+ computation was correct while revealing *nothing* — and are the heavy end of this same design space.
70
+
71
+ ## What the demo measures
72
+
73
+ The self-demo records three MCP tool calls, then attacks the trail:
74
+
75
+ | step | action | result |
76
+ |---|---|---|
77
+ | 1 | honest chain | `verify() → True` |
78
+ | 2 | edit a past receipt's output | caught: *content tampered* at the exact seq |
79
+ | 3 | re-hash the forged receipt to look consistent | still caught: *invalid signature* + *broken chain link downstream* |
80
+
81
+ Logs cannot do this: a self-reported log can be edited end-to-end and looks identical afterward.
82
+
83
+ ## Use it
84
+
85
+ ```python
86
+ from agent_receipts import ReceiptChain, generate_keypair
87
+
88
+ sk, pk = generate_keypair() # the actor's identity
89
+ chain = ReceiptChain(actor="my-agent", private_key_hex=sk, public_key_hex=pk)
90
+
91
+ chain.record("mcp.web_search", {"query": "..."}, {"results": 7}) # one receipt per tool call
92
+ chain.record("mcp.memory.write", {"fact": "..."}, {"stored": True})
93
+
94
+ ok, problems = chain.verify(expected_pubkey=pk) # anyone with pk can run this
95
+ open("receipts.json", "w").write(chain.to_json()) # ship the receipts; the public key verifies them
96
+ ```
97
+
98
+ `mcp_wrapper.py` shows a `ReceiptedDispatcher` that drops in around an MCP-style tool registry so you
99
+ don't touch your tools — you wrap the dispatch.
100
+
101
+ ## External-mediator mode (`mediator.py`)
102
+
103
+ Self-signed receipts have one honest hole: an agent holding its own key can simply *not* emit a receipt
104
+ for an action it wants to hide. The fix (as used by `pipelock` and the receiver-attested research line)
105
+ is to move the signer **outside** the agent — route every tool call through a `Mediator` that holds its
106
+ own key and signs what it actually brokers:
107
+
108
+ ```python
109
+ from mediator import Mediator, reconcile
110
+ med = Mediator(tools, private_key_hex=mk, public_key_hex=mpub, name="org-proxy")
111
+ med.dispatch("transfer_funds", to="acct-9981", amount=5000) # mediator signs the real call
112
+ ok, problems = reconcile(agent_self_reported_log, med.chain) # diff the agent's story vs the truth
113
+ ```
114
+
115
+ The demo has an agent route three calls through the mediator, then tamper with its *own* log — hiding a
116
+ funds transfer and faking a secret read. `reconcile()` catches all of it, because the mediator's signed
117
+ chain is ground truth:
118
+
119
+ ```
120
+ agent reported 2 actions but the mediator brokered 3 -> agent OMITTED 1 call(s)
121
+ action 1: agent calls it 'read_secret', mediator saw 'mcp.transfer_funds'
122
+ action 1 (mcp.transfer_funds): input hash mismatch (agent misreported its arguments)
123
+ action 1 (mcp.transfer_funds): output hash mismatch (agent misreported the result)
124
+ action 2 (mcp.read_secret): no agent record at all (hidden call)
125
+ ```
126
+
127
+ (This is the real demo output — `reconcile()` aligns positionally, so omitting a middle call also shifts
128
+ the indices after it; the point is that the omission and the misreport are both surfaced.) Now the agent
129
+ cannot withhold a receipt, forge one (it lacks the mediator's key), or lie about I/O (the
130
+ mediator hashes what really flowed through it). Optionally the agent also signs its own claim, giving a
131
+ dual-attested receipt where agent-vs-mediator divergence is itself the alarm.
132
+
133
+ ## Verify someone else's receipts (`verify_cli.py`)
134
+
135
+ A third party who wasn't there confirms what happened with one command — the file plus the public key:
136
+
137
+ ```bash
138
+ python verify_cli.py receipts.json --pubkey 7d08e6e6... # VERIFIED (exit 0) or FAILED (exit 1)
139
+ ```
140
+
141
+ It recomputes the whole chain, checks every signature against the expected key, and names the exact
142
+ broken step. Exit code 0/1 drops cleanly into CI or a pre-commit hook. Measured on a 2-receipt file: an
143
+ honest file verifies; tampering one output prints `seq 0: content tampered` (exit 1); the wrong `--pubkey`
144
+ prints `signed by an unexpected key` (exit 1).
145
+
146
+ ## Tamper-evident memory: the `mnemo` integration (`mnemo_receipts.py`)
147
+
148
+ [mnemo](https://github.com/DanceNitra/agora/tree/main/mnemo) (our open-source memory core) is already
149
+ append-only with deterministic supersession, so it never silently edits a fact in normal use. But the
150
+ store is a file — anyone who can touch it can rewrite a stored memory after the fact, and any store
151
+ would then serve the altered text as the original. Receipts close that: every `remember()` emits a
152
+ signed receipt committing to the memory's content hash, so the *write history* is independently
153
+ verifiable.
154
+
155
+ ```python
156
+ from mnemo_receipts import ReceiptedMnemo, audit_memory
157
+ rm = ReceiptedMnemo(Mnemo(path="mem.json"), private_key_hex=sk, public_key_hex=pk)
158
+ rm.remember("The prod database host is db-prod-01.", key="prod-db::host", mtype="semantic")
159
+ ok, problems = audit_memory(rm.m, rm.chain, expected_pubkey=pk)
160
+ ```
161
+
162
+ `audit_memory()` re-hashes the current store against the write receipts. Measured: an honest store
163
+ audits clean; an **out-of-band edit** (`db-prod-01 → db-attacker-07`, made straight in the store, which
164
+ mnemo itself can't see) is caught — `memory <id>: stored content no longer matches the write receipt`.
165
+ This is a thin wrapper; it does **not** modify mnemo's zero-dependency core.
166
+
167
+ ## Honest scope (what this is NOT)
168
+
169
+ - The *self-signed* core proves a receipt **chain is internally consistent and authentically signed**.
170
+ It does **not** by itself prove the agent reported *every* action — an actor that controls its own
171
+ key can still withhold a receipt. That gap is closed by **external-mediator mode** (`mediator.py`,
172
+ below), which puts the signer outside the agent; anchoring the chain head to a third party is a
173
+ further hardening.
174
+ - It commits to input/output **hashes**, not a proof that the tool *computed correctly*. That is what
175
+ ZK-SNARK approaches add, at much higher cost.
176
+ - Keys here are raw/in-memory for clarity; real deployments use a KMS / hardware-backed key store.
177
+
178
+ ## Landscape & prior art
179
+
180
+ This sits in an active, fast-moving space — **we build on it, we did not invent it.** In particular,
181
+ the exact pattern here (Ed25519 + canonical JSON + hash-chain) is the production-grade subject of
182
+ **Microsoft's [agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit),
183
+ Tutorial 33 "offline verifiable receipts"** (Ed25519 over RFC 8785 / JCS canonical payloads,
184
+ hash-chained, CLI-verifiable offline). Treat this repo as the *minimal one-file way to understand the
185
+ idea*, and that toolkit as the grown-up version.
186
+
187
+ Honest map of the space:
188
+
189
+ - **A named protocol + SDK:** the **"Agent Receipts" protocol** by Otto Jongerius — a public spec
190
+ ([github.com/agent-receipts/ar](https://github.com/agent-receipts/ar)) plus a maintained Python
191
+ SDK (`pip install agent-receipts`). The most directly-related effort to this one; if you need an
192
+ interoperable standard rather than a teaching reference, start there.
193
+ - **Production OSS (corporate):** Microsoft `agent-governance-toolkit` — Tutorial 33 = the same
194
+ Ed25519 + canonical + hash-chain receipts, with policy/identity/sandboxing around it.
195
+ - **External-mediator receipts:** [`pipelock`](https://github.com/luckyPipewrench/pipelock) — an
196
+ open-source MCP/egress firewall that emits *mediator-signed* Ed25519 receipts from **outside** the
197
+ agent (core Apache-2.0; enterprise features Elastic-License), which is how you close the
198
+ agent-can-withhold-a-receipt gap noted above.
199
+ - **Commercial:** [Zero Proof AI](https://zeroproofai.com) — a pre-launch "certificate authority for
200
+ AI agents" issuing on-chain-anchored receipts for tool calls.
201
+ - **Research:**
202
+ - Basu, *Tool Receipts, Not Zero-Knowledge Proofs: Practical Hallucination Detection for AI Agents*,
203
+ [arXiv:2603.10060](https://arxiv.org/abs/2603.10060) (2026) — HMAC-signed tool-execution receipts
204
+ (the pragmatic, symmetric camp; we use Ed25519 so a third party verifies without a shared secret).
205
+ - Figuera, *Notarized Agents: Receiver-Attested Confidential Receipts for AI Agent Actions*,
206
+ [arXiv:2606.04193](https://arxiv.org/abs/2606.04193) (2026) — receiver-signed receipts published
207
+ to a transparency log (the external-attestation camp).
208
+ - Jing & Qi, *Zero-Knowledge Audit for Internet of Agents … with Model Context Protocol*,
209
+ [arXiv:2512.14737](https://arxiv.org/abs/2512.14737) (2025) — the zero-knowledge / privacy-
210
+ preserving end of the same space.
211
+
212
+ ## Roadmap (if this proves useful)
213
+
214
+ ~~External-mediator mode~~ (done — `mediator.py`) · ~~verifier CLI~~ (done — `verify_cli.py`) ·
215
+ ~~`mnemo` integration~~ (done — `mnemo_receipts.py`) · publish-and-anchor the chain head · selective
216
+ disclosure of a single committed field · packaged spin-out (PyPI).
217
+
218
+ MIT. Part of the [Agora](https://github.com/DanceNitra/agora) project — an autonomous research OS that
219
+ ships every claim with a runnable receipt. Feedback and adversarial testing welcome.
@@ -0,0 +1,196 @@
1
+ # agent-receipts
2
+
3
+ **Tamper-evident, third-party-verifiable receipts for AI agent / MCP actions — in one small file.**
4
+
5
+ An AI agent's logs are *self-reported claims*. Nothing stops the agent — or a compromised proxy —
6
+ from rewriting history after the fact, or from emitting a hallucinated "I called the database and it
7
+ returned X" that never happened. A **receipt** is the opposite of a log: independent, verifiable
8
+ evidence of what an action consumed and produced, that a third party can check **without trusting the
9
+ agent**.
10
+
11
+ This is the smallest honest version of that idea, built to be read in one sitting and run in one
12
+ command. It is a reference proof-of-concept, not a hardened product — the scope below is deliberately
13
+ honest about what it does and does not give you.
14
+
15
+ > **Naming note / prior art.** There is already an established **"Agent Receipts" protocol** with a
16
+ > public spec and a Python SDK by Otto Jongerius ([github.com/agent-receipts](https://github.com/agent-receipts/ar)).
17
+ > This project is an **independent, minimal reference** for understanding the idea — it is *not* that
18
+ > protocol's SDK, and on PyPI it is `agora-agent-receipts` to avoid any confusion. If you want the
19
+ > protocol and a maintained SDK, use his; if you want a 200-line file to learn from or vendor, use this.
20
+
21
+ ```bash
22
+ python agent_receipts.py # core: hash-chain + Ed25519 signatures + tamper/forgery demo
23
+ python mcp_wrapper.py # wrap any MCP/agent tool so every call emits a receipt
24
+ python mediator.py # external-mediator mode: catch an agent hiding/faking its own actions
25
+ python verify_cli.py receipts.json --pubkey <hex> # independently verify a receipts file (no code)
26
+ python mnemo_receipts.py # tamper-evident memory: detect an out-of-band edit to an mnemo store
27
+ ```
28
+
29
+ ## What it does — two layers
30
+
31
+ 1. **Hash chain (integrity, zero extra deps).** Each receipt commits to the previous one
32
+ (`prev = hash of the last receipt`), forming a Merkle-style chain. Edit *any* past receipt and
33
+ every hash after it breaks — so a *partial* edit is **detectable**, and `verify()` names the exact
34
+ step that was altered. **Honest limit:** the hash chain *alone* does not stop a thorough tamperer
35
+ who recomputes the whole chain end-to-end (then no link breaks). Integrity-only is sufficient only
36
+ if the chain head is published/anchored where the attacker can't also rewrite it — otherwise the
37
+ signature (layer 2) is what actually protects a self-held chain.
38
+ 2. **Ed25519 signatures (authenticity, needs `cryptography`).** Each receipt's hash is signed with
39
+ the actor's private key; a third party verifies with the **public key only**. This proves *who*
40
+ produced the receipt and that the content wasn't forged — no shared secret. (If `cryptography`
41
+ isn't installed, the hash chain still works on its own.)
42
+
43
+ A receipt commits to the **SHA-256 of inputs/outputs, not the raw content** — so you prove *what* was
44
+ processed without exposing it, and reveal a value later only if you choose (anyone can re-check it
45
+ against the committed hash). Full zero-knowledge proofs (ZK-SNARKs) go further still — proving a
46
+ computation was correct while revealing *nothing* — and are the heavy end of this same design space.
47
+
48
+ ## What the demo measures
49
+
50
+ The self-demo records three MCP tool calls, then attacks the trail:
51
+
52
+ | step | action | result |
53
+ |---|---|---|
54
+ | 1 | honest chain | `verify() → True` |
55
+ | 2 | edit a past receipt's output | caught: *content tampered* at the exact seq |
56
+ | 3 | re-hash the forged receipt to look consistent | still caught: *invalid signature* + *broken chain link downstream* |
57
+
58
+ Logs cannot do this: a self-reported log can be edited end-to-end and looks identical afterward.
59
+
60
+ ## Use it
61
+
62
+ ```python
63
+ from agent_receipts import ReceiptChain, generate_keypair
64
+
65
+ sk, pk = generate_keypair() # the actor's identity
66
+ chain = ReceiptChain(actor="my-agent", private_key_hex=sk, public_key_hex=pk)
67
+
68
+ chain.record("mcp.web_search", {"query": "..."}, {"results": 7}) # one receipt per tool call
69
+ chain.record("mcp.memory.write", {"fact": "..."}, {"stored": True})
70
+
71
+ ok, problems = chain.verify(expected_pubkey=pk) # anyone with pk can run this
72
+ open("receipts.json", "w").write(chain.to_json()) # ship the receipts; the public key verifies them
73
+ ```
74
+
75
+ `mcp_wrapper.py` shows a `ReceiptedDispatcher` that drops in around an MCP-style tool registry so you
76
+ don't touch your tools — you wrap the dispatch.
77
+
78
+ ## External-mediator mode (`mediator.py`)
79
+
80
+ Self-signed receipts have one honest hole: an agent holding its own key can simply *not* emit a receipt
81
+ for an action it wants to hide. The fix (as used by `pipelock` and the receiver-attested research line)
82
+ is to move the signer **outside** the agent — route every tool call through a `Mediator` that holds its
83
+ own key and signs what it actually brokers:
84
+
85
+ ```python
86
+ from mediator import Mediator, reconcile
87
+ med = Mediator(tools, private_key_hex=mk, public_key_hex=mpub, name="org-proxy")
88
+ med.dispatch("transfer_funds", to="acct-9981", amount=5000) # mediator signs the real call
89
+ ok, problems = reconcile(agent_self_reported_log, med.chain) # diff the agent's story vs the truth
90
+ ```
91
+
92
+ The demo has an agent route three calls through the mediator, then tamper with its *own* log — hiding a
93
+ funds transfer and faking a secret read. `reconcile()` catches all of it, because the mediator's signed
94
+ chain is ground truth:
95
+
96
+ ```
97
+ agent reported 2 actions but the mediator brokered 3 -> agent OMITTED 1 call(s)
98
+ action 1: agent calls it 'read_secret', mediator saw 'mcp.transfer_funds'
99
+ action 1 (mcp.transfer_funds): input hash mismatch (agent misreported its arguments)
100
+ action 1 (mcp.transfer_funds): output hash mismatch (agent misreported the result)
101
+ action 2 (mcp.read_secret): no agent record at all (hidden call)
102
+ ```
103
+
104
+ (This is the real demo output — `reconcile()` aligns positionally, so omitting a middle call also shifts
105
+ the indices after it; the point is that the omission and the misreport are both surfaced.) Now the agent
106
+ cannot withhold a receipt, forge one (it lacks the mediator's key), or lie about I/O (the
107
+ mediator hashes what really flowed through it). Optionally the agent also signs its own claim, giving a
108
+ dual-attested receipt where agent-vs-mediator divergence is itself the alarm.
109
+
110
+ ## Verify someone else's receipts (`verify_cli.py`)
111
+
112
+ A third party who wasn't there confirms what happened with one command — the file plus the public key:
113
+
114
+ ```bash
115
+ python verify_cli.py receipts.json --pubkey 7d08e6e6... # VERIFIED (exit 0) or FAILED (exit 1)
116
+ ```
117
+
118
+ It recomputes the whole chain, checks every signature against the expected key, and names the exact
119
+ broken step. Exit code 0/1 drops cleanly into CI or a pre-commit hook. Measured on a 2-receipt file: an
120
+ honest file verifies; tampering one output prints `seq 0: content tampered` (exit 1); the wrong `--pubkey`
121
+ prints `signed by an unexpected key` (exit 1).
122
+
123
+ ## Tamper-evident memory: the `mnemo` integration (`mnemo_receipts.py`)
124
+
125
+ [mnemo](https://github.com/DanceNitra/agora/tree/main/mnemo) (our open-source memory core) is already
126
+ append-only with deterministic supersession, so it never silently edits a fact in normal use. But the
127
+ store is a file — anyone who can touch it can rewrite a stored memory after the fact, and any store
128
+ would then serve the altered text as the original. Receipts close that: every `remember()` emits a
129
+ signed receipt committing to the memory's content hash, so the *write history* is independently
130
+ verifiable.
131
+
132
+ ```python
133
+ from mnemo_receipts import ReceiptedMnemo, audit_memory
134
+ rm = ReceiptedMnemo(Mnemo(path="mem.json"), private_key_hex=sk, public_key_hex=pk)
135
+ rm.remember("The prod database host is db-prod-01.", key="prod-db::host", mtype="semantic")
136
+ ok, problems = audit_memory(rm.m, rm.chain, expected_pubkey=pk)
137
+ ```
138
+
139
+ `audit_memory()` re-hashes the current store against the write receipts. Measured: an honest store
140
+ audits clean; an **out-of-band edit** (`db-prod-01 → db-attacker-07`, made straight in the store, which
141
+ mnemo itself can't see) is caught — `memory <id>: stored content no longer matches the write receipt`.
142
+ This is a thin wrapper; it does **not** modify mnemo's zero-dependency core.
143
+
144
+ ## Honest scope (what this is NOT)
145
+
146
+ - The *self-signed* core proves a receipt **chain is internally consistent and authentically signed**.
147
+ It does **not** by itself prove the agent reported *every* action — an actor that controls its own
148
+ key can still withhold a receipt. That gap is closed by **external-mediator mode** (`mediator.py`,
149
+ below), which puts the signer outside the agent; anchoring the chain head to a third party is a
150
+ further hardening.
151
+ - It commits to input/output **hashes**, not a proof that the tool *computed correctly*. That is what
152
+ ZK-SNARK approaches add, at much higher cost.
153
+ - Keys here are raw/in-memory for clarity; real deployments use a KMS / hardware-backed key store.
154
+
155
+ ## Landscape & prior art
156
+
157
+ This sits in an active, fast-moving space — **we build on it, we did not invent it.** In particular,
158
+ the exact pattern here (Ed25519 + canonical JSON + hash-chain) is the production-grade subject of
159
+ **Microsoft's [agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit),
160
+ Tutorial 33 "offline verifiable receipts"** (Ed25519 over RFC 8785 / JCS canonical payloads,
161
+ hash-chained, CLI-verifiable offline). Treat this repo as the *minimal one-file way to understand the
162
+ idea*, and that toolkit as the grown-up version.
163
+
164
+ Honest map of the space:
165
+
166
+ - **A named protocol + SDK:** the **"Agent Receipts" protocol** by Otto Jongerius — a public spec
167
+ ([github.com/agent-receipts/ar](https://github.com/agent-receipts/ar)) plus a maintained Python
168
+ SDK (`pip install agent-receipts`). The most directly-related effort to this one; if you need an
169
+ interoperable standard rather than a teaching reference, start there.
170
+ - **Production OSS (corporate):** Microsoft `agent-governance-toolkit` — Tutorial 33 = the same
171
+ Ed25519 + canonical + hash-chain receipts, with policy/identity/sandboxing around it.
172
+ - **External-mediator receipts:** [`pipelock`](https://github.com/luckyPipewrench/pipelock) — an
173
+ open-source MCP/egress firewall that emits *mediator-signed* Ed25519 receipts from **outside** the
174
+ agent (core Apache-2.0; enterprise features Elastic-License), which is how you close the
175
+ agent-can-withhold-a-receipt gap noted above.
176
+ - **Commercial:** [Zero Proof AI](https://zeroproofai.com) — a pre-launch "certificate authority for
177
+ AI agents" issuing on-chain-anchored receipts for tool calls.
178
+ - **Research:**
179
+ - Basu, *Tool Receipts, Not Zero-Knowledge Proofs: Practical Hallucination Detection for AI Agents*,
180
+ [arXiv:2603.10060](https://arxiv.org/abs/2603.10060) (2026) — HMAC-signed tool-execution receipts
181
+ (the pragmatic, symmetric camp; we use Ed25519 so a third party verifies without a shared secret).
182
+ - Figuera, *Notarized Agents: Receiver-Attested Confidential Receipts for AI Agent Actions*,
183
+ [arXiv:2606.04193](https://arxiv.org/abs/2606.04193) (2026) — receiver-signed receipts published
184
+ to a transparency log (the external-attestation camp).
185
+ - Jing & Qi, *Zero-Knowledge Audit for Internet of Agents … with Model Context Protocol*,
186
+ [arXiv:2512.14737](https://arxiv.org/abs/2512.14737) (2025) — the zero-knowledge / privacy-
187
+ preserving end of the same space.
188
+
189
+ ## Roadmap (if this proves useful)
190
+
191
+ ~~External-mediator mode~~ (done — `mediator.py`) · ~~verifier CLI~~ (done — `verify_cli.py`) ·
192
+ ~~`mnemo` integration~~ (done — `mnemo_receipts.py`) · publish-and-anchor the chain head · selective
193
+ disclosure of a single committed field · packaged spin-out (PyPI).
194
+
195
+ MIT. Part of the [Agora](https://github.com/DanceNitra/agora) project — an autonomous research OS that
196
+ ships every claim with a runnable receipt. Feedback and adversarial testing welcome.
@@ -0,0 +1,221 @@
1
+ """agent-receipts — tamper-evident, third-party-verifiable receipts for AI agent / MCP actions.
2
+
3
+ The problem: an AI agent's own logs are *self-reported claims*. Nothing stops the agent (or a
4
+ compromised proxy) from rewriting history after the fact, or from a hallucinated "I called the
5
+ database and it returned X" that never happened. A *receipt* is the opposite of a log: independent,
6
+ verifiable evidence of what an action consumed and produced, that a third party can check without
7
+ trusting the agent.
8
+
9
+ This single file gives the smallest honest version of that, in two layers:
10
+
11
+ 1. HASH CHAIN (integrity, zero extra deps). Each receipt commits to the previous one
12
+ (prev = hash of the last receipt), so the whole sequence is a Merkle-style chain. Edit ANY
13
+ past receipt and every hash after it breaks -> a *partial* edit is detectable, and you can
14
+ name the exact step that was altered. IMPORTANT, honest limit: the hash chain ALONE does not
15
+ stop a thorough tamperer who recomputes the whole chain end-to-end (no link breaks then) -- so
16
+ integrity-only is enough only if the chain head is published/anchored somewhere the attacker
17
+ can't also rewrite. The SIGNATURE (layer 2) is what protects a self-held chain.
18
+
19
+ 2. ED25519 SIGNATURES (authenticity, needs `cryptography`). Each receipt's hash is signed with
20
+ the actor's private key. A third party verifies with the PUBLIC key only -> it proves *who*
21
+ produced the receipt and that the content wasn't forged, without anyone sharing a secret.
22
+ (Optional: if `cryptography` is not installed, the hash chain still works on its own.)
23
+
24
+ Privacy nod (the "zero-knowledge-ish" part): receipts store the SHA-256 of inputs/outputs, not the
25
+ raw content. You commit to *what* was processed; you reveal the content only if/when you choose, and
26
+ anyone can later check a revealed value against the committed hash. Real zero-knowledge proofs (ZK-
27
+ SNARKs) go further — proving a computation was correct without revealing inputs at all — and are the
28
+ heavier end of this same design space (see the README for the landscape and prior art).
29
+
30
+ Run the self-demo: python agent_receipts.py
31
+ MIT. Part of the Agora project. Honest scope: this is a reference PoC, not a hardened product.
32
+ """
33
+ from __future__ import annotations
34
+ import json
35
+ import time
36
+ import hashlib
37
+ from typing import Any, Optional
38
+
39
+ GENESIS = "0" * 64
40
+
41
+ # --- optional asymmetric signing (graceful if the lib is absent) ---
42
+ try:
43
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
44
+ Ed25519PrivateKey, Ed25519PublicKey,
45
+ )
46
+ from cryptography.hazmat.primitives import serialization
47
+ _HAVE_CRYPTO = True
48
+ except Exception: # pragma: no cover - exercised only on installs without `cryptography`
49
+ _HAVE_CRYPTO = False
50
+
51
+
52
+ def sha256_hex(data: bytes) -> str:
53
+ return hashlib.sha256(data).hexdigest()
54
+
55
+
56
+ def _canonical(obj: Any) -> bytes:
57
+ """Deterministic JSON so the same content always hashes the same (sorted keys, no spaces)."""
58
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
59
+
60
+
61
+ def hash_content(content: Any) -> str:
62
+ """Hash arbitrary input/output content. Bytes are hashed as-is; everything else is canonicalized."""
63
+ if isinstance(content, (bytes, bytearray)):
64
+ return sha256_hex(bytes(content))
65
+ return sha256_hex(_canonical(content))
66
+
67
+
68
+ def generate_keypair() -> tuple[str, str]:
69
+ """Return (private_key_hex, public_key_hex) for an Ed25519 actor identity."""
70
+ if not _HAVE_CRYPTO:
71
+ raise RuntimeError("signing requires the `cryptography` package (pip install cryptography)")
72
+ sk = Ed25519PrivateKey.generate()
73
+ sk_hex = sk.private_bytes(
74
+ serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()
75
+ ).hex()
76
+ pk_hex = sk.public_key().public_bytes(
77
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
78
+ ).hex()
79
+ return sk_hex, pk_hex
80
+
81
+
82
+ def _sign(private_key_hex: str, message: bytes) -> str:
83
+ sk = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex))
84
+ return sk.sign(message).hex()
85
+
86
+
87
+ def _verify_sig(public_key_hex: str, signature_hex: str, message: bytes) -> bool:
88
+ try:
89
+ pk = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex))
90
+ pk.verify(bytes.fromhex(signature_hex), message)
91
+ return True
92
+ except Exception:
93
+ return False
94
+
95
+
96
+ def receipt_hash(r: dict) -> str:
97
+ """The receipt's own hash commits to its content AND the previous receipt (the chain link)."""
98
+ core = {
99
+ "seq": r["seq"], "ts": r["ts"], "action": r["action"], "actor": r.get("actor"),
100
+ "input_sha256": r["input_sha256"], "output_sha256": r["output_sha256"],
101
+ "meta": r.get("meta"), "prev": r["prev"],
102
+ }
103
+ return sha256_hex(_canonical(core))
104
+
105
+
106
+ class ReceiptChain:
107
+ """An append-only, hash-chained, optionally-signed sequence of action receipts."""
108
+
109
+ def __init__(self, actor: Optional[str] = None, private_key_hex: Optional[str] = None,
110
+ public_key_hex: Optional[str] = None):
111
+ self.actor = actor
112
+ self._sk = private_key_hex
113
+ self.public_key_hex = public_key_hex
114
+ self.receipts: list[dict] = []
115
+
116
+ def record(self, action: str, inputs: Any, output: Any, meta: Optional[dict] = None,
117
+ ts: Optional[float] = None) -> dict:
118
+ """Record one action (e.g. an MCP tool call): commit to its input/output hashes, chain, sign."""
119
+ prev = self.receipts[-1]["hash"] if self.receipts else GENESIS
120
+ r = {
121
+ "seq": len(self.receipts),
122
+ "ts": ts if ts is not None else time.time(),
123
+ "action": action,
124
+ "actor": self.actor,
125
+ "input_sha256": hash_content(inputs),
126
+ "output_sha256": hash_content(output),
127
+ "meta": meta,
128
+ "prev": prev,
129
+ }
130
+ r["hash"] = receipt_hash(r)
131
+ if self._sk:
132
+ r["pubkey"] = self.public_key_hex
133
+ r["sig"] = _sign(self._sk, bytes.fromhex(r["hash"]))
134
+ self.receipts.append(r)
135
+ return r
136
+
137
+ def verify(self, expected_pubkey: Optional[str] = None) -> tuple[bool, list[str]]:
138
+ """Recompute the chain from scratch. Returns (ok, problems). Names the exact broken step."""
139
+ problems: list[str] = []
140
+ prev = GENESIS
141
+ for i, r in enumerate(self.receipts):
142
+ if r.get("seq") != i:
143
+ problems.append(f"seq {i}: out-of-order (claims seq={r.get('seq')})")
144
+ if r.get("prev") != prev:
145
+ problems.append(f"seq {i}: broken chain link (prev mismatch -> a prior receipt was altered/removed)")
146
+ if receipt_hash(r) != r.get("hash"):
147
+ problems.append(f"seq {i}: content tampered (hash does not match this receipt's fields)")
148
+ if "sig" in r:
149
+ pk = r.get("pubkey")
150
+ if expected_pubkey and pk != expected_pubkey:
151
+ problems.append(f"seq {i}: signed by an unexpected key (possible impersonation)")
152
+ if not _HAVE_CRYPTO:
153
+ problems.append(f"seq {i}: signature present but `cryptography` not installed to verify it")
154
+ elif not _verify_sig(pk, r["sig"], bytes.fromhex(r["hash"])):
155
+ problems.append(f"seq {i}: invalid signature (forged or wrong key)")
156
+ elif expected_pubkey:
157
+ problems.append(f"seq {i}: unsigned, but a signature was required")
158
+ prev = r.get("hash")
159
+ return (len(problems) == 0, problems)
160
+
161
+ def to_json(self) -> str:
162
+ return json.dumps(self.receipts, indent=2, ensure_ascii=False)
163
+
164
+ @classmethod
165
+ def from_receipts(cls, receipts: list[dict]) -> "ReceiptChain":
166
+ c = cls()
167
+ c.receipts = receipts
168
+ return c
169
+
170
+
171
+ def _demo() -> None:
172
+ print("=== agent-receipts: self-demo ===\n")
173
+ signed = _HAVE_CRYPTO
174
+ if signed:
175
+ sk, pk = generate_keypair()
176
+ chain = ReceiptChain(actor="research-agent-01", private_key_hex=sk, public_key_hex=pk)
177
+ print(f"actor identity (public key): {pk[:16]}... (third parties verify with this, no secret shared)\n")
178
+ else:
179
+ chain = ReceiptChain(actor="research-agent-01")
180
+ pk = None
181
+ print("(`cryptography` not installed -> hash-chain only, no signatures)\n")
182
+
183
+ # An agent does three MCP tool calls. Each gets a receipt.
184
+ chain.record("mcp.web_search", {"query": "supersession blind spot AUROC"},
185
+ {"results": 7, "top": "arXiv:2606.26511"})
186
+ chain.record("mcp.memory.write", {"fact": "Pro tier costs 39 USD/mo", "source": "billing:tool"},
187
+ {"stored": True, "id": "m-1042"})
188
+ chain.record("mcp.code.run", {"cmd": "python probe.py"}, {"exit": 0, "stdout_sha": "9f1c..."})
189
+ print(f"recorded {len(chain.receipts)} receipts (one per tool call)")
190
+
191
+ ok, problems = chain.verify(expected_pubkey=pk)
192
+ print(f"[1] honest chain verifies? {ok} {problems if problems else ''}")
193
+
194
+ # TAMPER: someone edits a past receipt's output AFTER the fact (e.g. to hide what really happened).
195
+ chain.receipts[1]["output_sha256"] = hash_content({"stored": True, "id": "m-DIFFERENT"})
196
+ ok2, problems2 = chain.verify(expected_pubkey=pk)
197
+ print(f"[2] after editing receipt #1's output: verifies? {ok2}")
198
+ for p in problems2:
199
+ print(f" - {p}")
200
+
201
+ # FORGE: rebuild #1 so its own hash matches the edit. The hash chain is now internally consistent,
202
+ # but the SIGNATURE was made over the original hash by the real key -> still caught (if signed).
203
+ chain.receipts[1]["hash"] = receipt_hash(chain.receipts[1])
204
+ ok3, problems3 = chain.verify(expected_pubkey=pk)
205
+ print(f"[3] after also recomputing the hash to match: verifies? {ok3}")
206
+ for p in problems3:
207
+ print(f" - {p}")
208
+ if signed:
209
+ print(" -> the signature (made by the real key over the ORIGINAL hash) exposes the forgery,")
210
+ print(" and #2's broken chain link points downstream. Integrity + authenticity together.")
211
+ else:
212
+ print(" -> this lazy edit broke a chain link, so it's caught. But WITHOUT signatures a")
213
+ print(" thorough tamperer would recompute the WHOLE chain (no link breaks) and it would")
214
+ print(" pass -- integrity-only needs an anchored/published head. Ed25519 signing closes this.")
215
+
216
+ print("\nMEASURED: an honest receipt chain verifies; a partial edit is detected at the exact step;")
217
+ print("a re-hashed forgery is caught by the SIGNATURE (a full recompute defeats the hash chain alone).")
218
+
219
+
220
+ if __name__ == "__main__":
221
+ _demo()