aer1kit 0.1.0__py3-none-any.whl
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.
- aer1kit/__init__.py +19 -0
- aer1kit/emitter.py +397 -0
- aer1kit-0.1.0.dist-info/METADATA +105 -0
- aer1kit-0.1.0.dist-info/RECORD +6 -0
- aer1kit-0.1.0.dist-info/WHEEL +5 -0
- aer1kit-0.1.0.dist-info/top_level.txt +1 -0
aer1kit/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""aer1kit: verifiable execution receipts for hackathon agents."""
|
|
2
|
+
|
|
3
|
+
from .emitter import (
|
|
4
|
+
ReceiptEmitter,
|
|
5
|
+
merkle_root,
|
|
6
|
+
verify_receipt,
|
|
7
|
+
ZAMBO_MCP_URL,
|
|
8
|
+
ZAMBO_VERIFY_URL,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ReceiptEmitter",
|
|
13
|
+
"merkle_root",
|
|
14
|
+
"verify_receipt",
|
|
15
|
+
"ZAMBO_MCP_URL",
|
|
16
|
+
"ZAMBO_VERIFY_URL",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
aer1kit/emitter.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""AER-1 receipt emitter for hackathon builders.
|
|
2
|
+
|
|
3
|
+
Three lines and your agent emits verifiable execution receipts:
|
|
4
|
+
|
|
5
|
+
from aer1kit import ReceiptEmitter
|
|
6
|
+
emitter = ReceiptEmitter(goal="answer the user's question")
|
|
7
|
+
emitter.record("web_search", {"query": "AER-1", "results": 3})
|
|
8
|
+
url = emitter.mint() # live verifiable receipt on zambo.dev
|
|
9
|
+
|
|
10
|
+
No API keys. No signup. The basic flow is fully offline; mint() submits
|
|
11
|
+
the receipt to the free zambo.dev verifier and returns a shareable URL.
|
|
12
|
+
|
|
13
|
+
AER-1 is an IETF draft (draft-zambo-aer1). This emitter implements the
|
|
14
|
+
Section 8 workflow receipt with the Section 8.1 Merkle construction.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import time
|
|
23
|
+
import uuid
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
|
|
26
|
+
WORKFLOW_TYPE = "verifiable-workflow-receipt"
|
|
27
|
+
WORKFLOW_VERSION = "1"
|
|
28
|
+
|
|
29
|
+
ZAMBO_MCP_URL = "https://zambo.dev/mcp"
|
|
30
|
+
ZAMBO_VERIFY_URL = "https://zambo.dev/verify"
|
|
31
|
+
|
|
32
|
+
_UUID_RE = re.compile(
|
|
33
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z"
|
|
34
|
+
)
|
|
35
|
+
_RFC3339_RE = re.compile(
|
|
36
|
+
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"
|
|
37
|
+
r"(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})\Z"
|
|
38
|
+
)
|
|
39
|
+
_HEX64_RE = re.compile(r"^[0-9a-f]{64}\Z")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _new_uuid() -> str:
|
|
43
|
+
return str(uuid.uuid4())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _rfc3339(ts: float) -> str:
|
|
47
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _json_safe(value):
|
|
51
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
52
|
+
return value
|
|
53
|
+
if isinstance(value, dict):
|
|
54
|
+
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
55
|
+
if isinstance(value, (list, tuple)):
|
|
56
|
+
return [_json_safe(v) for v in value]
|
|
57
|
+
if isinstance(value, set):
|
|
58
|
+
return sorted((_json_safe(v) for v in value), key=repr)
|
|
59
|
+
return str(value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _canonical(obj) -> bytes:
|
|
63
|
+
return json.dumps(
|
|
64
|
+
_json_safe(obj),
|
|
65
|
+
sort_keys=True,
|
|
66
|
+
separators=(",", ":"),
|
|
67
|
+
ensure_ascii=False,
|
|
68
|
+
).encode("utf-8")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _sha256_hex(data: bytes) -> str:
|
|
72
|
+
return hashlib.sha256(data).hexdigest()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def merkle_root(receipt_ids: list) -> str:
|
|
76
|
+
"""AER-1 Section 8.1 normative Merkle construction.
|
|
77
|
+
|
|
78
|
+
leaf = SHA-256 over the UTF-8 bytes of receipt_id; internal node =
|
|
79
|
+
SHA-256 over raw 32-byte left || raw 32-byte right; an odd trailing
|
|
80
|
+
node is duplicated.
|
|
81
|
+
"""
|
|
82
|
+
if not receipt_ids:
|
|
83
|
+
return hashlib.sha256(b"").hexdigest()
|
|
84
|
+
level = [hashlib.sha256(rid.encode("utf-8")).digest() for rid in receipt_ids]
|
|
85
|
+
while len(level) > 1:
|
|
86
|
+
if len(level) % 2:
|
|
87
|
+
level.append(level[-1])
|
|
88
|
+
level = [
|
|
89
|
+
hashlib.sha256(level[i] + level[i + 1]).digest()
|
|
90
|
+
for i in range(0, len(level), 2)
|
|
91
|
+
]
|
|
92
|
+
return level[0].hex()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def verify_receipt(workflow) -> list:
|
|
96
|
+
"""Offline-verify an AER-1 workflow receipt.
|
|
97
|
+
|
|
98
|
+
Returns a list of failure reasons; [] means the receipt is valid.
|
|
99
|
+
"""
|
|
100
|
+
failures = []
|
|
101
|
+
if not isinstance(workflow, dict):
|
|
102
|
+
return ["workflow is not a JSON object"]
|
|
103
|
+
if workflow.get("type") != WORKFLOW_TYPE:
|
|
104
|
+
failures.append('workflow type is not "verifiable-workflow-receipt"')
|
|
105
|
+
version = workflow.get("version")
|
|
106
|
+
if not isinstance(version, str) or not version:
|
|
107
|
+
failures.append("workflow version is not a non-empty string")
|
|
108
|
+
for field in ("workflow_id", "receipt_id"):
|
|
109
|
+
if not isinstance(workflow.get(field), str) or not _UUID_RE.match(
|
|
110
|
+
workflow.get(field)
|
|
111
|
+
):
|
|
112
|
+
failures.append(f"workflow {field} is not a lowercase UUID")
|
|
113
|
+
if not isinstance(workflow.get("session_id"), str) or not workflow.get("session_id"):
|
|
114
|
+
failures.append("workflow session_id is not a non-empty string")
|
|
115
|
+
if not isinstance(workflow.get("goal"), str) or not workflow.get("goal"):
|
|
116
|
+
failures.append("workflow goal is not a non-empty string")
|
|
117
|
+
if not isinstance(workflow.get("status"), str) or not workflow.get("status"):
|
|
118
|
+
failures.append("workflow status is not a non-empty string")
|
|
119
|
+
oh = workflow.get("output_hash")
|
|
120
|
+
if not isinstance(oh, str) or not _HEX64_RE.match(oh):
|
|
121
|
+
failures.append("workflow output_hash is not a 64-char lowercase hex digest")
|
|
122
|
+
vu = workflow.get("verify_url")
|
|
123
|
+
if not isinstance(vu, str) or not (
|
|
124
|
+
vu.startswith("http://") or vu.startswith("https://")
|
|
125
|
+
):
|
|
126
|
+
failures.append("workflow verify_url is not an http(s) URL string")
|
|
127
|
+
steps = workflow.get("steps")
|
|
128
|
+
if not isinstance(steps, list) or not steps:
|
|
129
|
+
failures.append("workflow steps is not a non-empty list")
|
|
130
|
+
return failures
|
|
131
|
+
for i, step in enumerate(steps):
|
|
132
|
+
if not isinstance(step, dict):
|
|
133
|
+
failures.append(f"step {i + 1} is not an object")
|
|
134
|
+
continue
|
|
135
|
+
seq = step.get("seq")
|
|
136
|
+
if isinstance(seq, bool) or not (
|
|
137
|
+
isinstance(seq, int) or (isinstance(seq, float) and seq.is_integer())
|
|
138
|
+
):
|
|
139
|
+
failures.append(f"step {i + 1} seq is not an integer")
|
|
140
|
+
if not isinstance(step.get("receipt_id"), str):
|
|
141
|
+
failures.append(f"step {i + 1} receipt_id is not a string")
|
|
142
|
+
if not isinstance(step.get("tool"), str) or not step.get("tool"):
|
|
143
|
+
failures.append(f"step {i + 1} tool is not a non-empty string")
|
|
144
|
+
rh = step.get("receipt_hash")
|
|
145
|
+
if not isinstance(rh, str) or not _HEX64_RE.match(rh):
|
|
146
|
+
failures.append(
|
|
147
|
+
f"step {i + 1} receipt_hash is not a 64-char lowercase hex digest"
|
|
148
|
+
)
|
|
149
|
+
for ts_field in ("started_at", "ended_at"):
|
|
150
|
+
if not isinstance(step.get(ts_field), str) or not _RFC3339_RE.match(
|
|
151
|
+
step.get(ts_field)
|
|
152
|
+
):
|
|
153
|
+
failures.append(
|
|
154
|
+
f"step {i + 1} {ts_field} is not a valid RFC 3339 timestamp"
|
|
155
|
+
)
|
|
156
|
+
if not isinstance(step.get("status"), str) or not step.get("status"):
|
|
157
|
+
failures.append(f"step {i + 1} status is not a non-empty string")
|
|
158
|
+
if failures:
|
|
159
|
+
return failures
|
|
160
|
+
n = len(steps)
|
|
161
|
+
for i, step in enumerate(steps):
|
|
162
|
+
if step["seq"] != i + 1:
|
|
163
|
+
failures.append(
|
|
164
|
+
f"step seq values are not 1..{n} in order "
|
|
165
|
+
f"(index {i} carries seq {step['seq']})"
|
|
166
|
+
)
|
|
167
|
+
break
|
|
168
|
+
seen = set()
|
|
169
|
+
for step in steps:
|
|
170
|
+
rid = step["receipt_id"]
|
|
171
|
+
if rid in seen:
|
|
172
|
+
failures.append(f"duplicate receipt_id: {rid}")
|
|
173
|
+
break
|
|
174
|
+
seen.add(rid)
|
|
175
|
+
root = merkle_root([step["receipt_id"] for step in steps])
|
|
176
|
+
mr = workflow.get("merkle_root")
|
|
177
|
+
if not isinstance(mr, str) or mr != root:
|
|
178
|
+
failures.append("merkle_root does not match the recomputed Section 8.1 root")
|
|
179
|
+
return failures
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _mint_audit_url(workflow, timeout=60) -> str:
|
|
183
|
+
"""Submit a finalized AER-1 receipt to zambo.dev and return the live URL.
|
|
184
|
+
|
|
185
|
+
Free tier, no auth, no API keys. Every submission mints a verifiable
|
|
186
|
+
receipt at https://zambo.dev/run/<id>.
|
|
187
|
+
"""
|
|
188
|
+
import urllib.error
|
|
189
|
+
import urllib.request
|
|
190
|
+
|
|
191
|
+
body = json.dumps(
|
|
192
|
+
{
|
|
193
|
+
"jsonrpc": "2.0",
|
|
194
|
+
"id": 1,
|
|
195
|
+
"method": "tools/call",
|
|
196
|
+
"params": {
|
|
197
|
+
"name": "zambo_check",
|
|
198
|
+
"arguments": {
|
|
199
|
+
"check": (
|
|
200
|
+
"Verify this AER-1 verifiable workflow receipt: "
|
|
201
|
+
"confirm the step sequence, receipt hashes, and "
|
|
202
|
+
"Merkle root are well-formed."
|
|
203
|
+
),
|
|
204
|
+
"context": json.dumps(
|
|
205
|
+
_json_safe(workflow), ensure_ascii=False
|
|
206
|
+
),
|
|
207
|
+
"type": "execution",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
).encode("utf-8")
|
|
212
|
+
last_error = None
|
|
213
|
+
for attempt in range(3):
|
|
214
|
+
try:
|
|
215
|
+
req = urllib.request.Request(
|
|
216
|
+
ZAMBO_MCP_URL,
|
|
217
|
+
data=body,
|
|
218
|
+
headers={
|
|
219
|
+
"Content-Type": "application/json",
|
|
220
|
+
"Accept": "application/json, text/event-stream",
|
|
221
|
+
},
|
|
222
|
+
method="POST",
|
|
223
|
+
)
|
|
224
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
225
|
+
raw = resp.read().decode("utf-8")
|
|
226
|
+
break
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
last_error = exc
|
|
229
|
+
time.sleep(0.6 * (attempt + 1))
|
|
230
|
+
else:
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
f"zambo.dev mint failed after 3 attempts: {last_error}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
payload = None
|
|
236
|
+
text = raw.strip()
|
|
237
|
+
if text.startswith("{"):
|
|
238
|
+
payload = json.loads(text)
|
|
239
|
+
else:
|
|
240
|
+
for line in text.splitlines():
|
|
241
|
+
if line.startswith("data:"):
|
|
242
|
+
try:
|
|
243
|
+
payload = json.loads(line[5:].strip())
|
|
244
|
+
except ValueError:
|
|
245
|
+
continue
|
|
246
|
+
if payload:
|
|
247
|
+
break
|
|
248
|
+
if not payload:
|
|
249
|
+
raise RuntimeError("zambo.dev mint returned an unreadable response")
|
|
250
|
+
if "error" in payload and payload["error"]:
|
|
251
|
+
raise RuntimeError(f"zambo.dev mint error: {payload['error']}")
|
|
252
|
+
result = payload.get("result") or {}
|
|
253
|
+
receipt = result.get("_receipt") or {}
|
|
254
|
+
audit = receipt.get("audit") or (
|
|
255
|
+
f"https://zambo.dev/run/{receipt['id']}" if receipt.get("id") else None
|
|
256
|
+
)
|
|
257
|
+
if not audit:
|
|
258
|
+
raise RuntimeError("zambo.dev mint returned no receipt id")
|
|
259
|
+
return audit
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ReceiptEmitter:
|
|
263
|
+
"""Emit AER-1 verifiable execution receipts for any agent.
|
|
264
|
+
|
|
265
|
+
Framework-independent: works with LangChain, CrewAI, raw API calls,
|
|
266
|
+
or a hand-rolled agent loop. Record one step per action your agent
|
|
267
|
+
takes; finalize when the run ends.
|
|
268
|
+
|
|
269
|
+
emitter = ReceiptEmitter(goal="book a flight to Austin")
|
|
270
|
+
emitter.record("search_flights", {"origin": "JFK", "dest": "AUS"})
|
|
271
|
+
emitter.record("book_flight", {"flight": "AA123", "price_usd": 249})
|
|
272
|
+
url = emitter.mint() # live verifiable receipt
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, goal=None, session_id=None, verify_url=None):
|
|
276
|
+
self._goal = goal
|
|
277
|
+
self._session_id = session_id or _new_uuid()
|
|
278
|
+
self._verify_url = verify_url or ZAMBO_VERIFY_URL
|
|
279
|
+
self._steps = []
|
|
280
|
+
self._seq = 0
|
|
281
|
+
|
|
282
|
+
def record(self, tool, evidence, status="ok", started=None, ended=None, **extra):
|
|
283
|
+
"""Record one receipt step for an action the agent took.
|
|
284
|
+
|
|
285
|
+
tool: short label for the action (e.g. "web_search", "send_email").
|
|
286
|
+
evidence: dict of what the action observed or produced; the
|
|
287
|
+
receipt_hash commits to its canonical JSON, so anyone can
|
|
288
|
+
check later that the evidence has not changed.
|
|
289
|
+
status: "ok", "error", or "skipped".
|
|
290
|
+
extra: informational members, e.g. agent="planner", model="gpt-4o".
|
|
291
|
+
None values are dropped.
|
|
292
|
+
"""
|
|
293
|
+
self._seq += 1
|
|
294
|
+
now = time.time()
|
|
295
|
+
if started is None:
|
|
296
|
+
started = now
|
|
297
|
+
if ended is None or ended < started:
|
|
298
|
+
ended = started
|
|
299
|
+
step = {
|
|
300
|
+
"seq": self._seq,
|
|
301
|
+
"receipt_id": _new_uuid(),
|
|
302
|
+
"tool": str(tool),
|
|
303
|
+
"receipt_hash": _sha256_hex(_canonical(evidence)),
|
|
304
|
+
"started_at": _rfc3339(started),
|
|
305
|
+
"ended_at": _rfc3339(ended),
|
|
306
|
+
"status": status,
|
|
307
|
+
}
|
|
308
|
+
for key, value in extra.items():
|
|
309
|
+
if value is not None:
|
|
310
|
+
step[key] = _json_safe(value)
|
|
311
|
+
self._steps.append(step)
|
|
312
|
+
return step
|
|
313
|
+
|
|
314
|
+
def finalize(self, final_answer=None) -> dict:
|
|
315
|
+
"""Build the AER-1 verifiable workflow receipt for the run."""
|
|
316
|
+
status = "error" if any(s["status"] == "error" for s in self._steps) else "ok"
|
|
317
|
+
answer_text = "" if final_answer is None else str(final_answer)
|
|
318
|
+
return {
|
|
319
|
+
"type": WORKFLOW_TYPE,
|
|
320
|
+
"version": WORKFLOW_VERSION,
|
|
321
|
+
"workflow_id": _new_uuid(),
|
|
322
|
+
"receipt_id": _new_uuid(),
|
|
323
|
+
"session_id": self._session_id,
|
|
324
|
+
"goal": self._goal or "agent run",
|
|
325
|
+
"status": status,
|
|
326
|
+
"steps": list(self._steps),
|
|
327
|
+
"merkle_root": merkle_root([s["receipt_id"] for s in self._steps]),
|
|
328
|
+
"output_hash": _sha256_hex(answer_text.encode("utf-8")),
|
|
329
|
+
"verify_url": self._verify_url,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
def verify(self, workflow=None) -> list:
|
|
333
|
+
"""Offline-verify a receipt. [] means valid."""
|
|
334
|
+
if workflow is None:
|
|
335
|
+
workflow = self.finalize()
|
|
336
|
+
return verify_receipt(workflow)
|
|
337
|
+
|
|
338
|
+
def mint(self, final_answer=None) -> str:
|
|
339
|
+
"""Finalize the run and mint a LIVE verifiable receipt on zambo.dev.
|
|
340
|
+
|
|
341
|
+
Returns the shareable audit URL (https://zambo.dev/run/<id>).
|
|
342
|
+
Free tier, no API key, no signup.
|
|
343
|
+
"""
|
|
344
|
+
return _mint_audit_url(self.finalize(final_answer=final_answer))
|
|
345
|
+
|
|
346
|
+
def explain(self, final_answer=None) -> str:
|
|
347
|
+
"""Render a human-readable "why" summary of the run.
|
|
348
|
+
|
|
349
|
+
Built for Track 02 ("The Agent That Can Explain Why"): every
|
|
350
|
+
step becomes one line of what the agent did and what evidence
|
|
351
|
+
it acted on, ending with the verification status.
|
|
352
|
+
"""
|
|
353
|
+
workflow = self.finalize(final_answer=final_answer)
|
|
354
|
+
problems = verify_receipt(workflow)
|
|
355
|
+
lines = [
|
|
356
|
+
f"Goal: {workflow['goal']}",
|
|
357
|
+
f"Steps: {len(workflow['steps'])} (status: {workflow['status']})",
|
|
358
|
+
"",
|
|
359
|
+
]
|
|
360
|
+
for step in workflow["steps"]:
|
|
361
|
+
lines.append(
|
|
362
|
+
f"{step['seq']}. {step['tool']} [{step['status']}] "
|
|
363
|
+
f"evidence sha256:{step['receipt_hash'][:12]}..."
|
|
364
|
+
)
|
|
365
|
+
lines.append("")
|
|
366
|
+
lines.append(f"Merkle root: {workflow['merkle_root'][:16]}...")
|
|
367
|
+
if problems:
|
|
368
|
+
lines.append("Verification: FAILED")
|
|
369
|
+
lines.extend(f" - {p}" for p in problems)
|
|
370
|
+
else:
|
|
371
|
+
lines.append("Verification: VALID (offline check passed)")
|
|
372
|
+
lines.append(f"Check it live: {self._verify_url}")
|
|
373
|
+
return "\n".join(lines)
|
|
374
|
+
|
|
375
|
+
def to_json(self, final_answer=None, indent=2) -> str:
|
|
376
|
+
return json.dumps(
|
|
377
|
+
self.finalize(final_answer=final_answer),
|
|
378
|
+
indent=indent,
|
|
379
|
+
ensure_ascii=False,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def save(self, path, final_answer=None) -> str:
|
|
383
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
384
|
+
f.write(self.to_json(final_answer=final_answer))
|
|
385
|
+
return path
|
|
386
|
+
|
|
387
|
+
def reset(self):
|
|
388
|
+
self._steps = []
|
|
389
|
+
self._seq = 0
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def step_count(self) -> int:
|
|
393
|
+
return len(self._steps)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def session_id(self) -> str:
|
|
397
|
+
return self._session_id
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aer1kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verifiable AER-1 execution receipts for hackathon agents. Three lines and your agent can explain why.
|
|
5
|
+
Author: Brennan Zambo
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://zambo.dev
|
|
8
|
+
Project-URL: Live verifier, https://zambo.dev/verify
|
|
9
|
+
Project-URL: Live demo, https://zambo.dev/demo
|
|
10
|
+
Project-URL: IETF Draft, https://datatracker.ietf.org/doc/draft-zambo-aer1/
|
|
11
|
+
Keywords: aer-1,receipts,agents,hackathon,verifiable,mcp
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# aer1kit: verifiable execution receipts for your hackathon agent
|
|
19
|
+
|
|
20
|
+
Built for the [Open Agent Hackathon 2026](https://hackathon.genai.works/event/open-agent-hackathon-2026), Track 02: **"The Agent That Can Explain Why."**
|
|
21
|
+
|
|
22
|
+
Judges ask "why did your agent do that?" Most teams answer with a story. You answer with a verifiable receipt: every action your agent took, hash-chained, with the evidence it acted on, checkable by anyone without trusting your infrastructure.
|
|
23
|
+
|
|
24
|
+
## What is AER-1?
|
|
25
|
+
|
|
26
|
+
AER-1 (Agent Execution Receipts) is an open IETF draft (`draft-zambo-aer1`) that defines a verifiable receipt format for agent tool calls. Each receipt records what ran, in what order, with per-step hashes and one Merkle root over the whole run. Anyone can verify it offline, or check it live at [zambo.dev/verify](https://zambo.dev/verify).
|
|
27
|
+
|
|
28
|
+
## Why this wins Track 02
|
|
29
|
+
|
|
30
|
+
Track 02 rewards agents that can explain themselves. A trace says "the model claims this ran." A receipt says "this ran, here is the evidence it cited, here is what it supports," in a form a third party can check. That is the whole track, and this kit gives it to you in three lines.
|
|
31
|
+
|
|
32
|
+
## Quickstart (3 steps)
|
|
33
|
+
|
|
34
|
+
**1. Clone and install (no dependencies, no API keys):**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone <this-repo>
|
|
38
|
+
cd aer1-hackathon-kit
|
|
39
|
+
pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**2. Add three lines to your agent:**
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from aer1kit import ReceiptEmitter
|
|
46
|
+
|
|
47
|
+
emitter = ReceiptEmitter(goal="what my agent is trying to do")
|
|
48
|
+
|
|
49
|
+
# after every action your agent takes:
|
|
50
|
+
emitter.record("web_search", {"query": "...", "results": [...]})
|
|
51
|
+
emitter.record("send_email", {"to": "...", "subject": "..."})
|
|
52
|
+
|
|
53
|
+
# at the end of the run:
|
|
54
|
+
url = emitter.mint() # live verifiable receipt, free, no signup
|
|
55
|
+
print(url) # https://zambo.dev/run/<id> - share it with the judges
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**3. See it work:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python demo.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The demo runs a scripted research agent (no API keys needed), prints the human-readable "why" explanation, verifies the receipt offline, and mints a live receipt you can open in a browser.
|
|
65
|
+
|
|
66
|
+
## What `record()` captures
|
|
67
|
+
|
|
68
|
+
Each call records one receipt step:
|
|
69
|
+
|
|
70
|
+
- `tool`: short label for the action (`web_search`, `book_flight`, ...)
|
|
71
|
+
- `evidence`: dict of what the action observed or produced. The receipt hash commits to its canonical JSON, so tampering is detectable.
|
|
72
|
+
- `status`: `ok`, `error`, or `skipped`
|
|
73
|
+
- extra kwargs become informational fields: `agent="planner"`, `model="gpt-4o"`, ...
|
|
74
|
+
|
|
75
|
+
## The "explain why" helper
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
print(emitter.explain(final_answer="..."))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Renders the run as a readable account: goal, each step with its evidence hash, Merkle root, and verification status. Paste it into your demo or your submission writeup.
|
|
82
|
+
|
|
83
|
+
## Verifying receipts
|
|
84
|
+
|
|
85
|
+
- **Offline:** `emitter.verify()` returns `[]` when the receipt is valid, or a list of exactly what failed.
|
|
86
|
+
- **Live:** paste any receipt JSON at [zambo.dev/verify](https://zambo.dev/verify).
|
|
87
|
+
- **Try it now:** run any call at [zambo.dev/demo](https://zambo.dev/demo) and inspect the receipt it produces.
|
|
88
|
+
|
|
89
|
+
## Files
|
|
90
|
+
|
|
91
|
+
- `aer1kit/emitter.py` - the whole kit. Stdlib only, no dependencies.
|
|
92
|
+
- `examples/explain_why_agent.py` - a complete Track 02 example agent.
|
|
93
|
+
- `demo.py` - one-command demo ending in a live receipt URL.
|
|
94
|
+
- `tests/` - offline verification tests.
|
|
95
|
+
|
|
96
|
+
## Links
|
|
97
|
+
|
|
98
|
+
- Hackathon: https://hackathon.genai.works/event/open-agent-hackathon-2026
|
|
99
|
+
- Live demo: https://zambo.dev/demo
|
|
100
|
+
- Live verifier: https://zambo.dev/verify
|
|
101
|
+
- AER-1 IETF draft: https://datatracker.ietf.org/doc/draft-zambo-aer1/
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
Apache-2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
aer1kit/__init__.py,sha256=YScODcjnOr3DjBM95Iq6Y2XU5QCGBEotG9Y9dqrB4Pg,337
|
|
2
|
+
aer1kit/emitter.py,sha256=V7ZO6TkAEdZ4fCo8rR_or6kdvhyM8kVSG_S9FBLSxGI,14619
|
|
3
|
+
aer1kit-0.1.0.dist-info/METADATA,sha256=TnG6U-sWGLCuuGsIOZ3WpKKuitljZja50fgcSw2uPEs,4246
|
|
4
|
+
aer1kit-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
aer1kit-0.1.0.dist-info/top_level.txt,sha256=ZAvFhX_jxUxwthwTl1Ex6oFv45k2dx87oo8NW9Ug2wQ,8
|
|
6
|
+
aer1kit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aer1kit
|