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.
- aer1_middleware-0.2.0/.gitignore +23 -0
- aer1_middleware-0.2.0/PKG-INFO +128 -0
- aer1_middleware-0.2.0/README.md +94 -0
- aer1_middleware-0.2.0/aer1_middleware/__init__.py +258 -0
- aer1_middleware-0.2.0/aer1_middleware/fastmcp.py +98 -0
- aer1_middleware-0.2.0/aer1_middleware/langchain.py +64 -0
- aer1_middleware-0.2.0/aer1_middleware/openai_agents.py +90 -0
- aer1_middleware-0.2.0/aer1_middleware/otel.py +100 -0
- aer1_middleware-0.2.0/demo.py +58 -0
- aer1_middleware-0.2.0/pyproject.toml +40 -0
|
@@ -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"]
|