proveyouragent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proveyouragent-0.1.0/PKG-INFO +327 -0
- proveyouragent-0.1.0/README.md +299 -0
- proveyouragent-0.1.0/proveyouragent/__init__.py +17 -0
- proveyouragent-0.1.0/proveyouragent/cache.py +159 -0
- proveyouragent-0.1.0/proveyouragent/delegation.py +355 -0
- proveyouragent-0.1.0/proveyouragent/dpop.py +142 -0
- proveyouragent-0.1.0/proveyouragent/identity.py +127 -0
- proveyouragent-0.1.0/proveyouragent/keypair.py +87 -0
- proveyouragent-0.1.0/proveyouragent/middleware.py +117 -0
- proveyouragent-0.1.0/proveyouragent/verify.py +242 -0
- proveyouragent-0.1.0/proveyouragent.egg-info/PKG-INFO +327 -0
- proveyouragent-0.1.0/proveyouragent.egg-info/SOURCES.txt +16 -0
- proveyouragent-0.1.0/proveyouragent.egg-info/dependency_links.txt +1 -0
- proveyouragent-0.1.0/proveyouragent.egg-info/requires.txt +12 -0
- proveyouragent-0.1.0/proveyouragent.egg-info/top_level.txt +1 -0
- proveyouragent-0.1.0/pyproject.toml +41 -0
- proveyouragent-0.1.0/setup.cfg +4 -0
- proveyouragent-0.1.0/tests/test_core.py +723 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proveyouragent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cryptographic identity and trust for AI agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: ai,agents,identity,security,authentication,dpop,trust
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: cryptography>=41.0
|
|
19
|
+
Requires-Dist: pyjwt>=2.8
|
|
20
|
+
Requires-Dist: fastapi>=0.100
|
|
21
|
+
Requires-Dist: uvicorn>=0.23
|
|
22
|
+
Requires-Dist: httpx>=0.24
|
|
23
|
+
Provides-Extra: redis
|
|
24
|
+
Requires-Dist: redis>=5.0; extra == "redis"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# agentid
|
|
30
|
+
|
|
31
|
+
Cryptographic identity for AI agents.
|
|
32
|
+
|
|
33
|
+
agentid gives each agent a keypair, a signed identity document, and a way to prove on every request that the request actually came from that agent. Services verify agent requests before processing them. Stolen tokens are useless without the private key.
|
|
34
|
+
|
|
35
|
+
Built on Ed25519, OAuth 2.0 Dynamic Client Registration (RFC 7591), and DPoP (RFC 9449).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## The problem
|
|
40
|
+
|
|
41
|
+
AI agents call APIs, read databases, write files, and send emails. Most do this with a hardcoded service account token or a borrowed user credential. There is no standard way for a service to know:
|
|
42
|
+
|
|
43
|
+
- which agent made a request
|
|
44
|
+
- who owns and is accountable for that agent
|
|
45
|
+
- what the agent is actually allowed to do
|
|
46
|
+
- whether the request was replayed from a stolen token
|
|
47
|
+
|
|
48
|
+
agentid solves this with a small set of primitives that compose together.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install agentid
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
**Give your agent an identity:**
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from agentid import generate_keypair, save_keypair, create_software_statement
|
|
66
|
+
|
|
67
|
+
key = generate_keypair()
|
|
68
|
+
save_keypair(key)
|
|
69
|
+
|
|
70
|
+
statement = create_software_statement(
|
|
71
|
+
private_key=key,
|
|
72
|
+
operator_domain="acme.com",
|
|
73
|
+
agent_name="billing-agent",
|
|
74
|
+
agent_version="1.0.0",
|
|
75
|
+
scopes=["invoices:read", "payments:write"],
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Sign every request:**
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from agentid import create_dpop_proof
|
|
83
|
+
|
|
84
|
+
proof = create_dpop_proof(key, method="GET", uri="https://api.acme.com/invoices")
|
|
85
|
+
|
|
86
|
+
response = httpx.get(
|
|
87
|
+
"https://api.acme.com/invoices",
|
|
88
|
+
headers={
|
|
89
|
+
"X-Agent-Statement": statement,
|
|
90
|
+
"X-Agent-DPoP": proof,
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Verify on the server:**
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from fastapi import FastAPI, Request
|
|
99
|
+
from agentid.middleware import AgentIDMiddleware, verify_agent
|
|
100
|
+
|
|
101
|
+
app = FastAPI()
|
|
102
|
+
|
|
103
|
+
app.add_middleware(AgentIDMiddleware, get_public_key=my_key_resolver)
|
|
104
|
+
|
|
105
|
+
@app.get("/invoices")
|
|
106
|
+
def list_invoices(request: Request):
|
|
107
|
+
agent = verify_agent(request, required_scope="invoices:read")
|
|
108
|
+
return {"agent": agent.agent_name, "invoices": [...]}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## How it works
|
|
114
|
+
|
|
115
|
+
### Agent identity
|
|
116
|
+
|
|
117
|
+
Every agent gets an Ed25519 keypair. The private key never leaves the agent. The public key is published at a well-known URL so any service can verify requests without calling home.
|
|
118
|
+
|
|
119
|
+
The agent's identity document is a signed JWT called a software statement. It declares who owns the agent, what the agent is allowed to do, and where to find the public key.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
statement = create_software_statement(
|
|
123
|
+
private_key=key,
|
|
124
|
+
operator_domain="acme.com", # who is accountable for this agent
|
|
125
|
+
agent_name="billing-agent",
|
|
126
|
+
agent_version="1.0.0",
|
|
127
|
+
scopes=["invoices:read"],
|
|
128
|
+
model="claude-sonnet-4-6", # optional
|
|
129
|
+
prompt_hash="sha256:abc123", # optional, for version tracking
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Request signing with DPoP
|
|
134
|
+
|
|
135
|
+
Bearer tokens can be stolen and replayed. DPoP (RFC 9449) binds each token to the agent's private key. Every request includes a fresh proof signed by the key, covering the HTTP method and URI. A stolen token is useless without the private key.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Create a fresh proof for each request
|
|
139
|
+
proof = create_dpop_proof(
|
|
140
|
+
private_key=key,
|
|
141
|
+
method="GET",
|
|
142
|
+
uri="https://api.acme.com/invoices",
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Verification
|
|
147
|
+
|
|
148
|
+
The service checks four things on every request:
|
|
149
|
+
|
|
150
|
+
1. The software statement signature is valid
|
|
151
|
+
2. The software statement has not expired
|
|
152
|
+
3. The agent has the required scope
|
|
153
|
+
4. The DPoP proof is fresh, matches this request, and has not been used before
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from agentid import verify_agent_request, VerifiedAgent, VerificationError
|
|
157
|
+
|
|
158
|
+
result = verify_agent_request(
|
|
159
|
+
software_statement=statement,
|
|
160
|
+
dpop_proof=proof,
|
|
161
|
+
method="GET",
|
|
162
|
+
uri="https://api.acme.com/invoices",
|
|
163
|
+
operator_public_key=public_key,
|
|
164
|
+
required_scope="invoices:read",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if isinstance(result, VerifiedAgent):
|
|
168
|
+
print(result.agent_name) # billing-agent
|
|
169
|
+
print(result.operator_domain) # acme.com
|
|
170
|
+
print(result.scopes) # ['invoices:read']
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### FastAPI middleware
|
|
174
|
+
|
|
175
|
+
The middleware handles verification automatically on every route. Verified agent details are attached to `request.state.agent`.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from agentid.middleware import AgentIDMiddleware, verify_agent
|
|
179
|
+
|
|
180
|
+
def get_public_key(operator_domain: str):
|
|
181
|
+
# Return the Ed25519PublicKey for this operator
|
|
182
|
+
# Fetch from your database, config, or key registry
|
|
183
|
+
return your_key_store.get(operator_domain)
|
|
184
|
+
|
|
185
|
+
app.add_middleware(
|
|
186
|
+
AgentIDMiddleware,
|
|
187
|
+
get_public_key=get_public_key,
|
|
188
|
+
exclude_paths=["/health", "/docs"], # skip verification on these
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@app.get("/invoices")
|
|
192
|
+
def list_invoices(request: Request):
|
|
193
|
+
agent = verify_agent(request, required_scope="invoices:read")
|
|
194
|
+
# agent.agent_name, agent.scopes, agent.operator_domain, etc.
|
|
195
|
+
return {"invoices": [...]}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Delegation chains
|
|
199
|
+
|
|
200
|
+
Orchestrator agents can delegate a subset of their permissions to sub-agents. The chain is cryptographically linked. Scopes can only shrink as they pass down the chain.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from agentid.delegation import create_root_mandate, create_delegation, verify_delegation_chain
|
|
204
|
+
|
|
205
|
+
# Human authorises orchestrator
|
|
206
|
+
root = create_root_mandate(
|
|
207
|
+
private_key=operator_key,
|
|
208
|
+
operator_domain="acme.com",
|
|
209
|
+
human_principal="alice@acme.com",
|
|
210
|
+
scopes=["invoices:read", "payments:write"],
|
|
211
|
+
agent_id="acme.com/orchestrator",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Orchestrator delegates a subset to sub-agent
|
|
215
|
+
delegation = create_delegation(
|
|
216
|
+
delegator_key=orchestrator_key,
|
|
217
|
+
delegator_statement=orchestrator_statement,
|
|
218
|
+
delegate_agent_id="acme.com/summariser",
|
|
219
|
+
delegate_public_key_b64=summariser_pub_key,
|
|
220
|
+
scopes=["invoices:read"], # subset of parent scopes only
|
|
221
|
+
parent_token=root,
|
|
222
|
+
human_principal="alice@acme.com",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Tool verifies the full chain
|
|
226
|
+
result = verify_delegation_chain(
|
|
227
|
+
token=delegation,
|
|
228
|
+
required_scope="invoices:read",
|
|
229
|
+
get_public_key=key_resolver,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
print(result.human_principal) # alice@acme.com
|
|
233
|
+
print(result.delegate_agent_id) # acme.com/summariser
|
|
234
|
+
print(result.depth) # 1
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Scope escalation is rejected immediately:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# This returns a DelegationError, not a token
|
|
241
|
+
create_delegation(..., scopes=["invoices:read", "admin:delete"])
|
|
242
|
+
# DelegationError: Cannot delegate scopes not present in parent token: {'admin:delete'}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Replay cache
|
|
248
|
+
|
|
249
|
+
The default replay cache is in-memory. It works for single-process deployments but will not survive a restart or work across multiple processes.
|
|
250
|
+
|
|
251
|
+
For production, use Redis:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from agentid.cache import RedisCache
|
|
255
|
+
from agentid.middleware import AgentIDMiddleware
|
|
256
|
+
|
|
257
|
+
app.add_middleware(
|
|
258
|
+
AgentIDMiddleware,
|
|
259
|
+
get_public_key=get_public_key,
|
|
260
|
+
cache=RedisCache(url="redis://localhost:6379"),
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Or pass a cache directly to `verify_agent_request`:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from agentid.cache import RedisCache
|
|
268
|
+
|
|
269
|
+
cache = RedisCache(url="redis://localhost:6379")
|
|
270
|
+
|
|
271
|
+
result = verify_agent_request(
|
|
272
|
+
...,
|
|
273
|
+
cache=cache,
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## What gets verified on every request
|
|
280
|
+
|
|
281
|
+
| Check | What it catches |
|
|
282
|
+
|---|---|
|
|
283
|
+
| Software statement signature | Forged or tampered identity documents |
|
|
284
|
+
| Statement expiry | Stale tokens |
|
|
285
|
+
| Scope enforcement | Agents claiming permissions they were not granted |
|
|
286
|
+
| DPoP proof signature | Requests not made by the key holder |
|
|
287
|
+
| DPoP method and URI binding | Proofs reused on a different endpoint |
|
|
288
|
+
| DPoP freshness | Old proofs being replayed |
|
|
289
|
+
| DPoP jti uniqueness | Exact replay of a captured request |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Running the examples
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Terminal 1: start the server
|
|
297
|
+
uvicorn examples.server:app --reload
|
|
298
|
+
|
|
299
|
+
# Terminal 2: run the client
|
|
300
|
+
python examples/client.py
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Running the tests
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
pytest tests/ -v
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Design decisions
|
|
314
|
+
|
|
315
|
+
**Ed25519 only.** No algorithm negotiation. Ed25519 is fast, has small keys, and has no known weaknesses. Supporting multiple algorithms adds complexity and attack surface.
|
|
316
|
+
|
|
317
|
+
**No blockchain, no DID infrastructure.** DNS is the trust anchor. Operators publish their public key at a well-known URL on their domain. Every developer already knows how DNS works.
|
|
318
|
+
|
|
319
|
+
**Errors as values, not exceptions.** `verify_agent_request` returns a `VerifiedAgent` or a `VerificationError`. No try/except needed in normal usage. The error always includes a human-readable reason.
|
|
320
|
+
|
|
321
|
+
**Replay cache is pluggable.** The default in-memory cache works for development. Redis works for production. Any backend that implements `ReplayCache` works.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# agentid
|
|
2
|
+
|
|
3
|
+
Cryptographic identity for AI agents.
|
|
4
|
+
|
|
5
|
+
agentid gives each agent a keypair, a signed identity document, and a way to prove on every request that the request actually came from that agent. Services verify agent requests before processing them. Stolen tokens are useless without the private key.
|
|
6
|
+
|
|
7
|
+
Built on Ed25519, OAuth 2.0 Dynamic Client Registration (RFC 7591), and DPoP (RFC 9449).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
AI agents call APIs, read databases, write files, and send emails. Most do this with a hardcoded service account token or a borrowed user credential. There is no standard way for a service to know:
|
|
14
|
+
|
|
15
|
+
- which agent made a request
|
|
16
|
+
- who owns and is accountable for that agent
|
|
17
|
+
- what the agent is actually allowed to do
|
|
18
|
+
- whether the request was replayed from a stolen token
|
|
19
|
+
|
|
20
|
+
agentid solves this with a small set of primitives that compose together.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install agentid
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
**Give your agent an identity:**
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from agentid import generate_keypair, save_keypair, create_software_statement
|
|
38
|
+
|
|
39
|
+
key = generate_keypair()
|
|
40
|
+
save_keypair(key)
|
|
41
|
+
|
|
42
|
+
statement = create_software_statement(
|
|
43
|
+
private_key=key,
|
|
44
|
+
operator_domain="acme.com",
|
|
45
|
+
agent_name="billing-agent",
|
|
46
|
+
agent_version="1.0.0",
|
|
47
|
+
scopes=["invoices:read", "payments:write"],
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Sign every request:**
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from agentid import create_dpop_proof
|
|
55
|
+
|
|
56
|
+
proof = create_dpop_proof(key, method="GET", uri="https://api.acme.com/invoices")
|
|
57
|
+
|
|
58
|
+
response = httpx.get(
|
|
59
|
+
"https://api.acme.com/invoices",
|
|
60
|
+
headers={
|
|
61
|
+
"X-Agent-Statement": statement,
|
|
62
|
+
"X-Agent-DPoP": proof,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Verify on the server:**
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from fastapi import FastAPI, Request
|
|
71
|
+
from agentid.middleware import AgentIDMiddleware, verify_agent
|
|
72
|
+
|
|
73
|
+
app = FastAPI()
|
|
74
|
+
|
|
75
|
+
app.add_middleware(AgentIDMiddleware, get_public_key=my_key_resolver)
|
|
76
|
+
|
|
77
|
+
@app.get("/invoices")
|
|
78
|
+
def list_invoices(request: Request):
|
|
79
|
+
agent = verify_agent(request, required_scope="invoices:read")
|
|
80
|
+
return {"agent": agent.agent_name, "invoices": [...]}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
### Agent identity
|
|
88
|
+
|
|
89
|
+
Every agent gets an Ed25519 keypair. The private key never leaves the agent. The public key is published at a well-known URL so any service can verify requests without calling home.
|
|
90
|
+
|
|
91
|
+
The agent's identity document is a signed JWT called a software statement. It declares who owns the agent, what the agent is allowed to do, and where to find the public key.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
statement = create_software_statement(
|
|
95
|
+
private_key=key,
|
|
96
|
+
operator_domain="acme.com", # who is accountable for this agent
|
|
97
|
+
agent_name="billing-agent",
|
|
98
|
+
agent_version="1.0.0",
|
|
99
|
+
scopes=["invoices:read"],
|
|
100
|
+
model="claude-sonnet-4-6", # optional
|
|
101
|
+
prompt_hash="sha256:abc123", # optional, for version tracking
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Request signing with DPoP
|
|
106
|
+
|
|
107
|
+
Bearer tokens can be stolen and replayed. DPoP (RFC 9449) binds each token to the agent's private key. Every request includes a fresh proof signed by the key, covering the HTTP method and URI. A stolen token is useless without the private key.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Create a fresh proof for each request
|
|
111
|
+
proof = create_dpop_proof(
|
|
112
|
+
private_key=key,
|
|
113
|
+
method="GET",
|
|
114
|
+
uri="https://api.acme.com/invoices",
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Verification
|
|
119
|
+
|
|
120
|
+
The service checks four things on every request:
|
|
121
|
+
|
|
122
|
+
1. The software statement signature is valid
|
|
123
|
+
2. The software statement has not expired
|
|
124
|
+
3. The agent has the required scope
|
|
125
|
+
4. The DPoP proof is fresh, matches this request, and has not been used before
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from agentid import verify_agent_request, VerifiedAgent, VerificationError
|
|
129
|
+
|
|
130
|
+
result = verify_agent_request(
|
|
131
|
+
software_statement=statement,
|
|
132
|
+
dpop_proof=proof,
|
|
133
|
+
method="GET",
|
|
134
|
+
uri="https://api.acme.com/invoices",
|
|
135
|
+
operator_public_key=public_key,
|
|
136
|
+
required_scope="invoices:read",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if isinstance(result, VerifiedAgent):
|
|
140
|
+
print(result.agent_name) # billing-agent
|
|
141
|
+
print(result.operator_domain) # acme.com
|
|
142
|
+
print(result.scopes) # ['invoices:read']
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### FastAPI middleware
|
|
146
|
+
|
|
147
|
+
The middleware handles verification automatically on every route. Verified agent details are attached to `request.state.agent`.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from agentid.middleware import AgentIDMiddleware, verify_agent
|
|
151
|
+
|
|
152
|
+
def get_public_key(operator_domain: str):
|
|
153
|
+
# Return the Ed25519PublicKey for this operator
|
|
154
|
+
# Fetch from your database, config, or key registry
|
|
155
|
+
return your_key_store.get(operator_domain)
|
|
156
|
+
|
|
157
|
+
app.add_middleware(
|
|
158
|
+
AgentIDMiddleware,
|
|
159
|
+
get_public_key=get_public_key,
|
|
160
|
+
exclude_paths=["/health", "/docs"], # skip verification on these
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@app.get("/invoices")
|
|
164
|
+
def list_invoices(request: Request):
|
|
165
|
+
agent = verify_agent(request, required_scope="invoices:read")
|
|
166
|
+
# agent.agent_name, agent.scopes, agent.operator_domain, etc.
|
|
167
|
+
return {"invoices": [...]}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Delegation chains
|
|
171
|
+
|
|
172
|
+
Orchestrator agents can delegate a subset of their permissions to sub-agents. The chain is cryptographically linked. Scopes can only shrink as they pass down the chain.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from agentid.delegation import create_root_mandate, create_delegation, verify_delegation_chain
|
|
176
|
+
|
|
177
|
+
# Human authorises orchestrator
|
|
178
|
+
root = create_root_mandate(
|
|
179
|
+
private_key=operator_key,
|
|
180
|
+
operator_domain="acme.com",
|
|
181
|
+
human_principal="alice@acme.com",
|
|
182
|
+
scopes=["invoices:read", "payments:write"],
|
|
183
|
+
agent_id="acme.com/orchestrator",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Orchestrator delegates a subset to sub-agent
|
|
187
|
+
delegation = create_delegation(
|
|
188
|
+
delegator_key=orchestrator_key,
|
|
189
|
+
delegator_statement=orchestrator_statement,
|
|
190
|
+
delegate_agent_id="acme.com/summariser",
|
|
191
|
+
delegate_public_key_b64=summariser_pub_key,
|
|
192
|
+
scopes=["invoices:read"], # subset of parent scopes only
|
|
193
|
+
parent_token=root,
|
|
194
|
+
human_principal="alice@acme.com",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Tool verifies the full chain
|
|
198
|
+
result = verify_delegation_chain(
|
|
199
|
+
token=delegation,
|
|
200
|
+
required_scope="invoices:read",
|
|
201
|
+
get_public_key=key_resolver,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
print(result.human_principal) # alice@acme.com
|
|
205
|
+
print(result.delegate_agent_id) # acme.com/summariser
|
|
206
|
+
print(result.depth) # 1
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Scope escalation is rejected immediately:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# This returns a DelegationError, not a token
|
|
213
|
+
create_delegation(..., scopes=["invoices:read", "admin:delete"])
|
|
214
|
+
# DelegationError: Cannot delegate scopes not present in parent token: {'admin:delete'}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Replay cache
|
|
220
|
+
|
|
221
|
+
The default replay cache is in-memory. It works for single-process deployments but will not survive a restart or work across multiple processes.
|
|
222
|
+
|
|
223
|
+
For production, use Redis:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from agentid.cache import RedisCache
|
|
227
|
+
from agentid.middleware import AgentIDMiddleware
|
|
228
|
+
|
|
229
|
+
app.add_middleware(
|
|
230
|
+
AgentIDMiddleware,
|
|
231
|
+
get_public_key=get_public_key,
|
|
232
|
+
cache=RedisCache(url="redis://localhost:6379"),
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Or pass a cache directly to `verify_agent_request`:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from agentid.cache import RedisCache
|
|
240
|
+
|
|
241
|
+
cache = RedisCache(url="redis://localhost:6379")
|
|
242
|
+
|
|
243
|
+
result = verify_agent_request(
|
|
244
|
+
...,
|
|
245
|
+
cache=cache,
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## What gets verified on every request
|
|
252
|
+
|
|
253
|
+
| Check | What it catches |
|
|
254
|
+
|---|---|
|
|
255
|
+
| Software statement signature | Forged or tampered identity documents |
|
|
256
|
+
| Statement expiry | Stale tokens |
|
|
257
|
+
| Scope enforcement | Agents claiming permissions they were not granted |
|
|
258
|
+
| DPoP proof signature | Requests not made by the key holder |
|
|
259
|
+
| DPoP method and URI binding | Proofs reused on a different endpoint |
|
|
260
|
+
| DPoP freshness | Old proofs being replayed |
|
|
261
|
+
| DPoP jti uniqueness | Exact replay of a captured request |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Running the examples
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Terminal 1: start the server
|
|
269
|
+
uvicorn examples.server:app --reload
|
|
270
|
+
|
|
271
|
+
# Terminal 2: run the client
|
|
272
|
+
python examples/client.py
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Running the tests
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
pytest tests/ -v
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Design decisions
|
|
286
|
+
|
|
287
|
+
**Ed25519 only.** No algorithm negotiation. Ed25519 is fast, has small keys, and has no known weaknesses. Supporting multiple algorithms adds complexity and attack surface.
|
|
288
|
+
|
|
289
|
+
**No blockchain, no DID infrastructure.** DNS is the trust anchor. Operators publish their public key at a well-known URL on their domain. Every developer already knows how DNS works.
|
|
290
|
+
|
|
291
|
+
**Errors as values, not exceptions.** `verify_agent_request` returns a `VerifiedAgent` or a `VerificationError`. No try/except needed in normal usage. The error always includes a human-readable reason.
|
|
292
|
+
|
|
293
|
+
**Replay cache is pluggable.** The default in-memory cache works for development. Redis works for production. Any backend that implements `ReplayCache` works.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from proveyouragent.keypair import generate_keypair, save_keypair, load_keypair
|
|
2
|
+
from proveyouragent.identity import create_software_statement, decode_software_statement
|
|
3
|
+
from proveyouragent.dpop import create_dpop_proof
|
|
4
|
+
from proveyouragent.verify import verify_agent_request, VerifiedAgent, VerificationError
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"generate_keypair",
|
|
9
|
+
"save_keypair",
|
|
10
|
+
"load_keypair",
|
|
11
|
+
"create_software_statement",
|
|
12
|
+
"decode_software_statement",
|
|
13
|
+
"create_dpop_proof",
|
|
14
|
+
"verify_agent_request",
|
|
15
|
+
"VerifiedAgent",
|
|
16
|
+
"VerificationError",
|
|
17
|
+
]
|