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.
@@ -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
+ ]