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.
Files changed (85) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/LICENSE +21 -0
  3. package/QUICKSTART.md +91 -0
  4. package/README.md +553 -0
  5. package/dist/a2a.d.ts +73 -0
  6. package/dist/a2a.d.ts.map +1 -0
  7. package/dist/a2a.js +117 -0
  8. package/dist/a2a.js.map +1 -0
  9. package/dist/audit.d.ts +12 -0
  10. package/dist/audit.d.ts.map +1 -0
  11. package/dist/audit.js +52 -0
  12. package/dist/audit.js.map +1 -0
  13. package/dist/behalf.d.ts +173 -0
  14. package/dist/behalf.d.ts.map +1 -0
  15. package/dist/behalf.js +475 -0
  16. package/dist/behalf.js.map +1 -0
  17. package/dist/capability.d.ts +56 -0
  18. package/dist/capability.d.ts.map +1 -0
  19. package/dist/capability.js +176 -0
  20. package/dist/capability.js.map +1 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +273 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/control-plane.d.ts +57 -0
  26. package/dist/control-plane.d.ts.map +1 -0
  27. package/dist/control-plane.js +332 -0
  28. package/dist/control-plane.js.map +1 -0
  29. package/dist/crypto.d.ts +68 -0
  30. package/dist/crypto.d.ts.map +1 -0
  31. package/dist/crypto.js +105 -0
  32. package/dist/crypto.js.map +1 -0
  33. package/dist/errors.d.ts +25 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +40 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/index.d.ts +35 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +34 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lint.d.ts +17 -0
  42. package/dist/lint.d.ts.map +1 -0
  43. package/dist/lint.js +75 -0
  44. package/dist/lint.js.map +1 -0
  45. package/dist/mandate.d.ts +99 -0
  46. package/dist/mandate.d.ts.map +1 -0
  47. package/dist/mandate.js +141 -0
  48. package/dist/mandate.js.map +1 -0
  49. package/dist/mcp-server.d.ts +26 -0
  50. package/dist/mcp-server.d.ts.map +1 -0
  51. package/dist/mcp-server.js +111 -0
  52. package/dist/mcp-server.js.map +1 -0
  53. package/dist/mcp.d.ts +63 -0
  54. package/dist/mcp.d.ts.map +1 -0
  55. package/dist/mcp.js +123 -0
  56. package/dist/mcp.js.map +1 -0
  57. package/dist/persist.d.ts +51 -0
  58. package/dist/persist.d.ts.map +1 -0
  59. package/dist/persist.js +150 -0
  60. package/dist/persist.js.map +1 -0
  61. package/dist/quickstart.d.ts +63 -0
  62. package/dist/quickstart.d.ts.map +1 -0
  63. package/dist/quickstart.js +171 -0
  64. package/dist/quickstart.js.map +1 -0
  65. package/dist/remote.d.ts +93 -0
  66. package/dist/remote.d.ts.map +1 -0
  67. package/dist/remote.js +120 -0
  68. package/dist/remote.js.map +1 -0
  69. package/dist/seal.d.ts +12 -0
  70. package/dist/seal.d.ts.map +1 -0
  71. package/dist/seal.js +96 -0
  72. package/dist/seal.js.map +1 -0
  73. package/dist/store.d.ts +119 -0
  74. package/dist/store.d.ts.map +1 -0
  75. package/dist/store.js +139 -0
  76. package/dist/store.js.map +1 -0
  77. package/dist/types.d.ts +173 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +17 -0
  80. package/dist/types.js.map +1 -0
  81. package/llms.txt +106 -0
  82. package/package.json +107 -0
  83. package/schemas/capability.schema.json +14 -0
  84. package/schemas/mandate.schema.json +68 -0
  85. 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
+ [![npm](https://img.shields.io/npm/v/agent-authority?logo=npm)](https://www.npmjs.com/package/agent-authority)
8
+ [![PyPI](https://img.shields.io/pypi/v/agent-authority?logo=pypi&logoColor=white)](https://pypi.org/project/agent-authority/)
9
+ [![CI](https://github.com/novaai0401-ui/agent-authority/actions/workflows/ci.yml/badge.svg)](https://github.com/novaai0401-ui/agent-authority/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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"}