Skip to content

Commit 10d384a

Browse files
authored
Merge pull request #13 from devonartis/docs/delegation-accuracy
docs: tech editor pass + delegation accuracy fix
2 parents ca6e223 + feb8d62 commit 10d384a

17 files changed

Lines changed: 226 additions & 89 deletions

README.md

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@
1515
Built on Ed25519 challenge-response and the <a href="https://github.com/devonartis/AI-Security-Blueprints/blob/main/patterns/ephemeral-agent-credentialing/versions/v1.3.md">Ephemeral Agent Credentialing v1.3</a> pattern.
1616
</p>
1717

18+
<p align="center">
19+
<a href="#why-agentwrit">Why</a> ·
20+
<a href="#installation">Install</a> ·
21+
<a href="#prerequisites">Prerequisites</a> ·
22+
<a href="#quick-start">Quick Start</a> ·
23+
<a href="#agent-lifecycle">Lifecycle</a> ·
24+
<a href="#medassist-ai-demo">Demo</a> ·
25+
<a href="#scope-format">Scopes</a> ·
26+
<a href="#delegation">Delegation</a> ·
27+
<a href="#error-handling">Errors</a> ·
28+
<a href="#architecture">Architecture</a> ·
29+
<a href="#documentation">Docs</a>
30+
</p>
31+
1832
---
1933

2034
## Why AgentWrit?
@@ -24,9 +38,9 @@ AI agents need credentials to access databases, APIs, and file systems. Most tea
2438
- **Ephemeral identities** — every agent gets a unique Ed25519 keypair, generated in memory and never persisted to disk
2539
- **Task-scoped tokens** — credentials are limited to exactly what the agent needs (`read:data:customers`, not `read:*:*`)
2640
- **Short-lived by default** — tokens expire in minutes, not hours or days
27-
- **Delegation chains** — agents can delegate narrower permissions to other agents, enforced at every hop
41+
- **Delegation chains** — agents can delegate a subset of their permissions to other agents; the broker rejects any attempt to widen
2842

29-
This SDK is the Python client for the [AgentWrit broker](https://github.com/devonartis/agentwrit). The broker is the credential authority; this SDK makes it easy to integrate from Python.
43+
This SDK is the Python client for the [AgentWrit broker](https://github.com/devonartis/agentwrit) — the broker is the credential authority, and this SDK is how your Python code talks to it.
3044

3145
## Installation
3246

@@ -50,19 +64,53 @@ cd agentwrit-python
5064
uv sync --all-extras
5165
```
5266

53-
**Requirements:** Python 3.10+ and a running [AgentWrit broker](https://github.com/devonartis/agentwrit) instance.
67+
**Requirements:** Python 3.10+. The SDK also needs a broker and credentials — see [Prerequisites](#prerequisites).
68+
69+
## Prerequisites
70+
71+
The SDK is a client. It does **not** run the broker, and it does **not** mint its own credentials. Before any code in [Quick Start](#quick-start) will work, you need three things:
72+
73+
**1. A reachable AgentWrit broker.**
74+
The broker is a separate service that issues and validates tokens.
75+
76+
- *Have a platform team running one?* Ask them for the broker URL.
77+
- *Running it yourself?* Stand one up locally — the [broker repo](https://github.com/devonartis/agentwrit) ships a `docker compose` setup. From this repo:
78+
```bash
79+
docker compose up -d # pulls devonartis/agentwrit from Docker Hub
80+
```
81+
82+
**2. App credentials (`client_id` + `client_secret`).**
83+
These are issued by the **broker operator/admin** when they register your app and set its scope ceiling. The SDK cannot create them for you.
84+
85+
- *Have a broker admin?* Ask them to register your app and send you the `client_id` and `client_secret`.
86+
- *You are the admin?* Use the included setup script (it registers an app and prints both values):
87+
```bash
88+
export AGENTWRIT_ADMIN_SECRET="<your-broker-admin-secret>"
89+
uv run python demo/setup.py
90+
```
91+
92+
**3. Environment variables set** on the process that uses the SDK:
93+
```bash
94+
export AGENTWRIT_BROKER_URL="http://localhost:8080" # from step 1
95+
export AGENTWRIT_CLIENT_ID="<from step 2>"
96+
export AGENTWRIT_CLIENT_SECRET="<from step 2>"
97+
```
98+
99+
> Auth is lazy — the SDK doesn't talk to the broker until your first `create_agent()` call. If that call raises `AuthenticationError`, your `client_id` or `client_secret` is wrong (or the operator rotated them). If it raises `TransportError`, the broker URL is unreachable.
54100
55101
## Quick Start
56102

103+
> Assumes [Prerequisites](#prerequisites) are met — broker reachable, app registered, env vars set.
104+
57105
```python
58106
import os
59107
from agentwrit import AgentWritApp, validate
60108

61109
# Connect to the broker (lazy — no auth until first create_agent)
62110
app = AgentWritApp(
63111
broker_url=os.environ["AGENTWRIT_BROKER_URL"],
64-
client_id=os.environ["AGENTWRIT_CLIENT_ID"],
65-
client_secret=os.environ["AGENTWRIT_CLIENT_SECRET"],
112+
client_id=os.environ["AGENTWRIT_CLIENT_ID"], # from broker admin
113+
client_secret=os.environ["AGENTWRIT_CLIENT_SECRET"], # from broker admin
66114
)
67115

68116
# Create an agent with specific scope
@@ -101,7 +149,7 @@ print(agent.expires_in) # 300 (seconds)
101149
# Renew — new token, same identity, old token revoked
102150
agent.renew()
103151

104-
# Delegate — give narrower scope to another agent
152+
# Delegate — pass a subset of scope to another agent (equal or narrower)
105153
delegated = agent.delegate(delegate_to=other.agent_id, scope=["read:data:x"])
106154

107155
# Release — self-revoke, idempotent
@@ -112,7 +160,7 @@ agent.release()
112160

113161
The [`demo/`](demo/) directory contains **MedAssist AI** — an interactive healthcare demo that showcases every AgentWrit capability against a live broker.
114162

115-
**What it does:** A FastAPI web app where you enter a patient ID and a plain-language request. A local LLM (OpenAI-compatible) chooses which tools to call. The app dynamically creates broker agents with only the scopes those tools need, for that specific patient. You see scope enforcement, cross-patient denial, delegation, token renewal, and release — all in a real-time execution trace.
163+
**What it does:** A FastAPI web app where you enter a patient ID and a plain-language request. A local LLM (OpenAI-compatible) chooses which tools to call, and the app dynamically creates broker agents with only the scopes those tools need for that specific patient. Every step — scope enforcement, cross-patient denial, delegation, token renewal, release — appears in a real-time execution trace.
116164

117165
**What it demonstrates:**
118166

@@ -169,7 +217,7 @@ scope_is_subset(["read:logs:customers"], ["read:data:*"]) # False (logs != d
169217

170218
## Delegation
171219

172-
Agents delegate narrower scope to other agents. Authority can only narrow, never widen.
220+
Agents delegate a subset of their scope to other agents. Delegation cannot widen authority — equal or narrower scope is accepted; any scope the delegator doesn't hold is rejected.
173221

174222
```python
175223
# A has broad scope
@@ -243,7 +291,7 @@ Application (your code — AgentWritApp)
243291
│ creates agents within ceiling
244292
245293
Agent (ephemeral SPIFFE identity + scoped JWT)
246-
scope can only narrow on delegation
294+
delegation cannot widen scope (equal or narrower allowed)
247295
248296
Delegated Agent (sub-agent, max 5 hops)
249297
```

demo/BEGINNERS_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Traditional setups often give every service (or every “agent”) the **same lo
1414
- **What task** they are doing,
1515
- **Which scopes** they are allowed to use (often **per patient**, per action).
1616

17-
The **broker** is the authority: it registers apps, mints tokens, validates them, supports **delegation** (narrowing authority), **renewal**, **release**, and **revocation**, and writes an **audit trail**.
17+
The **broker** is the authority: it registers apps, mints tokens, validates them, supports **delegation** (passing scope to another agent — equal or narrower, never widening), **renewal**, **release**, and **revocation**, and writes an **audit trail**.
1818

1919
---
2020

@@ -144,7 +144,7 @@ flowchart LR
144144

145145
## 7. Diagram: delegation (prescription write)
146146

147-
Delegation is **authority narrowing**: the clinical agent already has `write:prescriptions:{pid}`; it asks the broker to issue a **delegated token** for the prescription agents SPIFFE ID with **only** that scope (or a subset).
147+
Delegation is **bounded authority transfer**the broker rejects any attempt to widen past the delegator's scope. In this demo, the clinical agent already has `write:prescriptions:{pid}` and asks the broker to issue a **delegated token** for the prescription agent's SPIFFE ID with that same scope (or a narrower subset). Equal-scope and narrower delegation are both valid; widening is rejected.
148148

149149
```mermaid
150150
flowchart LR

demo/PRESENTERS_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Do **4–6** runs in this order so the room sees escalation, not repetition.
5757
**What to point at:**
5858
- **Clinical** agent, then **billing**, then **prescription** as the LLM calls tools.
5959
- **Delegation** step when a prescription write is allowed: clinical delegates `write:prescriptions:{pid}` to the Rx agent.
60-
- That is **authority narrowing**, not “same API key everywhere.”
60+
- That is **bounded authority transfer** — the broker rejects any widening past the delegator's scope — not “same API key everywhere.”
6161

6262
---
6363

demo/templates/operator.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ <h2>Broker Health</h2>
3838
<!-- Scope Ceiling -->
3939
<div class="panel">
4040
<h2>App Scope Ceiling</h2>
41-
<p class="description">The maximum scopes this application can grant to agents. Set by the operator at app registration. Each agent gets a strict subset, scoped to a specific patient.</p>
41+
<p class="description">The maximum scopes this application can grant to agents. Set by the operator at app registration. Each agent gets a subset of this ceiling, typically scoped to a specific patient.</p>
4242
<div class="scope-list">
4343
{% for scope in scope_ceiling %}
4444
<span class="scope-badge ceiling">{{ scope }}</span>

docs/api-reference.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# API Reference
22

3-
Complete reference for the AgentWrit Python SDK public API.
3+
Complete reference for the AgentWrit Python SDK public API. Companion to [Getting Started](getting-started.md) and [Developer Guide](developer-guide.md).
4+
5+
[AgentWritApp](#agentwritapp) · [Agent](#agent) · [Module Functions](#module-level-functions) · [Data Classes](#data-classes) · [Exceptions](#exceptions) · [Broker Endpoints](#broker-endpoint-mapping)
46

57
---
68

@@ -97,7 +99,18 @@ Shortcut for `agentwrit.validate(app.broker_url, token)`.
9799
app.close() -> None
98100
```
99101

100-
Closes the underlying HTTP transport. Call this when you're done with the app to release connections.
102+
Closes the underlying HTTP transport. Call this when you're done with the app to release the connection pool.
103+
104+
> `AgentWritApp` is **not** a context manager — `with AgentWritApp(...) as app:` will raise `AttributeError`. Use a `try/finally` instead:
105+
>
106+
> ```python
107+
> app = AgentWritApp(...)
108+
> try:
109+
> agent = app.create_agent(...)
110+
> # ... use agent ...
111+
> finally:
112+
> app.close()
113+
> ```
101114
102115
---
103116
@@ -255,7 +268,7 @@ from agentwrit import AgentClaims
255268
| `exp` | `int` | Expiration (Unix timestamp) |
256269
| `nbf` | `int` | Not before (Unix timestamp) |
257270
| `iat` | `int` | Issued at (Unix timestamp) |
258-
| `jti` | `str` | Unique token ID (32 hex chars) |
271+
| `jti` | `str` | Unique token ID (broker-assigned; format is broker-defined, do not assume a fixed length) |
259272
| `scope` | `list[str]` | Granted scope |
260273
| `task_id` | `str` | Task identifier |
261274
| `orch_id` | `str` | Orchestrator identifier |
@@ -320,6 +333,25 @@ RFC 7807 structured error from the broker.
320333
| `request_id` | `str \| None` | Broker-generated trace ID |
321334
| `hint` | `str \| None` | Optional guidance for resolution |
322335

336+
#### Known `error_code` values
337+
338+
> Observed broker behavior at the time of writing. The broker's [api.md](https://github.com/devonartis/agentwrit/blob/main/docs/api.md) is the authoritative source — consult it for additions or renames.
339+
340+
| `error_code` | HTTP | Fires when |
341+
|--------------|------|------------|
342+
| `invalid_request` | 400 | Malformed JSON, missing required field, invalid format |
343+
| `invalid_ttl` | 400 | Requested `token_ttl` outside the broker's allowed range |
344+
| `unauthorized` | 401 | Missing / invalid / expired credentials; failed token verification; Bearer header missing or malformed |
345+
| `forbidden` | 403 | Admin-side app-not-found or scope-ceiling check during admin launch-token creation |
346+
| `insufficient_scope` | 403 | Caller's token is valid but lacks the scope required for this endpoint (emitted by the validation middleware) — **or** the token has been revoked |
347+
| `scope_violation` | 403 | `/v1/register` or `/v1/delegate` rejected the requested scope as exceeding the caller's authority |
348+
| `not_found` | 404 | App or delegate agent does not exist |
349+
| `payload_too_large` | 413 | Request body exceeded 1 MB |
350+
| `rate_limited` | 429 | Rate limit hit on the auth endpoints |
351+
| `internal_error` | 500 | Server-side failure (launch-token creation, registration, delegation, etc.) |
352+
353+
> **Watch the 403 split.** Both `scope_violation` and `insufficient_scope` surface as `AuthorizationError` in the SDK. If you switch on `error_code`, handle both: `scope_violation` covers "your request exceeded your authority"; `insufficient_scope` covers "your token doesn't have the scope this endpoint needs, or the token was revoked."
354+
323355
### RegisterResult
324356

325357
```python
@@ -365,9 +397,11 @@ Raised on HTTP 401. App credentials are invalid.
365397

366398
### AuthorizationError
367399

368-
Raised on HTTP 403. Scope violation — either:
369-
- Requested scope exceeds app ceiling (during `create_agent()`)
370-
- Delegated scope exceeds delegator's scope (during `delegate()`)
400+
Raised on HTTP 403. Three distinct sub-cases, all surface through this class — inspect `e.problem.error_code` to differentiate:
401+
402+
- **Scope ceiling violation** — requested scope exceeds the app's ceiling during `create_agent()` (`error_code: scope_violation`)
403+
- **Delegation violation** — delegated scope exceeds delegator's scope, or exceeds broker-enforced delegation depth, during `delegate()` (`error_code: scope_violation`)
404+
- **Token revocation or insufficient scope at the endpoint** — caller's token was revoked, or lacks the scope the endpoint requires (`error_code: insufficient_scope`)
371405

372406
### RateLimitError
373407

0 commit comments

Comments
 (0)