agents-chain 0.0.1 → 0.0.3

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 (57) hide show
  1. package/README.md +248 -32
  2. package/dist/app/app-wrapper.d.ts +43 -0
  3. package/dist/app/app-wrapper.d.ts.map +1 -0
  4. package/dist/app/app-wrapper.js +122 -0
  5. package/dist/app/app-wrapper.js.map +1 -0
  6. package/dist/app/capability-registry.d.ts +31 -0
  7. package/dist/app/capability-registry.d.ts.map +1 -0
  8. package/dist/app/capability-registry.js +65 -0
  9. package/dist/app/capability-registry.js.map +1 -0
  10. package/dist/audit/audit-exporter.d.ts +82 -0
  11. package/dist/audit/audit-exporter.d.ts.map +1 -0
  12. package/dist/audit/audit-exporter.js +94 -0
  13. package/dist/audit/audit-exporter.js.map +1 -0
  14. package/dist/audit/audit-log.d.ts +10 -0
  15. package/dist/audit/audit-log.d.ts.map +1 -1
  16. package/dist/audit/audit-log.js +18 -0
  17. package/dist/audit/audit-log.js.map +1 -1
  18. package/dist/auth/constraints.d.ts +19 -0
  19. package/dist/auth/constraints.d.ts.map +1 -0
  20. package/dist/auth/constraints.js +85 -0
  21. package/dist/auth/constraints.js.map +1 -0
  22. package/dist/auth/token-verifier.d.ts +26 -2
  23. package/dist/auth/token-verifier.d.ts.map +1 -1
  24. package/dist/auth/token-verifier.js +47 -9
  25. package/dist/auth/token-verifier.js.map +1 -1
  26. package/dist/chain.d.ts +59 -1
  27. package/dist/chain.d.ts.map +1 -1
  28. package/dist/chain.js +115 -0
  29. package/dist/chain.js.map +1 -1
  30. package/dist/crypto/ed25519.d.ts.map +1 -1
  31. package/dist/crypto/ed25519.js +2 -1
  32. package/dist/crypto/ed25519.js.map +1 -1
  33. package/dist/crypto/utils.d.ts +1 -1
  34. package/dist/crypto/utils.d.ts.map +1 -1
  35. package/dist/host/host-identity.d.ts +66 -0
  36. package/dist/host/host-identity.d.ts.map +1 -0
  37. package/dist/host/host-identity.js +109 -0
  38. package/dist/host/host-identity.js.map +1 -0
  39. package/dist/index.d.ts +14 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +11 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/memory/jti-cache.d.ts +31 -4
  44. package/dist/memory/jti-cache.d.ts.map +1 -1
  45. package/dist/memory/jti-cache.js +40 -13
  46. package/dist/memory/jti-cache.js.map +1 -1
  47. package/dist/types/capabilities.d.ts +64 -0
  48. package/dist/types/capabilities.d.ts.map +1 -0
  49. package/dist/types/capabilities.js +9 -0
  50. package/dist/types/capabilities.js.map +1 -0
  51. package/dist/types/chain.d.ts +51 -1
  52. package/dist/types/chain.d.ts.map +1 -1
  53. package/dist/types/protocol.d.ts +61 -0
  54. package/dist/types/protocol.d.ts.map +1 -0
  55. package/dist/types/protocol.js +10 -0
  56. package/dist/types/protocol.js.map +1 -0
  57. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,29 @@
1
1
  # agents-chain
2
2
 
3
- Lightweight identity, authentication, and audit layer for AI agent SDKs.
3
+ **v0.0.3** — A zero-dependency security layer that wraps any app, service object, or AI SDK with Ed25519 identity, JWT-gated capability enforcement, constraint validation, and an encrypted audit trail.
4
4
 
5
- Wrap your OpenAI or Anthropic client with `agents-chain` to get:
5
+ Drop it in front of your billing service, a third-party API client, or an OpenAI/Anthropic SDK every call is signed, verified, scoped, and logged without touching your existing code.
6
6
 
7
- - **Ed25519 key-pair identity** per agent instance
8
- - **JWT-based capability tokens** — every API call is signed and verified
9
- - **Encrypted in-memory audit log** — AES-256-GCM, queryable at runtime
10
- - **Zero network calls** — everything runs locally, no external service required
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ - **Host + Agent identity** — Ed25519 keypairs with JWK thumbprints as stable IDs. Hosts sign agent registration JWTs; agents sign scoped capability tokens.
12
+ - **9-step JWT verification** — Every capability call mints a fresh 60-second single-use token. Sub, iss, aud, signature, expiry, and JTI replay are all verified before the call proceeds.
13
+ - **Capability registry + app wrapper** — Register named capabilities on any service object. A JavaScript Proxy intercepts method calls by name and gates them through the full auth pipeline.
14
+ - **Constraint enforcement** — Grants can carry `max`, `min`, `in`, `not_in`, or exact-equality constraints on call arguments, enforced before execution.
15
+ - **Encrypted audit log** — AES-256-GCM in-memory log of every call (success, denied, error). Drain to any HTTP endpoint or custom exporter on a schedule or at shutdown.
16
+ - **Adapter interfaces** — Plug in your own Redis client for JTI replay protection across restarts. Plug in your own grant resolver to read active grants from your DB. The package ships no Redis client.
17
+ - **Well-known discovery** — Serve `GET /.well-known/agent-configuration` with one call. Agents discover your capabilities, endpoints, and supported algorithms automatically.
18
+ - **Zero mandatory dependencies** — In-memory defaults for everything. Redis, databases, and HTTP clients are injected by you.
19
+
20
+ ---
21
+
22
+ ## Package overview
23
+
24
+ ![Package overview — all modules and how they connect](https://raw.githubusercontent.com/Brian-Mwangi-developer/agentchain/main/docs/overview1.jpeg)
25
+
26
+ The package has six layers. **HostIdentity** and **AgentIdentity** hold Ed25519 keypairs. **TokenBuilder** mints scoped JWTs; **TokenVerifier** runs the 9-step verification pipeline. **CapabilityRegistry** maps method names to capability definitions; **wrapApp()** turns any object into a secured Proxy using that registry. **AuditLog** records every call into an AES-256-GCM encrypted store and can drain to any **AuditExporter**. All state lives in **EncryptedStore** — there is no network I/O by default.
11
27
 
12
28
  ---
13
29
 
@@ -23,7 +39,90 @@ pnpm add agents-chain
23
39
 
24
40
  ---
25
41
 
26
- ## Quick start
42
+ ## Wrapping any app — AppChain
43
+
44
+ `AppChain` is the main entry point for wrapping your own services or third-party clients.
45
+
46
+ ### Integration flow
47
+
48
+ ![Integration flow — AppChain.create() through chain.wrap() to a secured Proxy](https://raw.githubusercontent.com/Brian-Mwangi-developer/agentchain/main/docs/flow1.jpeg)
49
+
50
+ ```ts
51
+ import { AppChain, HttpAuditExporter } from 'agents-chain';
52
+
53
+ const chain = await AppChain.create({
54
+ providerName: 'billing-service',
55
+ issuer: 'https://billing.mycompany.com',
56
+ capabilities: [
57
+ {
58
+ name: 'createInvoice',
59
+ description: 'Create a new invoice for a customer',
60
+ inputSchema: {
61
+ type: 'object',
62
+ required: ['customerId', 'amount'],
63
+ properties: {
64
+ customerId: { type: 'string' },
65
+ amount: { type: 'number' },
66
+ },
67
+ },
68
+ outputSchema: { type: 'object' },
69
+ execute: async (args, ctx) => {
70
+ // ctx carries agentId, hostId, and active permissions
71
+ return billingDb.createInvoice(args.customerId, args.amount);
72
+ },
73
+ },
74
+ ],
75
+
76
+ // Optional: resolve grants from your DB instead of in-memory
77
+ grantResolver: async (agentId, capability) => myDb.getGrant(agentId, capability),
78
+
79
+ // Optional: drain audit log to a hosted endpoint
80
+ auditExporter: new HttpAuditExporter({
81
+ endpoint: 'https://audit.yourservice.com/ingest',
82
+ apiKey: process.env.AUDIT_API_KEY,
83
+ }),
84
+ });
85
+
86
+ // Serve capability discovery
87
+ app.get('/.well-known/agent-configuration', (req, res) =>
88
+ res.json(chain.getWellKnownConfig())
89
+ );
90
+
91
+ // Wrap any object — every registered method is now capability-gated
92
+ const secured = chain.wrap(billingService, agentGrants);
93
+ const invoice = await secured.createInvoice({ customerId: 'c1', amount: 500 });
94
+
95
+ // Flush audit log on shutdown
96
+ process.on('SIGTERM', () => chain.drain());
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Per-call security pipeline
102
+
103
+ Every intercepted call goes through this verification pipeline before your code runs.
104
+
105
+ ![Per-call security flow — 9-step JWT verification pipeline](https://raw.githubusercontent.com/Brian-Mwangi-developer/agentchain/main/docs/agent-flow1.jpeg)
106
+
107
+ | Step | Check | Error on failure |
108
+ |------|-------|-----------------|
109
+ | 1–2 | Decode JWT header + payload, confirm `typ = "agent+jwt"` | `token_invalid` |
110
+ | 3 | `sub` matches registered `agentId` | `agent_not_found` |
111
+ | 4 | `iss` matches registered public key thumbprint | `token_invalid` |
112
+ | 5 | `aud` matches the requested capability name | `capability_denied` |
113
+ | 6 | Ed25519 signature is valid | `token_invalid` |
114
+ | 7 | `exp`/`iat` temporal check + 30s clock skew tolerance | `token_expired` / `token_invalid` |
115
+ | 8 | JTI not seen in 90-second replay window | `token_replayed` |
116
+ | 9 | Agent holds an `active` grant for the capability | `capability_denied` |
117
+ | 9b | Call arguments satisfy all grant constraints | `constraint_violated` |
118
+
119
+ All failures throw `ChainAuthError` and are recorded in the audit log as `result: "denied"`.
120
+
121
+ ---
122
+
123
+ ## Wrapping AI SDKs — AgentsChain
124
+
125
+ For OpenAI and Anthropic clients, use `AgentsChain` instead. It maps SDK method paths to capability strings automatically.
27
126
 
28
127
  ### OpenAI
29
128
 
@@ -41,10 +140,8 @@ const ai = chain.openai(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
41
140
 
42
141
  const response = await ai.chat.completions.create({
43
142
  model: 'gpt-4o',
44
- messages: [{ role: 'user', content: 'Summarize the water cycle in one sentence.' }],
143
+ messages: [{ role: 'user', content: 'Summarize the water cycle.' }],
45
144
  });
46
-
47
- console.log(response.choices[0].message.content);
48
145
  ```
49
146
 
50
147
  ### Anthropic
@@ -56,7 +153,7 @@ import Anthropic from '@anthropic-ai/sdk';
56
153
  const chain = await AgentsChain.create({
57
154
  agentName: 'classifier',
58
155
  hostname: 'my-app',
59
- capabilities: ['messages.create'],
156
+ capabilities: ['message'],
60
157
  });
61
158
 
62
159
  const ai = chain.anthropic(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }));
@@ -64,24 +161,85 @@ const ai = chain.anthropic(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY
64
161
  const response = await ai.messages.create({
65
162
  model: 'claude-sonnet-4-6',
66
163
  max_tokens: 256,
67
- messages: [{ role: 'user', content: 'Classify this text as positive or negative: "I love it!"' }],
164
+ messages: [{ role: 'user', content: 'Classify: "I love it!"' }],
68
165
  });
166
+ ```
167
+
168
+ ### Capability names
169
+
170
+ #### OpenAI
171
+
172
+ | SDK method | Capability string |
173
+ |-----------|------------------|
174
+ | `ai.chat.completions.create()` | `"chat.completion"` |
175
+ | `ai.embeddings.create()` | `"embedding"` |
176
+ | `ai.images.generate()` | `"image.generation"` |
177
+ | `ai.audio.transcriptions.create()` | `"audio.transcription"` |
178
+ | `ai.audio.speech.create()` | `"audio.speech"` |
179
+ | `ai.moderations.create()` | `"moderation"` |
180
+ | `ai.responses.create()` | `"response"` |
69
181
 
70
- console.log(response.content[0]);
182
+ #### Anthropic
183
+
184
+ | SDK method | Capability string |
185
+ |-----------|------------------|
186
+ | `ai.messages.create()` | `"message"` |
187
+ | `ai.messages.stream()` | `"message.stream"` |
188
+ | `ai.messages.countTokens()` | `"message.count_tokens"` |
189
+ | `ai.completions.create()` | `"completion"` |
190
+ | `ai.beta.messages.create()` | `"message.beta"` |
191
+
192
+ Any method not in these tables passes through without interception.
193
+
194
+ ---
195
+
196
+ ## Grant constraints
197
+
198
+ Constrain what an agent is allowed to pass in call arguments:
199
+
200
+ ```ts
201
+ const grants = [
202
+ {
203
+ capability: 'createInvoice',
204
+ status: 'active',
205
+ constraints: {
206
+ amount: { max: 1000 }, // amount <= 1000
207
+ currency: { in: ['USD', 'EUR'] }, // currency must be one of these
208
+ },
209
+ expiresAt: Date.now() + 86_400_000, // expires in 24h
210
+ },
211
+ ];
212
+
213
+ const secured = chain.wrap(billingService, grants);
214
+
215
+ // This passes
216
+ await secured.createInvoice({ customerId: 'c1', amount: 500, currency: 'USD' });
217
+
218
+ // This throws ChainAuthError("constraint_violated")
219
+ await secured.createInvoice({ customerId: 'c1', amount: 2000, currency: 'USD' });
71
220
  ```
72
221
 
222
+ Supported operators: `max`, `min`, `in`, `not_in`, exact primitive equality.
223
+
73
224
  ---
74
225
 
75
- ## Configuration
226
+ ## Persistent JTI replay protection
227
+
228
+ By default, JTI replay protection is in-memory and resets on restart. For shared deployments, plug in your own Redis client:
76
229
 
77
230
  ```ts
78
- const chain = await AgentsChain.create({
79
- agentName: string; // Human-readable name for this agent
80
- hostname: string; // Used to build the agentId: "<hostname>-agent-<32hex>"
81
- capabilities: string[]; // List of capability strings this agent is allowed to use
82
- encryptionKey?: string; // Optional 64-char hex (32-byte) AES-256-GCM key.
83
- // Omit to generate a random key per session.
84
- // Provide to persist and reload audit logs across restarts.
231
+ import { AppChain } from 'agents-chain';
232
+
233
+ const redisAdapter = {
234
+ has: (key) => redis.exists(key).then(Boolean),
235
+ set: (key, ttlMs) => redis.set(key, '1', 'PX', ttlMs).then(() => {}),
236
+ };
237
+
238
+ const chain = await AppChain.create({
239
+ providerName: 'my-service',
240
+ issuer: 'https://myservice.com',
241
+ capabilities: [...],
242
+ jtiAdapter: redisAdapter,
85
243
  });
86
244
  ```
87
245
 
@@ -89,37 +247,89 @@ const chain = await AgentsChain.create({
89
247
 
90
248
  ## Audit log
91
249
 
92
- Every API call through a wrapped client is recorded in an encrypted in-memory log.
250
+ Every call is recorded in an AES-256-GCM encrypted in-memory log.
93
251
 
94
252
  ```ts
95
253
  // Get all entries (decrypted)
96
254
  const entries = chain.getAuditLog();
97
255
 
98
- // Full snapshot with metadata
99
- const snapshot = chain.exportAudit();
100
- // { agentId, entries, exportedAt }
101
-
102
256
  // Summary counts
103
257
  const stats = chain.getStats();
104
258
  // { agentId, agentName, hostname, totalCalls, successfulCalls, deniedCalls, errorCalls, registeredAt }
259
+
260
+ // Export and clear — call periodically or on shutdown
261
+ await chain.drain(); // uses auditExporter from config
262
+ await chain.drain(new ConsoleAuditExporter()); // override exporter
105
263
  ```
106
264
 
107
265
  Each `AuditEntry` contains:
108
266
 
109
267
  | Field | Type | Description |
110
- |---|---|---|
268
+ |-------|------|-------------|
111
269
  | `id` | `string` | Unique entry ID |
112
- | `agentId` | `string` | The agent that made the call |
113
- | `capability` | `string` | Capability used |
270
+ | `agentId` | `string` | Agent that made the call |
271
+ | `agentName` | `string` | Human-readable agent name |
272
+ | `capability` | `string` | Capability requested |
273
+ | `args` | `object` | Sanitized call arguments (secrets redacted) |
114
274
  | `result` | `"success" \| "denied" \| "error"` | Outcome |
275
+ | `denialReason` | `string?` | Set when `result === "denied"` |
276
+ | `jti` | `string` | JWT ID used |
115
277
  | `timestamp` | `number` | Unix ms |
116
- | `meta` | `Record<string, unknown>` | Provider-specific metadata |
278
+ | `durationMs` | `number` | Execution time in ms |
279
+
280
+ Argument keys matching `key`, `secret`, `token`, `password`, `auth`, `credential`, or `bearer` are automatically replaced with `"[REDACTED]"` before logging.
117
281
 
118
282
  ---
119
283
 
120
- ## Identity & crypto utilities
284
+ ## Host identity
121
285
 
122
- Low-level utilities are exported if you need direct access:
286
+ `HostIdentity` is the user's anchor for signing agent registration JWTs against an agent-auth server.
287
+
288
+ ```ts
289
+ // chain.host is a HostIdentity instance
290
+ const hostJwt = await chain.host.signJwt();
291
+ const registrationJwt = await chain.host.signAgentRegistrationJwt(agentPublicKeyJwk);
292
+
293
+ // Stable identity across restarts — export and reload the private key
294
+ const privateKeyJwk = await chain.host.exportPrivateKeyJwk();
295
+ // persist privateKeyJwk securely
296
+
297
+ // On next startup:
298
+ const host = await HostIdentity.fromKeyPair(savedPrivateKeyJwk, savedPublicKeyJwk, config);
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Well-known discovery
304
+
305
+ ```ts
306
+ // Returns AgentConfiguration — serve at GET /.well-known/agent-configuration
307
+ chain.getWellKnownConfig();
308
+
309
+ // {
310
+ // version: "1.0-draft",
311
+ // provider_name: "billing-service",
312
+ // issuer: "https://billing.mycompany.com",
313
+ // algorithms: ["Ed25519"],
314
+ // modes: ["delegated", "autonomous"],
315
+ // approval_methods: ["device_authorization"],
316
+ // endpoints: {
317
+ // register: "/agent/register",
318
+ // capabilities: "/capability/list",
319
+ // execute: "/capability/execute",
320
+ // status: "/agent/status",
321
+ // revoke: "/agent/revoke",
322
+ // ...
323
+ // },
324
+ // default_capabilities: ["createInvoice", ...]
325
+ // }
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Crypto utilities
331
+
332
+ Low-level Ed25519 utilities are exported if you need them directly:
123
333
 
124
334
  ```ts
125
335
  import {
@@ -140,6 +350,12 @@ import {
140
350
 
141
351
  ---
142
352
 
353
+ ## Full architecture
354
+
355
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for Mermaid diagrams covering the package internals, integration flow, per-call security pipeline, Host JWT flow, well-known discovery, and persistence adapter swap-in points.
356
+
357
+ ---
358
+
143
359
  ## License
144
360
 
145
361
  MIT — [brianmwangidev](https://www.npmjs.com/~brianmwangidev)
@@ -0,0 +1,43 @@
1
+ /**
2
+ * wrapApp — wraps any object with capability-gated security.
3
+ *
4
+ * This is the generic version of the AI SDK wrappers (openai-wrapper, anthropic-wrapper).
5
+ * Instead of a hardcoded path→capability map, it uses a CapabilityRegistry.
6
+ *
7
+ * For every method call on the wrapped object:
8
+ * 1. Look up capability by method name in the registry
9
+ * 2. If not in registry → call through without any gating (pass-through)
10
+ * 3. If in registry:
11
+ * a. Verify the agent holds an active grant for this capability
12
+ * b. Enforce any constraints on the call arguments
13
+ * c. Call capability.execute(args, agentContext) — NOT the raw target method
14
+ * d. Record in audit log (success / denied / error)
15
+ *
16
+ * Note: The Proxy only intercepts direct method calls (not nested paths).
17
+ * Use the AI SDK wrappers for nested path interception (e.g. client.chat.completions.create).
18
+ * This wrapper is designed for flat service objects where method names are unique.
19
+ */
20
+ import type { CapabilityRegistry } from "./capability-registry.js";
21
+ import type { AuditLog } from "../audit/audit-log.js";
22
+ import type { TokenBuilder } from "../auth/token-builder.js";
23
+ import type { TokenVerifier } from "../auth/token-verifier.js";
24
+ import type { AgentIdentity } from "../identity/agent-identity.js";
25
+ import type { ResolvedGrant } from "../types/protocol.js";
26
+ export type AppInterceptContext = {
27
+ identity: AgentIdentity;
28
+ builder: TokenBuilder;
29
+ verifier: TokenVerifier;
30
+ log: AuditLog;
31
+ grants: ResolvedGrant[];
32
+ };
33
+ /**
34
+ * Wrap any object with capability-gated security.
35
+ *
36
+ * @param target The object to wrap (e.g. a service instance)
37
+ * @param registry Capability registry — defines what methods are gated
38
+ * @param ctx Intercept context — identity, auth, audit, grants
39
+ * @returns A Proxy with the same type as target
40
+ */
41
+ export declare function wrapApp<T extends object>(target: T, registry: CapabilityRegistry, ctx: AppInterceptContext): T;
42
+ export declare function attachRegistry(ctx: AppInterceptContext, registry: CapabilityRegistry): void;
43
+ //# sourceMappingURL=app-wrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-wrapper.d.ts","sourceRoot":"","sources":["../../src/app/app-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACpC,MAAM,EAAE,CAAC,EACT,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,EAAE,mBAAmB,GACzB,CAAC,CAgBH;AAuFD,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,EAAE,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAE3F"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * wrapApp — wraps any object with capability-gated security.
3
+ *
4
+ * This is the generic version of the AI SDK wrappers (openai-wrapper, anthropic-wrapper).
5
+ * Instead of a hardcoded path→capability map, it uses a CapabilityRegistry.
6
+ *
7
+ * For every method call on the wrapped object:
8
+ * 1. Look up capability by method name in the registry
9
+ * 2. If not in registry → call through without any gating (pass-through)
10
+ * 3. If in registry:
11
+ * a. Verify the agent holds an active grant for this capability
12
+ * b. Enforce any constraints on the call arguments
13
+ * c. Call capability.execute(args, agentContext) — NOT the raw target method
14
+ * d. Record in audit log (success / denied / error)
15
+ *
16
+ * Note: The Proxy only intercepts direct method calls (not nested paths).
17
+ * Use the AI SDK wrappers for nested path interception (e.g. client.chat.completions.create).
18
+ * This wrapper is designed for flat service objects where method names are unique.
19
+ */
20
+ import { ChainAuthError } from "../errors/chain-error.js";
21
+ import { enforceConstraints } from "../auth/constraints.js";
22
+ /**
23
+ * Wrap any object with capability-gated security.
24
+ *
25
+ * @param target The object to wrap (e.g. a service instance)
26
+ * @param registry Capability registry — defines what methods are gated
27
+ * @param ctx Intercept context — identity, auth, audit, grants
28
+ * @returns A Proxy with the same type as target
29
+ */
30
+ export function wrapApp(target, registry, ctx) {
31
+ return new Proxy(target, {
32
+ get(obj, prop) {
33
+ if (typeof prop !== "string")
34
+ return Reflect.get(obj, prop);
35
+ const capability = registry.get(prop);
36
+ const value = Reflect.get(obj, prop);
37
+ // Not a registered capability — pass through without gating
38
+ if (capability === undefined || typeof value !== "function") {
39
+ return value;
40
+ }
41
+ return createInterceptedMethod(capability.name, ctx);
42
+ },
43
+ });
44
+ }
45
+ function createInterceptedMethod(capabilityName, ctx) {
46
+ return async (...args) => {
47
+ const callArgs = (args[0] ?? {});
48
+ let jti = "unknown";
49
+ try {
50
+ // Build + verify the single-use JWT
51
+ const { token, claims } = await ctx.builder.build(capabilityName);
52
+ jti = claims.jti;
53
+ const verified = await ctx.verifier.verify(token, capabilityName, ctx.grants);
54
+ // Enforce grant constraints against call arguments
55
+ const grant = ctx.grants.find((g) => g.capability === capabilityName && g.status === "active");
56
+ if (grant?.constraints) {
57
+ enforceConstraints(grant.constraints, callArgs);
58
+ }
59
+ // Build agent context for capability.execute()
60
+ const agentContext = {
61
+ agentId: verified.agentId,
62
+ hostId: verified.hostId ?? "",
63
+ permissions: ctx.grants
64
+ .filter((g) => g.status === "active")
65
+ .map((g) => g.capability),
66
+ };
67
+ // Get the registered capability and execute it
68
+ const registryEntry = getCapabilityFromCtx(capabilityName, ctx);
69
+ if (!registryEntry) {
70
+ throw new ChainAuthError("capability_denied", `Capability "${capabilityName}" not found in registry`);
71
+ }
72
+ const start = Date.now();
73
+ let result;
74
+ try {
75
+ result = await registryEntry.execute(callArgs, agentContext);
76
+ }
77
+ catch (execErr) {
78
+ ctx.log.recordCall({
79
+ context: verified,
80
+ args: callArgs,
81
+ result: "error",
82
+ durationMs: Date.now() - start,
83
+ errorMessage: execErr instanceof Error ? execErr.message : String(execErr),
84
+ });
85
+ throw execErr;
86
+ }
87
+ ctx.log.recordCall({
88
+ context: verified,
89
+ args: callArgs,
90
+ result: "success",
91
+ durationMs: Date.now() - start,
92
+ });
93
+ return result;
94
+ }
95
+ catch (err) {
96
+ if (err instanceof ChainAuthError) {
97
+ ctx.log.recordDenied({
98
+ agentId: ctx.identity.agentId,
99
+ agentName: ctx.identity.registration.agentName,
100
+ hostname: ctx.identity.registration.hostname,
101
+ capability: capabilityName,
102
+ args: callArgs,
103
+ reason: err.message,
104
+ jti,
105
+ });
106
+ throw err;
107
+ }
108
+ throw err;
109
+ }
110
+ };
111
+ }
112
+ // Helper — we need registry access at intercept time.
113
+ // We store it on the context via a symbol to keep the type clean.
114
+ const REGISTRY_SYM = Symbol("registry");
115
+ export function attachRegistry(ctx, registry) {
116
+ ctx[REGISTRY_SYM] = registry;
117
+ }
118
+ function getCapabilityFromCtx(name, ctx) {
119
+ const registry = ctx[REGISTRY_SYM];
120
+ return registry?.get(name);
121
+ }
122
+ //# sourceMappingURL=app-wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-wrapper.js","sourceRoot":"","sources":["../../src/app/app-wrapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * CapabilityRegistry — registers Capability objects and generates well-known config.
3
+ *
4
+ * When building an AppChain, you register all capabilities the app exposes.
5
+ * The registry is then used by the app-wrapper Proxy to gate calls and by
6
+ * well-known.ts to generate the discovery config.
7
+ */
8
+ import type { Capability } from "../types/capabilities.js";
9
+ import type { AgentConfiguration } from "../types/protocol.js";
10
+ export declare class CapabilityRegistry {
11
+ private readonly caps;
12
+ /**
13
+ * Register a capability. Chainable.
14
+ * Throws if a capability with the same name is already registered.
15
+ */
16
+ register<TIn, TOut>(cap: Capability<TIn, TOut>): this;
17
+ get(name: string): Capability | undefined;
18
+ list(): Capability[];
19
+ has(name: string): boolean;
20
+ get size(): number;
21
+ /**
22
+ * Build an AgentConfiguration object suitable for serving at
23
+ * GET /.well-known/agent-configuration.
24
+ *
25
+ * @param issuer The base URL of the server (e.g. "https://billing.mycompany.com")
26
+ * @param providerName Short name for the app (e.g. "billing-service")
27
+ * @param endpointPrefix Optional prefix for endpoint paths (default: "")
28
+ */
29
+ buildWellKnownConfig(issuer: string, providerName: string, endpointPrefix?: string): AgentConfiguration;
30
+ }
31
+ //# sourceMappingURL=capability-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../src/app/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiC;IAEtD;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI;IAQrD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIzC,IAAI,IAAI,UAAU,EAAE;IAIpB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;OAOG;IACH,oBAAoB,CAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,cAAc,SAAK,GACpB,kBAAkB;CAqBxB"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CapabilityRegistry — registers Capability objects and generates well-known config.
3
+ *
4
+ * When building an AppChain, you register all capabilities the app exposes.
5
+ * The registry is then used by the app-wrapper Proxy to gate calls and by
6
+ * well-known.ts to generate the discovery config.
7
+ */
8
+ export class CapabilityRegistry {
9
+ constructor() {
10
+ this.caps = new Map();
11
+ }
12
+ /**
13
+ * Register a capability. Chainable.
14
+ * Throws if a capability with the same name is already registered.
15
+ */
16
+ register(cap) {
17
+ if (this.caps.has(cap.name)) {
18
+ throw new Error(`CapabilityRegistry: capability "${cap.name}" is already registered`);
19
+ }
20
+ this.caps.set(cap.name, cap);
21
+ return this;
22
+ }
23
+ get(name) {
24
+ return this.caps.get(name);
25
+ }
26
+ list() {
27
+ return Array.from(this.caps.values());
28
+ }
29
+ has(name) {
30
+ return this.caps.has(name);
31
+ }
32
+ get size() {
33
+ return this.caps.size;
34
+ }
35
+ /**
36
+ * Build an AgentConfiguration object suitable for serving at
37
+ * GET /.well-known/agent-configuration.
38
+ *
39
+ * @param issuer The base URL of the server (e.g. "https://billing.mycompany.com")
40
+ * @param providerName Short name for the app (e.g. "billing-service")
41
+ * @param endpointPrefix Optional prefix for endpoint paths (default: "")
42
+ */
43
+ buildWellKnownConfig(issuer, providerName, endpointPrefix = "") {
44
+ return {
45
+ version: "1.0-draft",
46
+ provider_name: providerName,
47
+ issuer,
48
+ algorithms: ["Ed25519"],
49
+ modes: ["delegated", "autonomous"],
50
+ approval_methods: ["device_authorization"],
51
+ endpoints: {
52
+ register: `${endpointPrefix}/agent/register`,
53
+ capabilities: `${endpointPrefix}/capability/list`,
54
+ execute: `${endpointPrefix}/capability/execute`,
55
+ status: `${endpointPrefix}/agent/status`,
56
+ reactivate: `${endpointPrefix}/agent/reactivate`,
57
+ revoke: `${endpointPrefix}/agent/revoke`,
58
+ rotate_key: `${endpointPrefix}/agent/rotate-key`,
59
+ request_capability: `${endpointPrefix}/agent/request-capability`,
60
+ },
61
+ default_capabilities: Array.from(this.caps.keys()),
62
+ };
63
+ }
64
+ }
65
+ //# sourceMappingURL=capability-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-registry.js","sourceRoot":"","sources":["../../src/app/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,OAAO,kBAAkB;IAA/B;QACqB,SAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;IA+D1D,CAAC;IA7DG;;;OAGG;IACH,QAAQ,CAAY,GAA0B;QAC1C,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,CAAC,IAAI,yBAAyB,CAAC,CAAC;QAC1F,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAiB,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,GAAG,CAAC,IAAY;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI;QACA,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,GAAG,CAAC,IAAY;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,oBAAoB,CAChB,MAAc,EACd,YAAoB,EACpB,cAAc,GAAG,EAAE;QAEnB,OAAO;YACH,OAAO,EAAE,WAAW;YACpB,aAAa,EAAE,YAAY;YAC3B,MAAM;YACN,UAAU,EAAE,CAAC,SAAS,CAAC;YACvB,KAAK,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC;YAClC,gBAAgB,EAAE,CAAC,sBAAsB,CAAC;YAC1C,SAAS,EAAE;gBACP,QAAQ,EAAE,GAAG,cAAc,iBAAiB;gBAC5C,YAAY,EAAE,GAAG,cAAc,kBAAkB;gBACjD,OAAO,EAAE,GAAG,cAAc,qBAAqB;gBAC/C,MAAM,EAAE,GAAG,cAAc,eAAe;gBACxC,UAAU,EAAE,GAAG,cAAc,mBAAmB;gBAChD,MAAM,EAAE,GAAG,cAAc,eAAe;gBACxC,UAAU,EAAE,GAAG,cAAc,mBAAmB;gBAChD,kBAAkB,EAAE,GAAG,cAAc,2BAA2B;aACnE;YACD,oBAAoB,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SACrD,CAAC;IACN,CAAC;CACJ"}