agent-authority 0.1.0
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.
- package/CHANGELOG.md +118 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +91 -0
- package/README.md +553 -0
- package/dist/a2a.d.ts +73 -0
- package/dist/a2a.d.ts.map +1 -0
- package/dist/a2a.js +117 -0
- package/dist/a2a.js.map +1 -0
- package/dist/audit.d.ts +12 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +52 -0
- package/dist/audit.js.map +1 -0
- package/dist/behalf.d.ts +173 -0
- package/dist/behalf.d.ts.map +1 -0
- package/dist/behalf.js +475 -0
- package/dist/behalf.js.map +1 -0
- package/dist/capability.d.ts +56 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +176 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +273 -0
- package/dist/cli.js.map +1 -0
- package/dist/control-plane.d.ts +57 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +332 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/crypto.d.ts +68 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +105 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +17 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +75 -0
- package/dist/lint.js.map +1 -0
- package/dist/mandate.d.ts +99 -0
- package/dist/mandate.d.ts.map +1 -0
- package/dist/mandate.js +141 -0
- package/dist/mandate.js.map +1 -0
- package/dist/mcp-server.d.ts +26 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +111 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +63 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +123 -0
- package/dist/mcp.js.map +1 -0
- package/dist/persist.d.ts +51 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +150 -0
- package/dist/persist.js.map +1 -0
- package/dist/quickstart.d.ts +63 -0
- package/dist/quickstart.d.ts.map +1 -0
- package/dist/quickstart.js +171 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/remote.d.ts +93 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +120 -0
- package/dist/remote.js.map +1 -0
- package/dist/seal.d.ts +12 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +96 -0
- package/dist/seal.js.map +1 -0
- package/dist/store.d.ts +119 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +139 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/llms.txt +106 -0
- package/package.json +107 -0
- package/schemas/capability.schema.json +14 -0
- package/schemas/mandate.schema.json +68 -0
- package/vectors/mandate-vector.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# agent-authority
|
|
2
|
+
|
|
3
|
+
> **Authorization for AI agents** — verifiable, scoped, revocable capability
|
|
4
|
+
> tokens with delegation, for MCP and A2A. Project name: **Behalf**. Zero
|
|
5
|
+
> dependencies, TypeScript **and** Python, offline-verifiable. Five verbs.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/agent-authority)
|
|
8
|
+
[](https://pypi.org/project/agent-authority/)
|
|
9
|
+
[](https://github.com/novaai0401-ui/agent-authority/actions/workflows/ci.yml)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+
**agent-authority** is the reference implementation of **agent authority**: the
|
|
13
|
+
authorization and delegation layer for AI agents. It gives any agent — and any
|
|
14
|
+
sub-agent it delegates to — a **verifiable, scoped, time-bound, revocable**
|
|
15
|
+
identity and permission chain, so a tool server, MCP host, or agent-to-agent
|
|
16
|
+
(A2A) call can answer *"is this agent actually allowed to do this, right now?"*
|
|
17
|
+
offline, with only a public key.
|
|
18
|
+
|
|
19
|
+
It solves **AI agent authorization / agent permissions** with **capability-based
|
|
20
|
+
security**: least-privilege, attenuable (macaroon/biscuit-style) **capability
|
|
21
|
+
tokens**, an **OAuth 2.1 on-behalf-of**–style principal→agent grant, and
|
|
22
|
+
**SPIFFE/SVID**-style cryptographic agent identity — without cloud, model, or
|
|
23
|
+
framework lock-in.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install agent-authority # Node / TypeScript
|
|
27
|
+
pip install agent-authority # Python (add the [seal] extra for sealed credentials)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Agent authority is becoming required infrastructure: multi-agent systems are
|
|
31
|
+
already the norm, yet most tool servers ship with no auth at all. The *standard*
|
|
32
|
+
for agent identity and delegation is being defined by NIST, the IETF, and the
|
|
33
|
+
Linux Foundation's Agentic AI Foundation. This project doesn't try to win that
|
|
34
|
+
race — it's the clean, neutral, AI-legible **implementation** of it. The
|
|
35
|
+
`requests` of the agent era: MIT-licensed, the install nobody reinvents.
|
|
36
|
+
|
|
37
|
+
Everything is one primitive — a **Mandate**: a signed, scoped, time-bound
|
|
38
|
+
capability token that proves *who authorized what, within which limits, and
|
|
39
|
+
through which chain of agents.*
|
|
40
|
+
|
|
41
|
+
## Secure an entire agent in ~6 lines
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { withBehalf } from "agent-authority/mcp";
|
|
45
|
+
|
|
46
|
+
const server = withBehalf(myMcpServer, {
|
|
47
|
+
policy: {
|
|
48
|
+
send_email: "write:email",
|
|
49
|
+
read_calendar: "read:calendar",
|
|
50
|
+
transfer_funds: (args) => `spend:usd<=${args.amount}`,
|
|
51
|
+
},
|
|
52
|
+
onDenied: "throw", // or "prompt" for just-in-time user consent
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every tool call through that server is now automatically checked against the
|
|
57
|
+
caller's mandate — scope, limits, expiry, revocation, and audit — with no
|
|
58
|
+
per-tool code.
|
|
59
|
+
|
|
60
|
+
## The five verbs
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { Behalf } from "agent-authority";
|
|
64
|
+
|
|
65
|
+
// 1. GRANT — a user authorizes an agent: scoped, capped, short-lived
|
|
66
|
+
const mandate = await Behalf.grant({
|
|
67
|
+
principal: user.id,
|
|
68
|
+
agent: "research-agent",
|
|
69
|
+
can: ["read:calendar", "spend:usd<=50"],
|
|
70
|
+
expiresIn: "1h",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 2. AUTHORIZE — before any action, prove authority (throws if denied)
|
|
74
|
+
await mandate.authorize("spend:usd=20");
|
|
75
|
+
|
|
76
|
+
// 3. ATTENUATE — hand a narrowed mandate to a sub-agent; can only shrink scope
|
|
77
|
+
const child = mandate.attenuate({ can: ["read:calendar"], expiresIn: "10m" });
|
|
78
|
+
|
|
79
|
+
// 4. REVOKE — kill a mandate and its whole downstream chain, instantly
|
|
80
|
+
await Behalf.revoke(mandate.id);
|
|
81
|
+
|
|
82
|
+
// 5. AUDIT — every authorize() already wrote a hash-chained record
|
|
83
|
+
const trail = await Behalf.audit(mandate.id);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
That's the entire core surface. Five verbs: **grant, authorize, attenuate,
|
|
87
|
+
revoke, audit.** There is no sixth.
|
|
88
|
+
|
|
89
|
+
## Capability grammar
|
|
90
|
+
|
|
91
|
+
Scopes are both human- and machine-writable:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
read:calendar # simple capability
|
|
95
|
+
write:repo/acme-app # resource-scoped (path segments)
|
|
96
|
+
spend:usd<=50 # quantitative limit
|
|
97
|
+
send:email rate<=10/h # rate limit
|
|
98
|
+
* # wildcard (discouraged; lint warns)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When authorizing, name a concrete amount — `spend:usd=20` is checked against the
|
|
102
|
+
grant's `spend:usd<=50`.
|
|
103
|
+
|
|
104
|
+
## Design principles (non-negotiable)
|
|
105
|
+
|
|
106
|
+
1. **Thin & end-of-chain.** Zero runtime dependencies — built on the platform's
|
|
107
|
+
own crypto.
|
|
108
|
+
2. **One obvious way.** Exactly one canonical method per task.
|
|
109
|
+
3. **AI-legible by default.** Ships with an MCP server, [`llms.txt`](./llms.txt),
|
|
110
|
+
and typed [schemas](./schemas) so coding agents discover and call it correctly.
|
|
111
|
+
4. **Standards-tracking, not standards-defining.** A clean facade over
|
|
112
|
+
SPIFFE / OAuth 2.1 OBO / capability tokens.
|
|
113
|
+
5. **Neutral.** No cloud, model, or framework lock-in.
|
|
114
|
+
6. **Offline-verifiable.** Signature chain, scope, expiry, and proof of
|
|
115
|
+
possession are all checked locally (asymmetric, public-key only). Only
|
|
116
|
+
revocation and the shared rate cap need a network check.
|
|
117
|
+
|
|
118
|
+
### The intersection rule is structural
|
|
119
|
+
|
|
120
|
+
A mandate is an **Ed25519 signature chain** (biscuit-style). Block 0 (the root
|
|
121
|
+
grant) is signed by the issuer; each block publishes a fresh public key, and the
|
|
122
|
+
*next* block is signed by the matching private key. A holder attenuates by
|
|
123
|
+
*appending* a narrowing block signed with the key it was handed — keylessly with
|
|
124
|
+
respect to the issuer, and offline. An agent's effective authority is always:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
principal's grant ∩ every narrowing along the chain
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A compromised middle agent can never widen scope; no block can be edited,
|
|
131
|
+
removed from the middle, or **truncated** off the end. Editing/middle-removal is
|
|
132
|
+
blocked by the signature chain; truncation is blocked by a **proof of
|
|
133
|
+
possession** at authorize time (below). This closes the OAuth
|
|
134
|
+
"delegation-chain splicing" weakness — see [`test/delegation.test.ts`](./test/delegation.test.ts)
|
|
135
|
+
and [`test/asymmetric.test.ts`](./test/asymmetric.test.ts).
|
|
136
|
+
|
|
137
|
+
### Authorizing requires proving possession (no bearer tokens)
|
|
138
|
+
|
|
139
|
+
A serialized mandate is **not** a usable bearer credential. To authorize, the
|
|
140
|
+
holder must prove possession of the chain's *terminal* private key — the one each
|
|
141
|
+
delegation hands to the next agent. A truncated prefix would need that prefix's
|
|
142
|
+
terminal key, which a downstream holder does not have, so it cannot escalate by
|
|
143
|
+
dropping its own block.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// In-process holder: mandate.authorize() mints + checks the proof for you.
|
|
147
|
+
await mandate.authorize("spend:usd=20");
|
|
148
|
+
|
|
149
|
+
// Across a trust boundary, the holder presents the token + a fresh,
|
|
150
|
+
// action-bound proof; the verifier needs only the issuer's PUBLIC key:
|
|
151
|
+
const verifier = createBehalf({ trust: [issuer.publicKey] });
|
|
152
|
+
await verifier.authorize(mandate.token, "spend:usd=20", mandate.prove("spend:usd=20"));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For an advisory "would this token's scope allow X?" check that does **not** prove
|
|
156
|
+
possession (e.g. tooling/dashboards), use `engine.inspect(token, action)`. Over
|
|
157
|
+
HTTP, `agent-authority/a2a`'s `present()` attaches the proof automatically.
|
|
158
|
+
|
|
159
|
+
Proofs are bound to the action and fresh within `proofSkewMs` (default 5 min).
|
|
160
|
+
For **true single-use anti-replay**, the verifier issues a challenge:
|
|
161
|
+
`const nonce = verifier.challenge()` → holder binds it with
|
|
162
|
+
`mandate.prove(action, { nonce })` → the verifier consumes it on use. Engines
|
|
163
|
+
created with `requireNonce: true` refuse nonce-less proofs entirely.
|
|
164
|
+
|
|
165
|
+
There are two serializations, and the difference matters:
|
|
166
|
+
|
|
167
|
+
- **`mandate.serialize()`** — the public token. Safe to show anyone; after
|
|
168
|
+
`import` it can be inspected and verified, but **not** authorized, proved, or
|
|
169
|
+
delegated (it carries no key).
|
|
170
|
+
- **`mandate.serializeWithKey()`** — the holder credential (token **+**
|
|
171
|
+
delegation key). This is how you hand a delegated mandate to a sub-agent in
|
|
172
|
+
another process: after `import` it has full holder powers. **Treat it as a
|
|
173
|
+
secret** and deliver it only over a secure channel.
|
|
174
|
+
|
|
175
|
+
### Binding a mandate to an agent identity (SVID-style)
|
|
176
|
+
|
|
177
|
+
A holder credential is a secret, so a leak is a real risk. Bind the mandate to a
|
|
178
|
+
specific agent identity and a stolen credential is **inert without the agent's
|
|
179
|
+
private key** — possession of the credential is no longer sufficient to act.
|
|
180
|
+
|
|
181
|
+
Grant (or attenuate) with `bindAgent` set to the agent's public key; that adds an
|
|
182
|
+
`agentKey` caveat. Authorizing then requires a proof of possession of the
|
|
183
|
+
matching private key, in addition to the chain's terminal key:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const agent = newKeyPair(); // the agent's long-lived identity
|
|
187
|
+
const mandate = issuer.grant({
|
|
188
|
+
principal: user.id,
|
|
189
|
+
agent: "research-agent",
|
|
190
|
+
can: ["spend:usd<=50"],
|
|
191
|
+
expiresIn: "1h",
|
|
192
|
+
bindAgent: exportPublicKey(agent.publicKey), // ← cryptographic binding
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// The agent proves BOTH the terminal key and its identity:
|
|
196
|
+
await verifier.authorize(
|
|
197
|
+
mandate.token,
|
|
198
|
+
"spend:usd=20",
|
|
199
|
+
mandate.prove("spend:usd=20", { agentKeys: [agent] }),
|
|
200
|
+
);
|
|
201
|
+
// An engine configured with `agentKey: agent` proves it automatically on the
|
|
202
|
+
// in-process path (mandate.authorize(...)) and over A2A via present(..., { agentKeys }).
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Bindings are **conjunctive**: every `agentKey` caveat in the chain must be
|
|
206
|
+
satisfied. A thief who steals the credential cannot strip the caveat (it is
|
|
207
|
+
signed into a block) and cannot bypass it by appending their own binding — doing
|
|
208
|
+
so only adds another requirement. The agent's private key is provisioned out of
|
|
209
|
+
band; Behalf never puts it on the wire.
|
|
210
|
+
|
|
211
|
+
### Sealing a holder credential (encrypted delivery)
|
|
212
|
+
|
|
213
|
+
`serializeWithKey()` is a secret. `bindAgent` makes a *stolen* one inert; for the
|
|
214
|
+
delivery channel itself, **seal** the credential to the recipient so it's
|
|
215
|
+
unreadable to anyone in between:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { newSealKeyPair } from "agent-authority";
|
|
219
|
+
|
|
220
|
+
const recipient = newSealKeyPair(); // recipient's X25519 sealing key
|
|
221
|
+
// ...recipient publishes recipient.publicKey...
|
|
222
|
+
|
|
223
|
+
const sealed = mandate.sealForRecipient(recipient.publicKey); // encrypted blob
|
|
224
|
+
// ...deliver `sealed` over any channel...
|
|
225
|
+
const mine = engine.importSealed(sealed, recipient); // only the recipient opens it
|
|
226
|
+
await mine.authorize("read:calendar");
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The scheme (`seal-1`) is ephemeral X25519 → HKDF-SHA256 → AES-256-GCM and is
|
|
230
|
+
**wire-compatible across both ports** (seal in TypeScript, open in Python or vice
|
|
231
|
+
versa). It's native in Node; in Python it needs the optional `cryptography`
|
|
232
|
+
package (the rest of the port stays dependency-free, and `importSealed` raises a
|
|
233
|
+
clear error if it's missing). The sealing key is X25519 and is *separate* from
|
|
234
|
+
the Ed25519 `bindAgent` identity — combine both for delivery + use protection.
|
|
235
|
+
|
|
236
|
+
### Issuer key rotation (with overlap)
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const v2 = v1.rotate(); // fresh key, same stores, still trusts v1
|
|
240
|
+
// 1) distribute v2.publicKey to verifiers (verifier.trustKey(v2.publicKey))
|
|
241
|
+
// 2) new grants are signed by v2; old mandates keep verifying
|
|
242
|
+
// 3) after the longest outstanding mandate expires:
|
|
243
|
+
v2.untrustKey(v1.publicKey); // end the overlap — old-key mandates retire
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Audit checkpoints (anchoring)
|
|
247
|
+
|
|
248
|
+
The audit log is an unkeyed hash chain — verifiable, but a writer with store
|
|
249
|
+
access could rewrite it and tail-deletion is invisible. `checkpointAudit()`
|
|
250
|
+
signs the current head; store the checkpoint **out of the writer's reach** and
|
|
251
|
+
`verifyAuditCheckpoint(cp)` later detects tail-deletion and rewrites:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const cp = await engine.checkpointAudit(); // ship to object storage / a ledger
|
|
255
|
+
// later, e.g. nightly:
|
|
256
|
+
const { ok, reason } = await engine.verifyAuditCheckpoint(cp);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## What maps to the standard underneath
|
|
260
|
+
|
|
261
|
+
| Behalf concept | Standard it tracks |
|
|
262
|
+
| ------------------------------- | ----------------------------------------------- |
|
|
263
|
+
| Mandate (capability token) | Agentic JWT / capability tokens (IBCT-style) |
|
|
264
|
+
| `attenuate()` (holder narrowing)| DeepMind macaroon-style attenuation |
|
|
265
|
+
| Agent identity | SPIFFE / SVID compatible |
|
|
266
|
+
| Principal → agent grant | OAuth 2.1 On-Behalf-Of / token exchange (RFC 8693) |
|
|
267
|
+
| Audit record | provenance / non-repudiation records |
|
|
268
|
+
| Transport bindings | MCP + A2A authorization layers |
|
|
269
|
+
|
|
270
|
+
Because the 5-line API is a facade, the standard can evolve underneath without
|
|
271
|
+
breaking anyone's code.
|
|
272
|
+
|
|
273
|
+
## Install & develop
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm install # dev deps only (typescript, @types/node)
|
|
277
|
+
npm run build # compile to dist/
|
|
278
|
+
npm test # 105 tests across capability/mandate/delegation/revocation/audit/mcp/asymmetric/persist/server/a2a/lint/control-plane/quickstart
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Run the reference integrations:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm run example:data-access # a read-only data agent
|
|
285
|
+
npm run example:spend # a budget- and rate-limited spend agent
|
|
286
|
+
npm run example:delegation # two-agent attenuation + cascade revoke
|
|
287
|
+
npm run example:a2a # agent-to-agent delegation over HTTP
|
|
288
|
+
npm run example:control-plane # revocation propagation across agents
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### CLI
|
|
292
|
+
|
|
293
|
+
After `npm run build`, the `agent-authority` CLI manages mandates from the terminal
|
|
294
|
+
(state lives under `$BEHALF_HOME`, default `~/.behalf`):
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
node dist/cli.js pubkey
|
|
298
|
+
M=$(node dist/cli.js grant --principal alice --agent research \
|
|
299
|
+
--can "read:calendar" --can "spend:usd<=50" --expires 1h)
|
|
300
|
+
# $M is a HOLDER credential (includes the delegation key — keep it secret).
|
|
301
|
+
# Add --public to emit the presentation-only token instead.
|
|
302
|
+
node dist/cli.js inspect "$M"
|
|
303
|
+
node dist/cli.js authorize "$M" "spend:usd=20" # ALLOW (real check, proof of possession)
|
|
304
|
+
node dist/cli.js authorize "$M" "spend:usd=99" # DENY
|
|
305
|
+
node dist/cli.js revoke <mandate-id>
|
|
306
|
+
node dist/cli.js audit <mandate-id>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### MCP server
|
|
310
|
+
|
|
311
|
+
A dependency-free stdio MCP server exposes the discovery tools to any MCP client:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
node dist/mcp-server.js # speaks JSON-RPC 2.0 over stdio
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```jsonc
|
|
318
|
+
// register with an MCP client, e.g.:
|
|
319
|
+
{ "mcpServers": { "agent-authority": { "command": "node", "args": ["dist/mcp-server.js"] } } }
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Quickstarts for any AI
|
|
323
|
+
|
|
324
|
+
`agent-authority quickstart` generates the wiring for any surface — Claude Code, Cursor,
|
|
325
|
+
Copilot, Windsurf, Gemini CLI, OpenAI Agents (MCP), and GPT / Gemini APIs
|
|
326
|
+
(function tools). Any other AI is configurable via a custom surface file or the
|
|
327
|
+
generic MCP template. See [QUICKSTART.md](./QUICKSTART.md).
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
agent-authority quickstart --list
|
|
331
|
+
agent-authority quickstart claude-code
|
|
332
|
+
agent-authority quickstart gpt # OpenAI function tools
|
|
333
|
+
agent-authority quickstart my-agent --surfaces ./surfaces.json # bring your own AI
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### A2A — agent-to-agent over HTTP
|
|
337
|
+
|
|
338
|
+
`agent-authority/a2a` carries a verifiable delegation chain across the network. The caller
|
|
339
|
+
attaches its mandate (optionally attenuating it first); the callee verifies the
|
|
340
|
+
chain offline with only the issuer's public key, then authorizes the action:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { behalfFetch, guard } from "agent-authority/a2a";
|
|
344
|
+
|
|
345
|
+
// callee: a node:http middleware that authorizes each request
|
|
346
|
+
const gate = guard({ engine: callee, capability: () => "spend:usd<=50" });
|
|
347
|
+
// ... in your http handler: if (!(await gate(req, res))) return;
|
|
348
|
+
|
|
349
|
+
// caller: forward the mandate, narrowed so the callee gets strictly less
|
|
350
|
+
await behalfFetch(url, mandate, { method: "POST" },
|
|
351
|
+
{ attenuate: { can: ["spend:usd<=20"] } });
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Capability linting
|
|
355
|
+
|
|
356
|
+
`lint()` flags loose scopes (`*`, unbounded `spend:`, rate-less `send:`, ...) so
|
|
357
|
+
agents and humans write tight capabilities by default:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { lint } from "agent-authority";
|
|
361
|
+
lint(["spend:usd", "*"]); // → warnings: add a limit; avoid wildcard
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Also available as `agent-authority lint <cap> ...` on the CLI.
|
|
365
|
+
|
|
366
|
+
### Persistence
|
|
367
|
+
|
|
368
|
+
`FileRevocationStore` and `FileAuditStore` keep revocation and audit state across
|
|
369
|
+
restarts with zero infrastructure:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import { createBehalf, FileRevocationStore, FileAuditStore } from "agent-authority";
|
|
373
|
+
const behalf = createBehalf({
|
|
374
|
+
revocations: new FileRevocationStore("./revocations.json"),
|
|
375
|
+
audit: new FileAuditStore("./audit.jsonl"),
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Control plane (revocation propagation + audit retention)
|
|
380
|
+
|
|
381
|
+
For multi-agent deployments, the control plane centralizes revocation (revoke
|
|
382
|
+
once, every agent sees it), retains one hash-chained audit log (integrity-
|
|
383
|
+
chained; see Limitations for its threat model), and offers a
|
|
384
|
+
consent/policy surface with a dashboard at `/`. It's a thin HTTP service over the
|
|
385
|
+
same stores — point agents at it with the `agent-authority/remote` client stores and the
|
|
386
|
+
five-verb API is unchanged.
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
node dist/control-plane.js # bin: agent-authority-control-plane; dashboard at /
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { createBehalf } from "agent-authority";
|
|
394
|
+
import { HttpRevocationStore, HttpAuditStore, HttpRateStore } from "agent-authority/remote";
|
|
395
|
+
|
|
396
|
+
const behalf = createBehalf({
|
|
397
|
+
revocations: new HttpRevocationStore("http://localhost:8787"),
|
|
398
|
+
audit: new HttpAuditStore("http://localhost:8787"),
|
|
399
|
+
rate: new HttpRateStore("http://localhost:8787"),
|
|
400
|
+
});
|
|
401
|
+
// revoke(id) propagates to every agent; audit is sealed centrally (race-free);
|
|
402
|
+
// and a `rate<=N/h` cap is enforced ONCE across all agents, not per process.
|
|
403
|
+
//
|
|
404
|
+
// Multi-tenant: createControlPlane({ tenants: { tokenA: issuerAPubKey } }) — a
|
|
405
|
+
// tenant token reads/writes only its own issuer's audit; `token` is admin.
|
|
406
|
+
|
|
407
|
+
// Optional: cache revocation checks with a bounded staleness window.
|
|
408
|
+
// import { CachingRevocationStore } from "agent-authority";
|
|
409
|
+
// revocations: new CachingRevocationStore(new HttpRevocationStore(url), { ttlMs: 5000 })
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Cross-language interop
|
|
413
|
+
|
|
414
|
+
A mandate issued by either reference port verifies in the other: both encode keys
|
|
415
|
+
as raw Ed25519 (base64url) and compute **sorted-key canonical JSON** for the
|
|
416
|
+
signed bytes, so a TS-issued mandate authorizes under the Python verifier and
|
|
417
|
+
vice versa — including attenuated multi-block chains and the action-bound
|
|
418
|
+
possession proof. A committed fixture, [`vectors/mandate-vector.json`](./vectors/mandate-vector.json),
|
|
419
|
+
is verified by *both* test suites so the wire format can't drift; any third-party
|
|
420
|
+
implementation should verify it too.
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npm run test:interop # PY⇄TS, issue in one port, verify/authorize in the other
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Python
|
|
427
|
+
|
|
428
|
+
An identical-shape port lives in [`python/`](./python):
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
cd python
|
|
432
|
+
python3 -m unittest discover -s tests # 85 tests, zero dependencies
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from agent_authority import create_behalf
|
|
437
|
+
|
|
438
|
+
b = create_behalf()
|
|
439
|
+
mandate = b.grant(
|
|
440
|
+
principal="alice", agent="research-agent",
|
|
441
|
+
can=["read:calendar", "spend:usd<=50"], expires_in="1h",
|
|
442
|
+
)
|
|
443
|
+
mandate.authorize("spend:usd=20")
|
|
444
|
+
child = mandate.attenuate(can=["read:calendar"], expires_in="10m")
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## What ships
|
|
448
|
+
|
|
449
|
+
- **`agent-authority`** (npm) — the core TypeScript library, near-zero deps.
|
|
450
|
+
- **`agent-authority/mcp`** + **`agent-authority/a2a`** — drop-in enforcement middleware.
|
|
451
|
+
- **`agent-authority`** (PyPI) — Python port, identical API shape.
|
|
452
|
+
- **MCP server + `llms.txt` + typed schemas** — the agent-adoption kit.
|
|
453
|
+
- **Three reference integrations** — data-access, spend-limited, two-agent delegation.
|
|
454
|
+
|
|
455
|
+
## Status
|
|
456
|
+
|
|
457
|
+
Beyond the initial MVP, this now includes **Ed25519 asymmetric verification**
|
|
458
|
+
(any party verifies offline with just the issuer public key), **file-backed
|
|
459
|
+
persistence** for revocation + audit, a **`agent-authority` CLI**, a **dependency-free
|
|
460
|
+
stdio MCP server**, an **A2A HTTP transport** that carries the verifiable chain
|
|
461
|
+
between agents, **capability linting**, **cross-language wire interop**
|
|
462
|
+
(TS⇄Python mandates verify in either port), and a **control plane** for
|
|
463
|
+
revocation propagation, audit retention, and consent/policy with a dashboard, and
|
|
464
|
+
**dynamic per-surface quickstarts** that wire Behalf into any AI (Claude Code,
|
|
465
|
+
Cursor, Copilot, Gemini, GPT, or a custom surface). CI runs both test suites plus
|
|
466
|
+
the interop check on Node 20/22 and Python 3.9/3.12.
|
|
467
|
+
|
|
468
|
+
All control-plane state can be file-backed for durability — revocation, audit,
|
|
469
|
+
and now consent + policy (`FileConsentStore`, `FilePolicyStore`); the
|
|
470
|
+
`agent-authority-control-plane` bin persists everything under `$BEHALF_HOME`. The Python
|
|
471
|
+
port has full parity: not just the library and control plane, but the tooling
|
|
472
|
+
too — the `agent-authority` CLI, the `agent-authority-mcp` stdio server, and the quickstart
|
|
473
|
+
generator (`python -m agent_authority.cli`, or the console scripts after `pip install`).
|
|
474
|
+
|
|
475
|
+
## Limitations & roadmap
|
|
476
|
+
|
|
477
|
+
Honest about what this reference implementation does *not* yet do:
|
|
478
|
+
|
|
479
|
+
- **Tenant isolation requires per-tenant tokens.** With
|
|
480
|
+
`tenants: { token: issuerPub }`, audit, policy, revocation, rate, and consent
|
|
481
|
+
are all namespaced per tenant (admin revocations stay global). Without tenant
|
|
482
|
+
tokens the plane is a single trust domain — run one plane per trust domain in
|
|
483
|
+
that mode.
|
|
484
|
+
- **Shared rate checks hit the network each call.** `HttpRateStore` consults the
|
|
485
|
+
control plane on every `authorize()` (the cap is authoritative and can't be
|
|
486
|
+
cached). The plane stamps each hit with its **own clock** and validates the
|
|
487
|
+
window/limit, so a skewed or hostile client can't slide the window; the
|
|
488
|
+
limit/window values themselves still come from the caller's mandate (the
|
|
489
|
+
honest-enforcer model — a node that bypasses its own runtime is out of scope,
|
|
490
|
+
like any client that skips the check). Revocation, by contrast, can be wrapped
|
|
491
|
+
in `CachingRevocationStore` for a bounded staleness window. Signature, scope,
|
|
492
|
+
and expiry are always fully offline.
|
|
493
|
+
- **Cross-language delegation works.** A holder credential
|
|
494
|
+
(`serializeWithKey()`) issued in one port can be imported **and attenuated** in
|
|
495
|
+
the other: a block signed in Python over a chain rooted in TypeScript (or vice
|
|
496
|
+
versa) verifies, because both ports use raw Ed25519 keys and byte-identical
|
|
497
|
+
canonical JSON. Pinned by the interop check (`PY->TS->PY` and `TS->PY->TS`
|
|
498
|
+
delegated-chain cases in `scripts/interop.mjs`).
|
|
499
|
+
- **Pure-Python Ed25519 is not constant-time — auto-upgraded when possible.** The
|
|
500
|
+
zero-dependency reference signer is correct but not hardened against timing
|
|
501
|
+
side-channels. The Python port now **auto-selects** a hardened native backend
|
|
502
|
+
when one is importable (`cryptography`, then `PyNaCl`), falling back to pure
|
|
503
|
+
Python otherwise; `agent_authority.crypto.backend()` reports which is active. Install
|
|
504
|
+
`cryptography` for constant-time Python in hostile-adjacency deployments. (Node
|
|
505
|
+
always uses its native, hardened crypto.)
|
|
506
|
+
- **Rate limiting offers two strategies.** The default `MemoryRateStore` is a
|
|
507
|
+
sliding-count window (max N per window); `TokenBucketRateStore` is now available
|
|
508
|
+
for **burst shaping** (an initial burst up to the limit, then a steady refill).
|
|
509
|
+
Both are drop-in for any `RateStore` slot (engine `rate:` or the control plane),
|
|
510
|
+
and neither counts rejected attempts.
|
|
511
|
+
- **The audit log is an unkeyed hash chain.** It detects edits, reordering, and
|
|
512
|
+
naive single-record tampering — but a writer with full store access can
|
|
513
|
+
recompute the chain, and tail deletion alone isn't detectable. Mitigation
|
|
514
|
+
shipped: `checkpointAudit()` signs the head; store checkpoints out of the
|
|
515
|
+
writer's reach and `verifyAuditCheckpoint()` detects deletion/rewrites.
|
|
516
|
+
WORM/append-only storage remains the strongest option.
|
|
517
|
+
- **The `agent` *caveat* is an advisory label** (a string), but cryptographic
|
|
518
|
+
agent-identity binding now ships: grant/attenuate with `bindAgent` to require a
|
|
519
|
+
proof of possession of the agent's key at authorize (SVID-style; see "Binding a
|
|
520
|
+
mandate to an agent identity"). The agent's private key must be provisioned out
|
|
521
|
+
of band — Behalf enforces the binding but does not distribute keys.
|
|
522
|
+
|
|
523
|
+
None of these affect the core security properties (unforgeable, attenuation-only,
|
|
524
|
+
truncation-resistant, offline-verifiable mandates); they are
|
|
525
|
+
durability/scaling/hardening trade-offs.
|
|
526
|
+
|
|
527
|
+
This implementation has not had an independent cryptographic audit — commission
|
|
528
|
+
one before any 1.0 / production positioning.
|
|
529
|
+
|
|
530
|
+
## Publishing
|
|
531
|
+
|
|
532
|
+
Releases are cut by `.github/workflows/release.yml` on a `v*` tag (every other
|
|
533
|
+
run is a safe dry-run). Both packages publish as **`agent-authority`**.
|
|
534
|
+
|
|
535
|
+
- **PyPI — Trusted Publishing (no token).** On PyPI, add a *pending publisher*
|
|
536
|
+
(Account → Publishing): PyPI project `agent-authority`, owner `novaai0401-ui`,
|
|
537
|
+
repository `agent-authority`, workflow `release.yml`. This authorizes the first
|
|
538
|
+
publish of a brand-new project over OIDC — no `PYPI_TOKEN` secret, nothing to
|
|
539
|
+
leak or rotate.
|
|
540
|
+
- **npm.** Add an automation `NPM_TOKEN` as a repository secret
|
|
541
|
+
(Settings → Secrets and variables → Actions); the workflow publishes with npm
|
|
542
|
+
provenance.
|
|
543
|
+
|
|
544
|
+
Then:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
git tag v0.1.0 && git push origin v0.1.0 # triggers the gated publish of both
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## License
|
|
551
|
+
|
|
552
|
+
MIT — see [LICENSE](./LICENSE). Open source, use it anywhere, including
|
|
553
|
+
commercially. Contributions welcome.
|
package/dist/a2a.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { Behalf } from "./behalf.js";
|
|
3
|
+
import { Mandate } from "./mandate.js";
|
|
4
|
+
import type { AttenuateOptions } from "./types.js";
|
|
5
|
+
import type { KeyPair } from "./crypto.js";
|
|
6
|
+
/**
|
|
7
|
+
* A2A (agent-to-agent) HTTP transport.
|
|
8
|
+
*
|
|
9
|
+
* The in-process `withBehalf` middleware secures tool calls inside one agent;
|
|
10
|
+
* this secures calls *between* agents over the wire. A caller attaches its
|
|
11
|
+
* mandate to an outgoing request (optionally attenuating it first, so the callee
|
|
12
|
+
* receives strictly less authority); the callee verifies the mandate and its
|
|
13
|
+
* whole delegation chain — offline, with only the issuer's public key — and
|
|
14
|
+
* authorizes the action before doing any work. The verifiable chain travels with
|
|
15
|
+
* the request, so a compromised hop still can't widen scope.
|
|
16
|
+
*
|
|
17
|
+
* Zero dependencies: built on `node:http` types and the global `fetch`.
|
|
18
|
+
*/
|
|
19
|
+
/** Header carrying the serialized mandate on an A2A request. */
|
|
20
|
+
export declare const MANDATE_HEADER = "x-behalf-mandate";
|
|
21
|
+
/** Header carrying the caller's proof of possession (anti-truncation / replay). */
|
|
22
|
+
export declare const PROOF_HEADER = "x-behalf-proof";
|
|
23
|
+
/** Header carrying the caller's declared action (the proof is bound to it). */
|
|
24
|
+
export declare const ACTION_HEADER = "x-behalf-action";
|
|
25
|
+
export interface PresentOptions {
|
|
26
|
+
/** The concrete action the caller intends — the proof is bound to it. */
|
|
27
|
+
action: string;
|
|
28
|
+
/** Narrow the mandate before sending, so the callee gets less authority. */
|
|
29
|
+
attenuate?: AttenuateOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Agent-identity keys to prove, satisfying any `agentKey` caveat bound to this
|
|
32
|
+
* caller (SVID-style binding). Required when the mandate was granted with
|
|
33
|
+
* `bindAgent`; otherwise the callee's authorize denies "agent identity proof
|
|
34
|
+
* required".
|
|
35
|
+
*/
|
|
36
|
+
agentKeys?: KeyPair[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the headers that carry a mandate to a downstream agent: the serialized
|
|
40
|
+
* (optionally attenuated) token, the caller's declared action, and a fresh proof
|
|
41
|
+
* of possession bound to that action and the exact chain. The callee verifies
|
|
42
|
+
* all three, so a truncated, intercepted, or repurposed token is useless.
|
|
43
|
+
*/
|
|
44
|
+
export declare function present(mandate: Mandate, opts: PresentOptions): Record<string, string>;
|
|
45
|
+
/** `fetch` wrapper that attaches (and optionally attenuates) a mandate. */
|
|
46
|
+
export declare function behalfFetch(input: string | URL, mandate: Mandate, init?: RequestInit, presentOpts?: PresentOptions): Promise<Response>;
|
|
47
|
+
/**
|
|
48
|
+
* Verify + authorize an incoming A2A request. Returns the verified Mandate, or
|
|
49
|
+
* throws {@link AuthorizationError} if it is missing, untrusted, or out of scope.
|
|
50
|
+
* Framework-agnostic: pass any header bag (node:http or fetch `Headers`).
|
|
51
|
+
*/
|
|
52
|
+
export declare function authorizeIncoming(engine: Behalf, headers: Record<string, string | string[] | undefined> | Headers, capability: string): Promise<Mandate>;
|
|
53
|
+
export interface GuardOptions {
|
|
54
|
+
/** Engine that holds the trusted issuer key(s). Defaults to the shared one. */
|
|
55
|
+
engine?: Behalf;
|
|
56
|
+
/** Map a request to the capability it requires (return undefined to skip). */
|
|
57
|
+
capability: (req: IncomingMessage) => string | undefined;
|
|
58
|
+
/** Override the default 403 JSON response. */
|
|
59
|
+
onDenied?: (req: IncomingMessage, res: ServerResponse, reason: string) => void;
|
|
60
|
+
}
|
|
61
|
+
/** Request with the verified mandate attached by {@link guard}. */
|
|
62
|
+
export interface GuardedRequest extends IncomingMessage {
|
|
63
|
+
mandate?: Mandate;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A connect/express/node:http-style middleware that authorizes each request
|
|
67
|
+
* before it reaches your handler. On success the verified mandate is attached as
|
|
68
|
+
* `req.mandate` and `next()` is called; on failure it writes 403 and returns.
|
|
69
|
+
*/
|
|
70
|
+
export declare function guard(opts: GuardOptions): (req: GuardedRequest, res: ServerResponse, next?: () => void) => Promise<boolean>;
|
|
71
|
+
/** The symmetric in-process A2A binding (re-exported for continuity). */
|
|
72
|
+
export { withBehalfA2A } from "./mcp.js";
|
|
73
|
+
//# sourceMappingURL=a2a.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a.d.ts","sourceRoot":"","sources":["../src/a2a.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;GAYG;AAEH,gEAAgE;AAChE,eAAO,MAAM,cAAc,qBAAqB,CAAC;AAEjD,mFAAmF;AACnF,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAE7C,+EAA+E;AAC/E,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQtF;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE,WAAgB,EACtB,WAAW,GAAE,cAA+B,GAC3C,OAAO,CAAC,QAAQ,CAAC,CAGnB;AAaD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,OAAO,EAChE,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAuBlB;AAED,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,UAAU,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC;IACzD,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF;AAED,mEAAmE;AACnE,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,IAExB,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,OAAO,MAAM,IAAI,KAAG,OAAO,CAAC,OAAO,CAAC,CAsB7F;AAED,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC"}
|