aer1-middleware 0.2.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,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+
13
+ # TLA+
14
+ *.tlacfg
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Editors
21
+ .vscode/
22
+ .idea/
23
+ *.swp
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.5
2
+ Name: aer1-middleware
3
+ Version: 0.2.0
4
+ Summary: AER-1 verifiable execution receipts for AI agent frameworks
5
+ Project-URL: Homepage, https://zambo.dev
6
+ Project-URL: Repository, https://gitlab.com/rambozambodotdev/zambo
7
+ Project-URL: Documentation, https://datatracker.ietf.org/doc/draft-zambo-aer1/
8
+ Author: Brennan Zambo
9
+ License: MIT
10
+ Keywords: aer1,agents,mcp,receipts,verifiable
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.9
17
+ Provides-Extra: all
18
+ Requires-Dist: cryptography>=41.0.0; extra == 'all'
19
+ Requires-Dist: fastmcp>=0.1.0; extra == 'all'
20
+ Requires-Dist: langchain-core>=0.1.0; extra == 'all'
21
+ Requires-Dist: openai-agents>=0.1.0; extra == 'all'
22
+ Requires-Dist: opentelemetry-sdk>=1.0.0; extra == 'all'
23
+ Provides-Extra: fastmcp
24
+ Requires-Dist: fastmcp>=0.1.0; extra == 'fastmcp'
25
+ Provides-Extra: langchain
26
+ Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
27
+ Provides-Extra: openai-agents
28
+ Requires-Dist: openai-agents>=0.1.0; extra == 'openai-agents'
29
+ Provides-Extra: otel
30
+ Requires-Dist: opentelemetry-sdk>=1.0.0; extra == 'otel'
31
+ Provides-Extra: signatures
32
+ Requires-Dist: cryptography>=41.0.0; extra == 'signatures'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # AER-1 Middleware
36
+
37
+ Verifiable execution receipts for AI agent frameworks.
38
+
39
+ Implements [IETF draft-zambo-aer1](https://datatracker.ietf.org/doc/draft-zambo-aer1/) — an IETF Internet-Draft for verifiable AI agent execution receipts.
40
+
41
+ > **Status:** the framework adapters have been validated end to end against
42
+ > the real framework SDKs (FastMCP, langchain-core, openai-agents,
43
+ > opentelemetry-sdk), and generated receipts pass the AER-1 conformance
44
+ > kit's core check against draft-zambo-aer1-10. The package is not yet
45
+ > published on PyPI.
46
+
47
+ ## Installation
48
+
49
+ Not yet published on PyPI. Install from source:
50
+
51
+ ```bash
52
+ git clone https://gitlab.com/rambozambodotdev/zambo.git
53
+ cd zambo/aer1-middleware
54
+ pip install -e .
55
+ ```
56
+
57
+ ## FastMCP (one line)
58
+
59
+ ```python
60
+ from fastmcp import FastMCP
61
+ from aer1_middleware.fastmcp import AER1Middleware
62
+
63
+ mcp = FastMCP("my-server")
64
+ mcp.add_middleware(AER1Middleware()) # That's it. Every tool call now generates a receipt.
65
+ ```
66
+
67
+ ## LangChain (one line)
68
+
69
+ ```python
70
+ from langchain_openai import ChatOpenAI
71
+ from aer1_middleware.langchain import AER1Callback
72
+
73
+ llm = ChatOpenAI(callbacks=[AER1Callback()])
74
+ ```
75
+
76
+ ## OpenAI Agents SDK (one decorator)
77
+
78
+ ```python
79
+ from agents import function_tool
80
+ from aer1_middleware.openai_agents import with_aer1
81
+
82
+ @function_tool
83
+ @with_aer1
84
+ def search(query: str) -> str:
85
+ return f"Results for {query}"
86
+ ```
87
+
88
+ (`with_aer1` must wrap the raw function, so it goes directly above `def`,
89
+ inside `function_tool`. Retrieve receipts with
90
+ `from aer1_middleware.openai_agents import get_receipts`.)
91
+
92
+ ## OpenTelemetry (span exporter)
93
+
94
+ ```python
95
+ from aer1_middleware.otel import AER1SpanExporter
96
+
97
+ exporter = AER1SpanExporter()
98
+ exporter.export_receipt(receipt)
99
+ ```
100
+
101
+ ## Manual usage
102
+
103
+ ```python
104
+ from aer1_middleware import create_receipt, verify_receipt
105
+
106
+ receipt = create_receipt(
107
+ tool_name="search",
108
+ inputs={"query": "AER-1"},
109
+ output={"results": [...]},
110
+ )
111
+
112
+ is_valid, reason = verify_receipt(receipt)
113
+ ```
114
+
115
+ ## What is AER-1?
116
+
117
+ AER-1 (Agent Execution Receipt v1) is an IETF Internet-Draft for verifiable records of AI agent tool executions. Each receipt contains:
118
+
119
+ - SHA-256 commitment over the canonical tool-call payload
120
+ - Timestamp and unique IDs
121
+ - Optional Ed25519 signature
122
+ - Independently verifiable against the conformance kit
123
+
124
+ A receipt proves the saved result has not changed. It does not prove the model was right, and it does not by itself prove an external outcome.
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,94 @@
1
+ # AER-1 Middleware
2
+
3
+ Verifiable execution receipts for AI agent frameworks.
4
+
5
+ Implements [IETF draft-zambo-aer1](https://datatracker.ietf.org/doc/draft-zambo-aer1/) — an IETF Internet-Draft for verifiable AI agent execution receipts.
6
+
7
+ > **Status:** the framework adapters have been validated end to end against
8
+ > the real framework SDKs (FastMCP, langchain-core, openai-agents,
9
+ > opentelemetry-sdk), and generated receipts pass the AER-1 conformance
10
+ > kit's core check against draft-zambo-aer1-10. The package is not yet
11
+ > published on PyPI.
12
+
13
+ ## Installation
14
+
15
+ Not yet published on PyPI. Install from source:
16
+
17
+ ```bash
18
+ git clone https://gitlab.com/rambozambodotdev/zambo.git
19
+ cd zambo/aer1-middleware
20
+ pip install -e .
21
+ ```
22
+
23
+ ## FastMCP (one line)
24
+
25
+ ```python
26
+ from fastmcp import FastMCP
27
+ from aer1_middleware.fastmcp import AER1Middleware
28
+
29
+ mcp = FastMCP("my-server")
30
+ mcp.add_middleware(AER1Middleware()) # That's it. Every tool call now generates a receipt.
31
+ ```
32
+
33
+ ## LangChain (one line)
34
+
35
+ ```python
36
+ from langchain_openai import ChatOpenAI
37
+ from aer1_middleware.langchain import AER1Callback
38
+
39
+ llm = ChatOpenAI(callbacks=[AER1Callback()])
40
+ ```
41
+
42
+ ## OpenAI Agents SDK (one decorator)
43
+
44
+ ```python
45
+ from agents import function_tool
46
+ from aer1_middleware.openai_agents import with_aer1
47
+
48
+ @function_tool
49
+ @with_aer1
50
+ def search(query: str) -> str:
51
+ return f"Results for {query}"
52
+ ```
53
+
54
+ (`with_aer1` must wrap the raw function, so it goes directly above `def`,
55
+ inside `function_tool`. Retrieve receipts with
56
+ `from aer1_middleware.openai_agents import get_receipts`.)
57
+
58
+ ## OpenTelemetry (span exporter)
59
+
60
+ ```python
61
+ from aer1_middleware.otel import AER1SpanExporter
62
+
63
+ exporter = AER1SpanExporter()
64
+ exporter.export_receipt(receipt)
65
+ ```
66
+
67
+ ## Manual usage
68
+
69
+ ```python
70
+ from aer1_middleware import create_receipt, verify_receipt
71
+
72
+ receipt = create_receipt(
73
+ tool_name="search",
74
+ inputs={"query": "AER-1"},
75
+ output={"results": [...]},
76
+ )
77
+
78
+ is_valid, reason = verify_receipt(receipt)
79
+ ```
80
+
81
+ ## What is AER-1?
82
+
83
+ AER-1 (Agent Execution Receipt v1) is an IETF Internet-Draft for verifiable records of AI agent tool executions. Each receipt contains:
84
+
85
+ - SHA-256 commitment over the canonical tool-call payload
86
+ - Timestamp and unique IDs
87
+ - Optional Ed25519 signature
88
+ - Independently verifiable against the conformance kit
89
+
90
+ A receipt proves the saved result has not changed. It does not prove the model was right, and it does not by itself prove an external outcome.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,258 @@
1
+ """
2
+ AER-1 Middleware: Verifiable execution receipts for AI agent frameworks.
3
+
4
+ This package provides integrations for popular agent frameworks
5
+ to generate IETF draft-zambo-aer1 execution receipts.
6
+
7
+ Supported frameworks (validated against the real framework SDKs):
8
+ - FastMCP (via aer1_middleware.fastmcp)
9
+ - LangChain (via aer1_middleware.langchain)
10
+ - OpenAI Agents SDK (via aer1_middleware.openai_agents)
11
+
12
+ Plus an OpenTelemetry span exporter (via aer1_middleware.otel).
13
+
14
+ Generated receipts conform to the AER-1 conformance kit core schema
15
+ (draft-zambo-aer1-10, Section 3) and pass the kit's check().
16
+
17
+ Each receipt is a verifiable, hash-chained record of tool execution
18
+ that can be independently verified against the AER-1 specification.
19
+
20
+ A receipt proves the saved result has not changed. It does not prove the
21
+ model was right, and it does not by itself prove an external outcome.
22
+ """
23
+
24
+ __version__ = "0.2.0"
25
+ __all__ = ["create_receipt", "verify_receipt", "generate_keypair", "sign_receipt"]
26
+
27
+ import base64
28
+ import hashlib
29
+ import json
30
+ import re
31
+ import uuid
32
+ from datetime import datetime, timezone
33
+
34
+ try:
35
+ from cryptography.hazmat.primitives.asymmetric import ed25519
36
+ from cryptography.hazmat.primitives import serialization
37
+ HAS_CRYPTO = True
38
+ except ImportError:
39
+ HAS_CRYPTO = False
40
+
41
+ # Kit-conformance patterns (mirrors aer-1/conformance.py)
42
+ _UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
43
+ r"[0-9a-f]{4}-[0-9a-f]{12}\Z")
44
+ _RFC3339_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:"
45
+ r"[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})\Z")
46
+ _HASH_RE = re.compile(r"^sha256:[0-9a-f]{64}\Z")
47
+ _B64_RE = re.compile(r"\A[A-Za-z0-9+/]*={0,2}\Z")
48
+ _PROV_RE = re.compile(r"^(EXECUTED BY [A-Z0-9][A-Z0-9 ._-]*|"
49
+ r"OBSERVED VIA GATEWAY|LOGGED BY AGENT)\Z")
50
+ _KIT_CORE = ["id", "receipt_schema_version", "created_at", "tool",
51
+ "provenance_class", "canonical_bytes", "output_hash",
52
+ "verification_status"]
53
+
54
+
55
+ def generate_keypair():
56
+ """
57
+ Generate an Ed25519 keypair for signing receipts.
58
+
59
+ Returns:
60
+ Tuple of (private_key_pem, public_key_pem) as strings.
61
+ Requires the 'cryptography' package.
62
+ """
63
+ if not HAS_CRYPTO:
64
+ raise ImportError("Ed25519 signatures require the 'cryptography' package: pip install cryptography")
65
+
66
+ private_key = ed25519.Ed25519PrivateKey.generate()
67
+ public_key = private_key.public_key()
68
+
69
+ private_pem = private_key.private_bytes(
70
+ encoding=serialization.Encoding.PEM,
71
+ format=serialization.PrivateFormat.PKCS8,
72
+ encryption_algorithm=serialization.NoEncryption()
73
+ ).decode('utf-8')
74
+
75
+ public_pem = public_key.public_bytes(
76
+ encoding=serialization.Encoding.PEM,
77
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
78
+ ).decode('utf-8')
79
+
80
+ return private_pem, public_pem
81
+
82
+
83
+ def sign_receipt(receipt, private_key_pem):
84
+ """
85
+ Sign a receipt with Ed25519.
86
+
87
+ Signs the receipt's output_hash (the content commitment).
88
+
89
+ Args:
90
+ receipt: Dict containing the receipt (must have 'output_hash' field)
91
+ private_key_pem: PEM-encoded Ed25519 private key
92
+
93
+ Returns:
94
+ Receipt dict with 'signature' and 'public_key' fields added.
95
+ """
96
+ if not HAS_CRYPTO:
97
+ raise ImportError("Ed25519 signatures require the 'cryptography' package")
98
+
99
+ private_key = serialization.load_pem_private_key(
100
+ private_key_pem.encode('utf-8'),
101
+ password=None
102
+ )
103
+
104
+ # Sign the output_hash (the content commitment)
105
+ signature = private_key.sign(receipt['output_hash'].encode('utf-8'))
106
+
107
+ # Get public key for verification
108
+ public_key = private_key.public_key()
109
+ public_pem = public_key.public_bytes(
110
+ encoding=serialization.Encoding.PEM,
111
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
112
+ ).decode('utf-8')
113
+
114
+ receipt['signature'] = signature.hex()
115
+ receipt['public_key'] = public_pem
116
+ receipt['signature_algorithm'] = 'Ed25519'
117
+
118
+ return receipt
119
+
120
+
121
+ def create_receipt(tool_name, inputs, output, job_id=None, did=None, profile="public-v1", trust_boundary=None, tool_version="1.0.0", tool_scope="public"):
122
+ """
123
+ Create an AER-1 execution receipt for a tool call.
124
+
125
+ The receipt conforms to the AER-1 conformance kit core schema
126
+ (draft-zambo-aer1-10, Section 3): id, receipt_schema_version,
127
+ created_at, tool, provenance_class, canonical_bytes, output_hash,
128
+ verification_status.
129
+
130
+ Args:
131
+ tool_name: Name of the tool that was executed
132
+ inputs: Dict of input parameters
133
+ output: Tool output (will be hashed, not stored)
134
+ job_id: Optional job ID for chaining (generates one if not provided)
135
+ did: Optional DID string recorded as metadata (e.g., "did:key:z6Mk...").
136
+ Recorded only -- no DID resolution, key validation, or delegation
137
+ verification is performed.
138
+ profile: Receipt profile label ("public-v1" default, or "private-v1").
139
+ Recorded only -- no encryption or access control is applied.
140
+ trust_boundary: Optional URN recorded as metadata scoping the trust
141
+ boundary. Recorded only -- not enforced.
142
+ tool_version: Version string for the tool object (default "1.0.0")
143
+ tool_scope: Scope string for the tool object (default "public")
144
+
145
+ Returns:
146
+ Dict containing the AER-1 receipt
147
+ """
148
+ receipt_id = str(uuid.uuid4())
149
+ job_id = job_id or str(uuid.uuid4())
150
+ created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
151
+
152
+ # Canonical payload per draft Sections 3-4
153
+ payload = {
154
+ "tool": tool_name,
155
+ "inputs": inputs,
156
+ "outputs": output if isinstance(output, dict) else {"result": output},
157
+ }
158
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
159
+ canonical_bytes = base64.b64encode(canonical.encode("utf-8")).decode("ascii")
160
+ output_hash = "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
161
+
162
+ receipt = {
163
+ "id": receipt_id,
164
+ "receipt_schema_version": "0.3",
165
+ "created_at": created_at,
166
+ "tool": {"name": tool_name, "version": tool_version, "scope": tool_scope},
167
+ "provenance_class": "EXECUTED BY ZAMBO",
168
+ "canonical_bytes": canonical_bytes,
169
+ "output_hash": output_hash,
170
+ "verification_status": "verified",
171
+ "job_id": job_id,
172
+ "profile": profile,
173
+ }
174
+
175
+ # Add DID identity binding if provided (metadata only)
176
+ if did:
177
+ receipt["identity"] = {
178
+ "did": did,
179
+ "proof_purpose": "assertionMethod"
180
+ }
181
+
182
+ # Add trust boundary for private profile (metadata only)
183
+ if trust_boundary:
184
+ receipt["trust_boundary"] = trust_boundary
185
+ receipt["visibility"] = "private" if profile == "private-v1" else "public"
186
+
187
+ return receipt
188
+
189
+
190
+
191
+
192
+ def verify_receipt(receipt):
193
+ """
194
+ Verify an AER-1 receipt against the conformance kit core schema
195
+ (draft-zambo-aer1-10, Section 3) plus Ed25519 signature if present.
196
+
197
+ Args:
198
+ receipt: Dict containing the receipt to verify
199
+
200
+ Returns:
201
+ Tuple of (is_valid, reason)
202
+ """
203
+ if not isinstance(receipt, dict):
204
+ return False, "receipt is not a JSON object"
205
+ for field in _KIT_CORE:
206
+ if field not in receipt:
207
+ return False, f"missing required member: {field}"
208
+ if not isinstance(receipt["id"], str) or not _UUID_RE.match(receipt["id"]):
209
+ return False, "id is not a lowercase UUID"
210
+ if (not isinstance(receipt["receipt_schema_version"], str)
211
+ or not receipt["receipt_schema_version"]):
212
+ return False, "receipt_schema_version is not a non-empty string"
213
+ if (not isinstance(receipt["created_at"], str)
214
+ or not _RFC3339_RE.match(receipt["created_at"])):
215
+ return False, "created_at is not RFC 3339"
216
+ tool = receipt["tool"]
217
+ if not isinstance(tool, dict) or not all(
218
+ isinstance(tool.get(k), str) and tool[k]
219
+ for k in ("name", "version", "scope")):
220
+ return False, "tool is not an object with name/version/scope strings"
221
+ if (not isinstance(receipt["provenance_class"], str)
222
+ or not _PROV_RE.match(receipt["provenance_class"])):
223
+ return False, "provenance_class is not a known class"
224
+ cb = receipt["canonical_bytes"]
225
+ raw = None
226
+ if not isinstance(cb, str) or not _B64_RE.match(cb):
227
+ return False, "canonical_bytes is not valid base64"
228
+ try:
229
+ raw = base64.b64decode(cb, validate=True)
230
+ raw.decode("utf-8")
231
+ except Exception:
232
+ return False, "canonical_bytes is not valid base64 UTF-8"
233
+ oh = receipt["output_hash"]
234
+ if not isinstance(oh, str) or not _HASH_RE.match(oh):
235
+ return False, "output_hash is not sha256: + lowercase hex digest"
236
+ want = "sha256:" + hashlib.sha256(raw).hexdigest()
237
+ if oh != want:
238
+ return False, "output_hash does not match sha256(canonical_bytes)"
239
+ if receipt["verification_status"] != "verified":
240
+ return False, "verification_status is not verified"
241
+
242
+ # Verify Ed25519 signature if present (signs the output_hash commitment)
243
+ if "signature" in receipt:
244
+ if not HAS_CRYPTO:
245
+ return False, "receipt is signed but 'cryptography' not installed"
246
+ if "public_key" not in receipt:
247
+ return False, "signature present but public_key missing"
248
+ try:
249
+ public_key = serialization.load_pem_public_key(
250
+ receipt["public_key"].encode("utf-8")
251
+ )
252
+ signature = bytes.fromhex(receipt["signature"])
253
+ public_key.verify(signature, receipt["output_hash"].encode("utf-8"))
254
+ except Exception as e:
255
+ return False, f"signature verification failed: {e}"
256
+ return True, "receipt is valid and signature verified"
257
+
258
+ return True, "receipt is valid (unsigned)"
@@ -0,0 +1,98 @@
1
+ """
2
+ FastMCP integration for AER-1 verifiable execution receipts.
3
+
4
+ One-line integration:
5
+ from aer1_middleware.fastmcp import AER1Middleware
6
+ mcp.add_middleware(AER1Middleware())
7
+
8
+ Every tool call automatically generates an AER-1 receipt.
9
+ """
10
+
11
+ try:
12
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
13
+ HAS_FASTMCP = True
14
+ except ImportError:
15
+ HAS_FASTMCP = False
16
+ # Stub for documentation without dependency
17
+ class Middleware:
18
+ pass
19
+ class MiddlewareContext:
20
+ pass
21
+
22
+ from . import create_receipt
23
+ import json
24
+ import logging
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _serialize_output(output):
30
+ """Convert framework result objects to JSON-serializable values."""
31
+ if isinstance(output, list):
32
+ parts = [_serialize_output(item) for item in output]
33
+ return parts[0] if len(parts) == 1 else parts
34
+ if hasattr(output, "text") and isinstance(output.text, str):
35
+ return output.text
36
+ if hasattr(output, "model_dump"):
37
+ try:
38
+ return output.model_dump()
39
+ except Exception:
40
+ pass
41
+ try:
42
+ json.dumps(output)
43
+ return output
44
+ except TypeError:
45
+ return str(output)
46
+
47
+
48
+ class AER1Middleware(Middleware if HAS_FASTMCP else object):
49
+ """
50
+ FastMCP middleware that generates AER-1 receipts for every tool call.
51
+
52
+ Usage:
53
+ from fastmcp import FastMCP
54
+ from aer1_middleware.fastmcp import AER1Middleware
55
+
56
+ mcp = FastMCP("my-server")
57
+ mcp.add_middleware(AER1Middleware())
58
+
59
+ Each tool execution will automatically generate a verifiable
60
+ AER-1 receipt that can be retrieved via get_receipts().
61
+ """
62
+
63
+ def __init__(self, job_id=None):
64
+ self.job_id = job_id
65
+ self.receipts = []
66
+
67
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
68
+ """Intercept tool calls and generate receipts."""
69
+ tool_name = context.message.name if hasattr(context.message, 'name') else 'unknown'
70
+ inputs = context.message.arguments if hasattr(context.message, 'arguments') else {}
71
+
72
+ # Execute the tool
73
+ result = await call_next(context)
74
+
75
+ # Generate receipt
76
+ try:
77
+ raw_output = result.content if hasattr(result, 'content') else str(result)
78
+ output = _serialize_output(raw_output)
79
+ receipt = create_receipt(
80
+ tool_name=tool_name,
81
+ inputs=inputs,
82
+ output=output,
83
+ job_id=self.job_id,
84
+ )
85
+ self.receipts.append(receipt)
86
+ logger.info(f"AER-1 receipt generated: {receipt['id']}")
87
+ except Exception as e:
88
+ logger.warning(f"Failed to generate AER-1 receipt: {e}")
89
+
90
+ return result
91
+
92
+ def get_receipts(self):
93
+ """Return all generated receipts."""
94
+ return self.receipts
95
+
96
+ def clear_receipts(self):
97
+ """Clear the receipt buffer."""
98
+ self.receipts = []
@@ -0,0 +1,64 @@
1
+ """
2
+ LangChain integration for AER-1 verifiable execution receipts.
3
+
4
+ One-line integration:
5
+ from aer1_middleware.langchain import AER1Callback
6
+ llm = ChatOpenAI(callbacks=[AER1Callback()])
7
+
8
+ Every tool call automatically generates an AER-1 receipt.
9
+ """
10
+
11
+ try:
12
+ from langchain_core.callbacks import BaseCallbackHandler
13
+ HAS_LANGCHAIN = True
14
+ except ImportError:
15
+ HAS_LANGCHAIN = False
16
+ class BaseCallbackHandler:
17
+ pass
18
+
19
+ from . import create_receipt
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AER1Callback(BaseCallbackHandler if HAS_LANGCHAIN else object):
26
+ """
27
+ LangChain callback handler that generates AER-1 receipts.
28
+
29
+ Usage:
30
+ from langchain_openai import ChatOpenAI
31
+ from aer1_middleware.langchain import AER1Callback
32
+
33
+ llm = ChatOpenAI(callbacks=[AER1Callback()])
34
+
35
+ Every tool execution will automatically generate a verifiable
36
+ AER-1 receipt.
37
+ """
38
+
39
+ def __init__(self, job_id=None):
40
+ self.job_id = job_id
41
+ self.receipts = []
42
+
43
+ def on_tool_start(self, serialized, input_str, **kwargs):
44
+ """Called when a tool starts."""
45
+ self._current_tool = serialized.get("name", "unknown")
46
+ self._current_inputs = {"input": input_str}
47
+
48
+ def on_tool_end(self, output, **kwargs):
49
+ """Called when a tool ends - generate receipt."""
50
+ try:
51
+ receipt = create_receipt(
52
+ tool_name=getattr(self, '_current_tool', 'unknown'),
53
+ inputs=getattr(self, '_current_inputs', {}),
54
+ output=str(output),
55
+ job_id=self.job_id,
56
+ )
57
+ self.receipts.append(receipt)
58
+ logger.info(f"AER-1 receipt generated: {receipt['id']}")
59
+ except Exception as e:
60
+ logger.warning(f"Failed to generate AER-1 receipt: {e}")
61
+
62
+ def get_receipts(self):
63
+ """Return all generated receipts."""
64
+ return self.receipts
@@ -0,0 +1,90 @@
1
+ """
2
+ OpenAI Agents SDK integration for AER-1 verifiable execution receipts.
3
+
4
+ One-line integration via function wrapper:
5
+ from aer1_middleware.openai_agents import with_aer1, get_receipts
6
+
7
+ @function_tool
8
+ @with_aer1
9
+ def search(query: str) -> str:
10
+ ...
11
+
12
+ `with_aer1` must wrap the raw function, so it goes directly above `def`,
13
+ inside `function_tool`. Retrieve receipts via get_receipts().
14
+ """
15
+
16
+ from . import create_receipt
17
+ import functools
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Module-level registry: tool name -> list of receipts. FunctionTool objects
23
+ # don't expose the wrapped function, so receipts are also tracked here.
24
+ _receipt_registry = {}
25
+
26
+
27
+ def get_receipts(tool_name=None):
28
+ """Return generated receipts, optionally filtered by tool name."""
29
+ if tool_name:
30
+ return list(_receipt_registry.get(tool_name, []))
31
+ return [r for receipts in _receipt_registry.values() for r in receipts]
32
+
33
+
34
+ def clear_receipts(tool_name=None):
35
+ """Clear generated receipts, optionally filtered by tool name."""
36
+ if tool_name:
37
+ _receipt_registry.pop(tool_name, None)
38
+ else:
39
+ _receipt_registry.clear()
40
+
41
+
42
+ def with_aer1(func):
43
+ """
44
+ Decorator that generates AER-1 receipts for OpenAI Agents SDK tools.
45
+
46
+ Usage:
47
+ from agents import function_tool
48
+ from aer1_middleware.openai_agents import with_aer1, get_receipts
49
+
50
+ @function_tool
51
+ @with_aer1
52
+ def search(query: str) -> str:
53
+ return f"Results for {query}"
54
+
55
+ receipts = get_receipts("search")
56
+
57
+ `with_aer1` must wrap the raw function, so it goes directly above `def`,
58
+ inside `function_tool`.
59
+
60
+ Every tool execution will automatically generate a verifiable
61
+ AER-1 receipt accessible via func.aer1_receipts.
62
+ """
63
+ @functools.wraps(func)
64
+ def wrapper(*args, **kwargs):
65
+ # Extract tool name and inputs
66
+ tool_name = func.__name__
67
+ inputs = {"args": str(args), "kwargs": str(kwargs)}
68
+
69
+ # Execute the tool
70
+ result = func(*args, **kwargs)
71
+
72
+ # Generate receipt
73
+ try:
74
+ receipt = create_receipt(
75
+ tool_name=tool_name,
76
+ inputs=inputs,
77
+ output=str(result),
78
+ )
79
+ if not hasattr(wrapper, 'aer1_receipts'):
80
+ wrapper.aer1_receipts = []
81
+ wrapper.aer1_receipts.append(receipt)
82
+ _receipt_registry.setdefault(tool_name, []).append(receipt)
83
+ logger.info(f"AER-1 receipt generated: {receipt['id']}")
84
+ except Exception as e:
85
+ logger.warning(f"Failed to generate AER-1 receipt: {e}")
86
+
87
+ return result
88
+
89
+ wrapper.aer1_receipts = []
90
+ return wrapper
@@ -0,0 +1,100 @@
1
+ """
2
+ OpenTelemetry exporter bridge for AER-1.
3
+
4
+ Converts AER-1 receipts to OTel spans for integration with
5
+ Datadog, Honeycomb, Jaeger, and other observability platforms.
6
+
7
+ Usage:
8
+ from aer1_middleware.otel import AER1SpanExporter
9
+ exporter = AER1SpanExporter()
10
+ exporter.export_receipt(receipt)
11
+ """
12
+
13
+ try:
14
+ from opentelemetry.trace import SpanKind, Status, StatusCode
15
+ from opentelemetry.sdk.trace import TracerProvider
16
+ HAS_OTEL = True
17
+ except ImportError:
18
+ HAS_OTEL = False
19
+
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AER1SpanExporter:
26
+ """
27
+ Exports AER-1 receipts as OpenTelemetry spans.
28
+
29
+ This bridges AER-1's verifiable receipts with the OTel ecosystem,
30
+ enabling integration with Datadog, Honeycomb, Jaeger, LangGraph,
31
+ CrewAI, AutoGen, and other platforms that consume OTel.
32
+
33
+ Each receipt becomes a span with:
34
+ - Span name: aer1.tool.{tool_name}
35
+ - Attributes: receipt ID, job ID, hashes, signature status
36
+ - Events: receipt creation, verification
37
+ """
38
+
39
+ def __init__(self, tracer_provider=None):
40
+ if not HAS_OTEL:
41
+ raise ImportError(
42
+ "OTel bridge requires 'opentelemetry-sdk': "
43
+ "pip install opentelemetry-sdk"
44
+ )
45
+ self.tracer_provider = tracer_provider or TracerProvider()
46
+ self.tracer = self.tracer_provider.get_tracer("aer1-middleware")
47
+
48
+ def export_receipt(self, receipt):
49
+ """
50
+ Export an AER-1 receipt as an OTel span.
51
+
52
+ Args:
53
+ receipt: Dict containing the AER-1 receipt
54
+ """
55
+ tool = receipt.get("tool", {})
56
+ tool_name = tool.get("name", "unknown") if isinstance(tool, dict) else str(tool)
57
+ span_name = f"aer1.tool.{tool_name}"
58
+
59
+ with self.tracer.start_as_current_span(
60
+ span_name,
61
+ kind=SpanKind.INTERNAL,
62
+ ) as span:
63
+ # Set span attributes from receipt
64
+ span.set_attribute("aer1.receipt_id", receipt.get("id", ""))
65
+ span.set_attribute("aer1.job_id", receipt.get("job_id", ""))
66
+ span.set_attribute("aer1.schema_version", receipt.get("receipt_schema_version", ""))
67
+ span.set_attribute("aer1.tool", tool_name)
68
+ span.set_attribute("aer1.created_at", receipt.get("created_at", ""))
69
+ span.set_attribute("aer1.output_hash", receipt.get("output_hash", ""))
70
+
71
+ # Signature status
72
+ is_signed = "signature" in receipt
73
+ span.set_attribute("aer1.signed", is_signed)
74
+ if is_signed:
75
+ span.set_attribute(
76
+ "aer1.signature_algorithm",
77
+ receipt.get("signature_algorithm", "unknown")
78
+ )
79
+
80
+ # Provenance
81
+ span.set_attribute(
82
+ "aer1.provenance_class",
83
+ receipt.get("provenance_class", "")
84
+ )
85
+
86
+ # Add event for receipt creation
87
+ span.add_event(
88
+ "aer1.receipt_created",
89
+ attributes={
90
+ "receipt.id": receipt.get("id", ""),
91
+ }
92
+ )
93
+
94
+ span.set_status(Status(StatusCode.OK))
95
+ logger.info(f"Exported AER-1 receipt {receipt.get('id', '')[:8]} as OTel span")
96
+
97
+ def export_receipts(self, receipts):
98
+ """Export multiple receipts."""
99
+ for receipt in receipts:
100
+ self.export_receipt(receipt)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AER-1 Live Demo: Verifiable receipts in action.
4
+
5
+ Run this to see AER-1 generate verifiable receipts for tool calls.
6
+ No setup required - uses the middleware directly.
7
+ """
8
+
9
+ from aer1_middleware import create_receipt, verify_receipt
10
+ import json
11
+
12
+ def demo():
13
+ print("=" * 60)
14
+ print("AER-1 Verifiable Execution Receipts - Live Demo")
15
+ print("=" * 60)
16
+ print()
17
+
18
+ # Simulate a tool call
19
+ print("1. Agent calls a tool...")
20
+ tool_name = "search"
21
+ inputs = {"query": "verifiable receipts"}
22
+ output = {"results": ["AER-1 draft", "conformance kit", "middleware"]}
23
+
24
+ print(f" Tool: {tool_name}")
25
+ print(f" Inputs: {inputs}")
26
+ print()
27
+
28
+ # Generate receipt
29
+ print("2. AER-1 middleware generates a receipt...")
30
+ receipt = create_receipt(tool_name, inputs, output)
31
+ print(f" Receipt ID: {receipt['id']}")
32
+ print(f" Output hash: {receipt['output_hash'][:20]}...")
33
+ print(f" Timestamp: {receipt['created_at']}")
34
+ print()
35
+
36
+ # Verify it
37
+ print("3. Anyone can verify the receipt...")
38
+ is_valid, reason = verify_receipt(receipt)
39
+ print(f" Valid: {is_valid}")
40
+ print(f" Reason: {reason}")
41
+ print()
42
+
43
+ # Tamper with it
44
+ print("4. Tampering is detected...")
45
+ tampered = receipt.copy()
46
+ tampered["canonical_bytes"] = "dGFtcGVyZWQ=" # not the original payload
47
+ is_valid2, reason2 = verify_receipt(tampered)
48
+ print(f" Valid after tamper: {is_valid2}")
49
+ print(f" Reason: {reason2}")
50
+ print()
51
+
52
+ print("=" * 60)
53
+ print("That's AER-1. Verifiable. Tamper-evident.")
54
+ print("IETF Internet-Draft. MIT licensed.")
55
+ print("=" * 60)
56
+
57
+ if __name__ == "__main__":
58
+ demo()
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aer1-middleware"
7
+ version = "0.2.0"
8
+ description = "AER-1 verifiable execution receipts for AI agent frameworks"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Brennan Zambo"},
14
+ ]
15
+ keywords = ["aer1", "mcp", "agents", "receipts", "verifiable"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+
24
+ dependencies = []
25
+
26
+ [project.optional-dependencies]
27
+ fastmcp = ["fastmcp>=0.1.0"]
28
+ langchain = ["langchain-core>=0.1.0"]
29
+ openai-agents = ["openai-agents>=0.1.0"]
30
+ otel = ["opentelemetry-sdk>=1.0.0"]
31
+ signatures = ["cryptography>=41.0.0"]
32
+ all = ["fastmcp>=0.1.0", "langchain-core>=0.1.0", "openai-agents>=0.1.0", "opentelemetry-sdk>=1.0.0", "cryptography>=41.0.0"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://zambo.dev"
36
+ Repository = "https://gitlab.com/rambozambodotdev/zambo"
37
+ Documentation = "https://datatracker.ietf.org/doc/draft-zambo-aer1/"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["aer1_middleware"]