zambo-adk 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.
- zambo_adk-0.1.0/PKG-INFO +184 -0
- zambo_adk-0.1.0/README.md +168 -0
- zambo_adk-0.1.0/pyproject.toml +27 -0
- zambo_adk-0.1.0/setup.cfg +4 -0
- zambo_adk-0.1.0/tests/test_collector.py +173 -0
- zambo_adk-0.1.0/zambo_adk/__init__.py +51 -0
- zambo_adk-0.1.0/zambo_adk/collector.py +471 -0
- zambo_adk-0.1.0/zambo_adk/hooks.py +172 -0
- zambo_adk-0.1.0/zambo_adk/testing.py +77 -0
- zambo_adk-0.1.0/zambo_adk.egg-info/PKG-INFO +184 -0
- zambo_adk-0.1.0/zambo_adk.egg-info/SOURCES.txt +12 -0
- zambo_adk-0.1.0/zambo_adk.egg-info/dependency_links.txt +1 -0
- zambo_adk-0.1.0/zambo_adk.egg-info/requires.txt +1 -0
- zambo_adk-0.1.0/zambo_adk.egg-info/top_level.txt +1 -0
zambo_adk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zambo-adk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zambo verifiable execution receipts for Google ADK agents
|
|
5
|
+
Author: Brennan Zambo
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://zambo.dev
|
|
8
|
+
Keywords: zambo,aer-1,google-adk,ai-agents,verifiable-receipts,audit,tracing
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: google-adk>=1.0
|
|
16
|
+
|
|
17
|
+
# zambo-adk
|
|
18
|
+
|
|
19
|
+
Zambo verifiable execution receipts for [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) agents.
|
|
20
|
+
|
|
21
|
+
Attach receipt recording to any ADK agent and every tool execution
|
|
22
|
+
becomes a hash-chained step in a **verifiable workflow receipt**
|
|
23
|
+
(AER-1, Section 8): each step carries the tool name, arguments,
|
|
24
|
+
result, agent identity, and timestamps, with one Merkle root over the
|
|
25
|
+
whole run. The hooks only observe: every callback returns `None`, so
|
|
26
|
+
ADK executes tools exactly as it would without us. No network calls
|
|
27
|
+
until you call `mint()`.
|
|
28
|
+
|
|
29
|
+
Works with `LlmAgent` and any agent carrying the standard ADK tool
|
|
30
|
+
callbacks, via per-agent callbacks or a runner-wide plugin.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install zambo-adk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires `google-adk>=1.0` (installed automatically).
|
|
39
|
+
|
|
40
|
+
## Quickstart (zero API keys)
|
|
41
|
+
|
|
42
|
+
This runs a real ADK agent end to end with a scripted fake model, no
|
|
43
|
+
keys, no network, and ends at a live receipt URL on zambo.dev:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from google.adk.runners import Runner
|
|
48
|
+
from google.adk.sessions import InMemorySessionService
|
|
49
|
+
from google.genai import types as genai_types
|
|
50
|
+
|
|
51
|
+
from zambo_adk import AER1ADKCollector, attach_receipts
|
|
52
|
+
from zambo_adk.testing import scripted_agent
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def add(a: int, b: int) -> int:
|
|
56
|
+
"""Add two numbers."""
|
|
57
|
+
return a + b
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
collector = AER1ADKCollector(goal="Add 40 and 2")
|
|
62
|
+
|
|
63
|
+
agent = scripted_agent(
|
|
64
|
+
"calc", [add],
|
|
65
|
+
script=[("add", {"a": 40, "b": 2})],
|
|
66
|
+
final_text="42",
|
|
67
|
+
)
|
|
68
|
+
attach_receipts(agent, collector) # line 1: attach
|
|
69
|
+
|
|
70
|
+
session_service = InMemorySessionService()
|
|
71
|
+
session = await session_service.create_session(app_name="app", user_id="u1")
|
|
72
|
+
runner = Runner(agent=agent, app_name="app", session_service=session_service)
|
|
73
|
+
message = genai_types.Content(
|
|
74
|
+
parts=[genai_types.Part(text="Add 40 and 2")], role="user"
|
|
75
|
+
)
|
|
76
|
+
async for _event in runner.run_async(
|
|
77
|
+
user_id="u1", session_id=session.id, new_message=message
|
|
78
|
+
):
|
|
79
|
+
pass # line 2: run
|
|
80
|
+
|
|
81
|
+
receipt = collector.finalize(final_answer="42")
|
|
82
|
+
assert collector.verify(receipt) == [] # VALID
|
|
83
|
+
print(collector.mint(receipt)) # line 3: live receipt URL
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Output:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
recorded 1 tool call(s), receipt is VALID
|
|
92
|
+
live receipt: https://zambo.dev/run/6b3e89e9-1952-4850-aef4-a9534688637c
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Swap `scripted_agent` for your own `LlmAgent` with a real model and
|
|
96
|
+
the same three lines give you receipts on every tool call.
|
|
97
|
+
|
|
98
|
+
## Runner-wide plugin
|
|
99
|
+
|
|
100
|
+
To cover every agent under a Runner, including sub-agents, register
|
|
101
|
+
the plugin on the App instead of attaching per-agent callbacks:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from google.adk.apps import App
|
|
105
|
+
from zambo_adk import AER1ADKCollector, AER1ADKPlugin
|
|
106
|
+
|
|
107
|
+
collector = AER1ADKCollector(goal="Research the launch plan")
|
|
108
|
+
plugin = AER1ADKPlugin(collector)
|
|
109
|
+
app = App(name="my_app", root_agent=agent, plugins=[plugin])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Plugins run before agent-level callbacks, so the plugin also sees
|
|
113
|
+
tool calls that an agent callback short-circuits.
|
|
114
|
+
|
|
115
|
+
## What the receipt contains
|
|
116
|
+
|
|
117
|
+
Workflow level (AER-1 Section 8, Table 2):
|
|
118
|
+
|
|
119
|
+
- `type`, `version`, `workflow_id`, `receipt_id`, `session_id`
|
|
120
|
+
- `goal`, `status`
|
|
121
|
+
- `steps`: one record per tool execution, seq 1..n in order
|
|
122
|
+
- `merkle_root`: Section 8.1 root over the ordered step receipt ids
|
|
123
|
+
- `output_hash`: SHA-256 of the final answer
|
|
124
|
+
- `verify_url`: where the verification procedure is documented
|
|
125
|
+
|
|
126
|
+
Step level (AER-1 Section 8, Table 3), plus agent identity:
|
|
127
|
+
|
|
128
|
+
- `seq`, `receipt_id`, `tool`, `receipt_hash`, `started_at`, `ended_at`, `status`
|
|
129
|
+
- `agent`: the name of the ADK agent that acted (every step)
|
|
130
|
+
|
|
131
|
+
Each step `receipt_hash` is SHA-256 over the canonical JSON of the
|
|
132
|
+
tool name, arguments, result, and error flag. The hash commits to the
|
|
133
|
+
content; the receipt stays compact. Tool errors are recorded as steps
|
|
134
|
+
with `status: "error"` and the exception captured, so failures are
|
|
135
|
+
auditable too.
|
|
136
|
+
|
|
137
|
+
## Verification
|
|
138
|
+
|
|
139
|
+
`collector.verify(receipt)` runs the full offline check and returns a
|
|
140
|
+
list of failure reasons, empty when valid:
|
|
141
|
+
|
|
142
|
+
- all Table 2 / Table 3 members present and well-formed
|
|
143
|
+
- `seq` values exactly 1..n in order, no gaps
|
|
144
|
+
- no two steps share a `receipt_id`
|
|
145
|
+
- `merkle_root` matches the recomputed Section 8.1 root
|
|
146
|
+
- strict RFC 3339 timestamps, lowercase UUIDs, 64-char hex digests
|
|
147
|
+
|
|
148
|
+
Tamper with any field and verification fails. Try it:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
receipt["steps"][0]["tool"] = ""
|
|
152
|
+
assert collector.verify(receipt) != [] # fails, as it should
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Minting a live Zambo receipt
|
|
156
|
+
|
|
157
|
+
`collector.mint(receipt)` submits the finalized AER-1 receipt to
|
|
158
|
+
zambo.dev for verification and returns the live audit URL
|
|
159
|
+
(`https://zambo.dev/run/<receipt-id>`). It uses the free zambo.dev
|
|
160
|
+
MCP tier: no auth, no API keys. The mint itself is a Zambo tool call,
|
|
161
|
+
so it carries its own verifiable Zambo receipt.
|
|
162
|
+
|
|
163
|
+
## Notes
|
|
164
|
+
|
|
165
|
+
- `attach_receipts` preserves callbacks already on the agent: ours are
|
|
166
|
+
appended to the existing callback lists.
|
|
167
|
+
- The collector never touches the network and never changes what the
|
|
168
|
+
agents do until `mint()` is called. A recording failure can never
|
|
169
|
+
break a run.
|
|
170
|
+
- `session_id` defaults to a fresh UUID per collector; pass your own to
|
|
171
|
+
correlate receipts across runs.
|
|
172
|
+
- `ScriptedModel` / `scripted_agent` in `zambo_adk.testing` are test
|
|
173
|
+
helpers: scripted tool-call sequences with no model credentials.
|
|
174
|
+
- Verified against `google-adk` 2.11.0.
|
|
175
|
+
|
|
176
|
+
## Spec
|
|
177
|
+
|
|
178
|
+
AER-1: Agent Execution Receipts, `draft-zambo-aer1`
|
|
179
|
+
(an IETF draft):
|
|
180
|
+
https://datatracker.ietf.org/doc/draft-zambo-aer1/
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
Apache-2.0
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# zambo-adk
|
|
2
|
+
|
|
3
|
+
Zambo verifiable execution receipts for [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) agents.
|
|
4
|
+
|
|
5
|
+
Attach receipt recording to any ADK agent and every tool execution
|
|
6
|
+
becomes a hash-chained step in a **verifiable workflow receipt**
|
|
7
|
+
(AER-1, Section 8): each step carries the tool name, arguments,
|
|
8
|
+
result, agent identity, and timestamps, with one Merkle root over the
|
|
9
|
+
whole run. The hooks only observe: every callback returns `None`, so
|
|
10
|
+
ADK executes tools exactly as it would without us. No network calls
|
|
11
|
+
until you call `mint()`.
|
|
12
|
+
|
|
13
|
+
Works with `LlmAgent` and any agent carrying the standard ADK tool
|
|
14
|
+
callbacks, via per-agent callbacks or a runner-wide plugin.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install zambo-adk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires `google-adk>=1.0` (installed automatically).
|
|
23
|
+
|
|
24
|
+
## Quickstart (zero API keys)
|
|
25
|
+
|
|
26
|
+
This runs a real ADK agent end to end with a scripted fake model, no
|
|
27
|
+
keys, no network, and ends at a live receipt URL on zambo.dev:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from google.adk.runners import Runner
|
|
32
|
+
from google.adk.sessions import InMemorySessionService
|
|
33
|
+
from google.genai import types as genai_types
|
|
34
|
+
|
|
35
|
+
from zambo_adk import AER1ADKCollector, attach_receipts
|
|
36
|
+
from zambo_adk.testing import scripted_agent
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add(a: int, b: int) -> int:
|
|
40
|
+
"""Add two numbers."""
|
|
41
|
+
return a + b
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
collector = AER1ADKCollector(goal="Add 40 and 2")
|
|
46
|
+
|
|
47
|
+
agent = scripted_agent(
|
|
48
|
+
"calc", [add],
|
|
49
|
+
script=[("add", {"a": 40, "b": 2})],
|
|
50
|
+
final_text="42",
|
|
51
|
+
)
|
|
52
|
+
attach_receipts(agent, collector) # line 1: attach
|
|
53
|
+
|
|
54
|
+
session_service = InMemorySessionService()
|
|
55
|
+
session = await session_service.create_session(app_name="app", user_id="u1")
|
|
56
|
+
runner = Runner(agent=agent, app_name="app", session_service=session_service)
|
|
57
|
+
message = genai_types.Content(
|
|
58
|
+
parts=[genai_types.Part(text="Add 40 and 2")], role="user"
|
|
59
|
+
)
|
|
60
|
+
async for _event in runner.run_async(
|
|
61
|
+
user_id="u1", session_id=session.id, new_message=message
|
|
62
|
+
):
|
|
63
|
+
pass # line 2: run
|
|
64
|
+
|
|
65
|
+
receipt = collector.finalize(final_answer="42")
|
|
66
|
+
assert collector.verify(receipt) == [] # VALID
|
|
67
|
+
print(collector.mint(receipt)) # line 3: live receipt URL
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
recorded 1 tool call(s), receipt is VALID
|
|
76
|
+
live receipt: https://zambo.dev/run/6b3e89e9-1952-4850-aef4-a9534688637c
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Swap `scripted_agent` for your own `LlmAgent` with a real model and
|
|
80
|
+
the same three lines give you receipts on every tool call.
|
|
81
|
+
|
|
82
|
+
## Runner-wide plugin
|
|
83
|
+
|
|
84
|
+
To cover every agent under a Runner, including sub-agents, register
|
|
85
|
+
the plugin on the App instead of attaching per-agent callbacks:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from google.adk.apps import App
|
|
89
|
+
from zambo_adk import AER1ADKCollector, AER1ADKPlugin
|
|
90
|
+
|
|
91
|
+
collector = AER1ADKCollector(goal="Research the launch plan")
|
|
92
|
+
plugin = AER1ADKPlugin(collector)
|
|
93
|
+
app = App(name="my_app", root_agent=agent, plugins=[plugin])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Plugins run before agent-level callbacks, so the plugin also sees
|
|
97
|
+
tool calls that an agent callback short-circuits.
|
|
98
|
+
|
|
99
|
+
## What the receipt contains
|
|
100
|
+
|
|
101
|
+
Workflow level (AER-1 Section 8, Table 2):
|
|
102
|
+
|
|
103
|
+
- `type`, `version`, `workflow_id`, `receipt_id`, `session_id`
|
|
104
|
+
- `goal`, `status`
|
|
105
|
+
- `steps`: one record per tool execution, seq 1..n in order
|
|
106
|
+
- `merkle_root`: Section 8.1 root over the ordered step receipt ids
|
|
107
|
+
- `output_hash`: SHA-256 of the final answer
|
|
108
|
+
- `verify_url`: where the verification procedure is documented
|
|
109
|
+
|
|
110
|
+
Step level (AER-1 Section 8, Table 3), plus agent identity:
|
|
111
|
+
|
|
112
|
+
- `seq`, `receipt_id`, `tool`, `receipt_hash`, `started_at`, `ended_at`, `status`
|
|
113
|
+
- `agent`: the name of the ADK agent that acted (every step)
|
|
114
|
+
|
|
115
|
+
Each step `receipt_hash` is SHA-256 over the canonical JSON of the
|
|
116
|
+
tool name, arguments, result, and error flag. The hash commits to the
|
|
117
|
+
content; the receipt stays compact. Tool errors are recorded as steps
|
|
118
|
+
with `status: "error"` and the exception captured, so failures are
|
|
119
|
+
auditable too.
|
|
120
|
+
|
|
121
|
+
## Verification
|
|
122
|
+
|
|
123
|
+
`collector.verify(receipt)` runs the full offline check and returns a
|
|
124
|
+
list of failure reasons, empty when valid:
|
|
125
|
+
|
|
126
|
+
- all Table 2 / Table 3 members present and well-formed
|
|
127
|
+
- `seq` values exactly 1..n in order, no gaps
|
|
128
|
+
- no two steps share a `receipt_id`
|
|
129
|
+
- `merkle_root` matches the recomputed Section 8.1 root
|
|
130
|
+
- strict RFC 3339 timestamps, lowercase UUIDs, 64-char hex digests
|
|
131
|
+
|
|
132
|
+
Tamper with any field and verification fails. Try it:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
receipt["steps"][0]["tool"] = ""
|
|
136
|
+
assert collector.verify(receipt) != [] # fails, as it should
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Minting a live Zambo receipt
|
|
140
|
+
|
|
141
|
+
`collector.mint(receipt)` submits the finalized AER-1 receipt to
|
|
142
|
+
zambo.dev for verification and returns the live audit URL
|
|
143
|
+
(`https://zambo.dev/run/<receipt-id>`). It uses the free zambo.dev
|
|
144
|
+
MCP tier: no auth, no API keys. The mint itself is a Zambo tool call,
|
|
145
|
+
so it carries its own verifiable Zambo receipt.
|
|
146
|
+
|
|
147
|
+
## Notes
|
|
148
|
+
|
|
149
|
+
- `attach_receipts` preserves callbacks already on the agent: ours are
|
|
150
|
+
appended to the existing callback lists.
|
|
151
|
+
- The collector never touches the network and never changes what the
|
|
152
|
+
agents do until `mint()` is called. A recording failure can never
|
|
153
|
+
break a run.
|
|
154
|
+
- `session_id` defaults to a fresh UUID per collector; pass your own to
|
|
155
|
+
correlate receipts across runs.
|
|
156
|
+
- `ScriptedModel` / `scripted_agent` in `zambo_adk.testing` are test
|
|
157
|
+
helpers: scripted tool-call sequences with no model credentials.
|
|
158
|
+
- Verified against `google-adk` 2.11.0.
|
|
159
|
+
|
|
160
|
+
## Spec
|
|
161
|
+
|
|
162
|
+
AER-1: Agent Execution Receipts, `draft-zambo-aer1`
|
|
163
|
+
(an IETF draft):
|
|
164
|
+
https://datatracker.ietf.org/doc/draft-zambo-aer1/
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
Apache-2.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zambo-adk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zambo verifiable execution receipts for Google ADK agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Brennan Zambo" }]
|
|
13
|
+
dependencies = ["google-adk>=1.0"]
|
|
14
|
+
keywords = ["zambo", "aer-1", "google-adk", "ai-agents", "verifiable-receipts", "audit", "tracing"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://zambo.dev"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["zambo_adk*"]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for zambo-adk. No google-adk and no network required, except
|
|
2
|
+
test_mint which hits the live zambo.dev MCP (free tier, no auth)."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from zambo_adk import AER1ADKCollector, attach_receipts
|
|
9
|
+
from zambo_adk.collector import (
|
|
10
|
+
_mint_audit_url,
|
|
11
|
+
merkle_root,
|
|
12
|
+
verify_workflow_receipt,
|
|
13
|
+
)
|
|
14
|
+
from zambo_adk.hooks import _CallbackState
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeTool:
|
|
18
|
+
def __init__(self, name):
|
|
19
|
+
self.name = name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeContext:
|
|
23
|
+
def __init__(self, agent_name="test_agent"):
|
|
24
|
+
self.agent_name = agent_name
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FakeAgent:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.before_tool_callback = None
|
|
30
|
+
self.after_tool_callback = None
|
|
31
|
+
self.on_tool_error_callback = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_tool_through_callbacks(agent, tool_name, args, result):
|
|
35
|
+
tool = FakeTool(tool_name)
|
|
36
|
+
ctx = FakeContext()
|
|
37
|
+
for cb in agent.before_tool_callback:
|
|
38
|
+
assert cb(tool=tool, args=args, tool_context=ctx) is None
|
|
39
|
+
for cb in agent.after_tool_callback:
|
|
40
|
+
assert (
|
|
41
|
+
cb(tool=tool, args=args, tool_context=ctx, tool_response=result)
|
|
42
|
+
is None
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_tool_call_recorded_as_step():
|
|
47
|
+
collector = AER1ADKCollector(goal="add numbers")
|
|
48
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
49
|
+
run_tool_through_callbacks(
|
|
50
|
+
agent, "add", {"a": 2, "b": 3}, {"result": 5}
|
|
51
|
+
)
|
|
52
|
+
assert collector.step_count == 1
|
|
53
|
+
receipt = collector.finalize(final_answer="5")
|
|
54
|
+
assert collector.verify(receipt) == []
|
|
55
|
+
step = receipt["steps"][0]
|
|
56
|
+
assert step["tool"] == "add"
|
|
57
|
+
assert step["status"] == "ok"
|
|
58
|
+
assert step["agent"] == "test_agent"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_error_tool_call_recorded_as_error():
|
|
62
|
+
collector = AER1ADKCollector(goal="risky run")
|
|
63
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
64
|
+
tool = FakeTool("divide")
|
|
65
|
+
ctx = FakeContext()
|
|
66
|
+
for cb in agent.before_tool_callback:
|
|
67
|
+
cb(tool=tool, args={"a": 1, "b": 0}, tool_context=ctx)
|
|
68
|
+
boom = ZeroDivisionError("division by zero")
|
|
69
|
+
for cb in agent.on_tool_error_callback:
|
|
70
|
+
assert (
|
|
71
|
+
cb(tool=tool, args={}, tool_context=ctx, exception=boom) is None
|
|
72
|
+
)
|
|
73
|
+
receipt = collector.finalize()
|
|
74
|
+
assert receipt["status"] == "error"
|
|
75
|
+
assert receipt["steps"][0]["status"] == "error"
|
|
76
|
+
assert collector.verify(receipt) == []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_existing_callbacks_preserved():
|
|
80
|
+
seen = []
|
|
81
|
+
|
|
82
|
+
def existing_before(tool=None, args=None, tool_context=None):
|
|
83
|
+
seen.append(("before", getattr(tool, "name", None)))
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
collector = AER1ADKCollector(goal="preserve")
|
|
87
|
+
agent = FakeAgent()
|
|
88
|
+
agent.before_tool_callback = existing_before
|
|
89
|
+
attach_receipts(agent, collector)
|
|
90
|
+
run_tool_through_callbacks(agent, "search", {"q": "x"}, {"hits": []})
|
|
91
|
+
assert seen == [("before", "search")]
|
|
92
|
+
assert collector.step_count == 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_callbacks_never_short_circuit():
|
|
96
|
+
# Every zambo-adk callback must return None: a non-None return
|
|
97
|
+
# would make ADK skip the tool entirely.
|
|
98
|
+
collector = AER1ADKCollector(goal="passthrough")
|
|
99
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
100
|
+
tool, ctx = FakeTool("t"), FakeContext()
|
|
101
|
+
for cb in agent.before_tool_callback:
|
|
102
|
+
assert cb(tool=tool, args={}, tool_context=ctx) is None
|
|
103
|
+
for cb in agent.after_tool_callback:
|
|
104
|
+
assert cb(tool=tool, args={}, tool_context=ctx, tool_response={}) is None
|
|
105
|
+
for cb in agent.on_tool_error_callback:
|
|
106
|
+
assert (
|
|
107
|
+
cb(tool=tool, args={}, tool_context=ctx, exception=ValueError())
|
|
108
|
+
is None
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_receipt_hash_commits_to_result():
|
|
113
|
+
c1 = AER1ADKCollector(goal="g")
|
|
114
|
+
a1 = attach_receipts(FakeAgent(), c1)
|
|
115
|
+
run_tool_through_callbacks(a1, "add", {"a": 1}, {"result": 1})
|
|
116
|
+
c2 = AER1ADKCollector(goal="g")
|
|
117
|
+
a2 = attach_receipts(FakeAgent(), c2)
|
|
118
|
+
run_tool_through_callbacks(a2, "add", {"a": 1}, {"result": 999})
|
|
119
|
+
h1 = c1.finalize()["steps"][0]["receipt_hash"]
|
|
120
|
+
h2 = c2.finalize()["steps"][0]["receipt_hash"]
|
|
121
|
+
assert h1 != h2
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_tampered_receipt_fails_verification():
|
|
125
|
+
collector = AER1ADKCollector(goal="g")
|
|
126
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
127
|
+
run_tool_through_callbacks(agent, "add", {"a": 1}, {"result": 1})
|
|
128
|
+
receipt = collector.finalize()
|
|
129
|
+
receipt["steps"][0]["tool"] = ""
|
|
130
|
+
assert collector.verify(receipt) != []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_merkle_root_matches_recomputed():
|
|
134
|
+
collector = AER1ADKCollector(goal="g")
|
|
135
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
136
|
+
for i in range(3):
|
|
137
|
+
run_tool_through_callbacks(agent, f"tool_{i}", {}, {"i": i})
|
|
138
|
+
receipt = collector.finalize()
|
|
139
|
+
assert receipt["merkle_root"] == merkle_root(
|
|
140
|
+
[s["receipt_id"] for s in receipt["steps"]]
|
|
141
|
+
)
|
|
142
|
+
assert verify_workflow_receipt(receipt) == []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_seq_ordering_and_agent_identity():
|
|
146
|
+
collector = AER1ADKCollector(goal="g")
|
|
147
|
+
state = _CallbackState(collector)
|
|
148
|
+
for i in range(2):
|
|
149
|
+
ctx = FakeContext(agent_name=f"agent_{i}")
|
|
150
|
+
state.begin(FakeTool("t"), {}, ctx)
|
|
151
|
+
state.end(ctx, result={"i": i})
|
|
152
|
+
receipt = collector.finalize()
|
|
153
|
+
assert [s["seq"] for s in receipt["steps"]] == [1, 2]
|
|
154
|
+
assert [s["agent"] for s in receipt["steps"]] == ["agent_0", "agent_1"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_receipt_serializes_to_json():
|
|
158
|
+
collector = AER1ADKCollector(goal="g")
|
|
159
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
160
|
+
run_tool_through_callbacks(agent, "add", {"a": 1}, {"result": 1})
|
|
161
|
+
text = collector.to_json()
|
|
162
|
+
assert json.loads(text)["type"] == "verifiable-workflow-receipt"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_mint_returns_live_audit_url():
|
|
166
|
+
collector = AER1ADKCollector(goal="mint test")
|
|
167
|
+
agent = attach_receipts(FakeAgent(), collector)
|
|
168
|
+
run_tool_through_callbacks(agent, "add", {"a": 40, "b": 2}, {"result": 42})
|
|
169
|
+
receipt = collector.finalize(final_answer="42")
|
|
170
|
+
assert collector.verify(receipt) == []
|
|
171
|
+
url = collector.mint(receipt)
|
|
172
|
+
assert url.startswith("https://zambo.dev/run/")
|
|
173
|
+
print(f"\nlive audit url: {url}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""zambo-adk: Zambo verifiable execution receipts for Google ADK agents.
|
|
2
|
+
|
|
3
|
+
Attach AER-1 receipt recording to any Google Agent Development Kit
|
|
4
|
+
agent. Every tool execution becomes a hash-chained step in a
|
|
5
|
+
verifiable workflow receipt (AER-1, Section 8); finalize() emits the
|
|
6
|
+
receipt, verify() checks it offline, and mint() submits it to
|
|
7
|
+
zambo.dev for a live audit URL.
|
|
8
|
+
|
|
9
|
+
Two integration routes:
|
|
10
|
+
|
|
11
|
+
from zambo_adk import AER1ADKCollector, attach_receipts
|
|
12
|
+
|
|
13
|
+
collector = AER1ADKCollector(goal="Research the launch plan")
|
|
14
|
+
agent = attach_receipts(my_adk_agent, collector)
|
|
15
|
+
# ... run the agent ...
|
|
16
|
+
receipt = collector.finalize(final_answer="done")
|
|
17
|
+
assert collector.verify(receipt) == [] # VALID
|
|
18
|
+
print(collector.mint(receipt)) # live audit URL
|
|
19
|
+
|
|
20
|
+
Or runner-wide, covering every agent under the Runner:
|
|
21
|
+
|
|
22
|
+
from zambo_adk import AER1ADKPlugin
|
|
23
|
+
app = App(name="my_app", root_agent=agent,
|
|
24
|
+
plugins=[AER1ADKPlugin(collector)])
|
|
25
|
+
|
|
26
|
+
The hooks only observe: every callback returns None, so ADK executes
|
|
27
|
+
tools exactly as it would without us. No network calls until you call
|
|
28
|
+
mint(). A recording failure can never break a run.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .collector import (
|
|
32
|
+
AER1ADKCollector,
|
|
33
|
+
ZAMBO_MCP_URL,
|
|
34
|
+
merkle_root,
|
|
35
|
+
verify_workflow_receipt,
|
|
36
|
+
)
|
|
37
|
+
from .hooks import AER1ADKPlugin, attach_receipts
|
|
38
|
+
from .testing import ScriptedModel, scripted_agent
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AER1ADKCollector",
|
|
42
|
+
"AER1ADKPlugin",
|
|
43
|
+
"attach_receipts",
|
|
44
|
+
"ScriptedModel",
|
|
45
|
+
"scripted_agent",
|
|
46
|
+
"ZAMBO_MCP_URL",
|
|
47
|
+
"merkle_root",
|
|
48
|
+
"verify_workflow_receipt",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|