@absol-labs/agent 0.7.3 → 0.9.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 (80) hide show
  1. package/README.md +19 -4
  2. package/dist/discovery/registry.d.ts +110 -305
  3. package/dist/discovery/registry.d.ts.map +1 -1
  4. package/dist/discovery/registry.js +141 -318
  5. package/dist/discovery/registry.js.map +1 -1
  6. package/dist/frameworks/agentkit.d.ts.map +1 -1
  7. package/dist/frameworks/agentkit.js +23 -6
  8. package/dist/frameworks/agentkit.js.map +1 -1
  9. package/dist/gateway/caller-auth-gateway.d.ts +103 -2
  10. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  11. package/dist/gateway/caller-auth-gateway.js +176 -19
  12. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  13. package/dist/gateway/http-server.d.ts +12 -0
  14. package/dist/gateway/http-server.d.ts.map +1 -1
  15. package/dist/gateway/http-server.js +45 -1
  16. package/dist/gateway/http-server.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/wallet/autonomous-wallet-store.d.ts +123 -0
  22. package/dist/wallet/autonomous-wallet-store.d.ts.map +1 -0
  23. package/dist/wallet/autonomous-wallet-store.js +318 -0
  24. package/dist/wallet/autonomous-wallet-store.js.map +1 -0
  25. package/dist/wallet/autonomous-wallet.d.ts +14 -39
  26. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  27. package/dist/wallet/autonomous-wallet.js +12 -145
  28. package/dist/wallet/autonomous-wallet.js.map +1 -1
  29. package/dist/wallet/cdp-sdk.d.ts +23 -0
  30. package/dist/wallet/cdp-sdk.d.ts.map +1 -0
  31. package/dist/wallet/cdp-sdk.js +27 -0
  32. package/dist/wallet/cdp-sdk.js.map +1 -0
  33. package/dist/wallet/encrypted-file-credential-store.d.ts +41 -0
  34. package/dist/wallet/encrypted-file-credential-store.d.ts.map +1 -0
  35. package/dist/wallet/encrypted-file-credential-store.js +221 -0
  36. package/dist/wallet/encrypted-file-credential-store.js.map +1 -0
  37. package/dist/wallet/provider.d.ts +1 -1
  38. package/dist/wallet/provider.d.ts.map +1 -1
  39. package/dist/wallet/provider.js +8 -3
  40. package/dist/wallet/provider.js.map +1 -1
  41. package/dist/wallet/secret-service-probe.d.ts +56 -0
  42. package/dist/wallet/secret-service-probe.d.ts.map +1 -0
  43. package/dist/wallet/secret-service-probe.js +407 -0
  44. package/dist/wallet/secret-service-probe.js.map +1 -0
  45. package/dist/zktls/reclaim-js-sdk.d.ts +24 -0
  46. package/dist/zktls/reclaim-js-sdk.d.ts.map +1 -0
  47. package/dist/zktls/reclaim-js-sdk.js +29 -0
  48. package/dist/zktls/reclaim-js-sdk.js.map +1 -0
  49. package/dist/zktls/reclaim.d.ts +14 -2
  50. package/dist/zktls/reclaim.d.ts.map +1 -1
  51. package/dist/zktls/reclaim.js +29 -6
  52. package/dist/zktls/reclaim.js.map +1 -1
  53. package/dist/zktls/t2-delivery-proof.d.ts +8 -1
  54. package/dist/zktls/t2-delivery-proof.d.ts.map +1 -1
  55. package/dist/zktls/t2-delivery-proof.js +22 -6
  56. package/dist/zktls/t2-delivery-proof.js.map +1 -1
  57. package/docs/agent-layer.md +150 -0
  58. package/docs/autonomous-privy-wallet.md +133 -0
  59. package/docs/crewai.md +70 -0
  60. package/docs/eliza.md +109 -0
  61. package/docs/langchain.md +63 -0
  62. package/docs/mcp-hosted.md +137 -0
  63. package/docs/privy-embedded-wallet.md +102 -0
  64. package/docs/quickstart.md +370 -0
  65. package/docs/threat-model.md +160 -0
  66. package/package.json +19 -6
  67. package/src/discovery/registry.ts +242 -414
  68. package/src/frameworks/agentkit.ts +24 -5
  69. package/src/gateway/caller-auth-gateway.ts +281 -15
  70. package/src/gateway/http-server.ts +64 -0
  71. package/src/index.ts +24 -0
  72. package/src/wallet/autonomous-wallet-store.ts +487 -0
  73. package/src/wallet/autonomous-wallet.ts +57 -224
  74. package/src/wallet/cdp-sdk.ts +33 -0
  75. package/src/wallet/encrypted-file-credential-store.ts +341 -0
  76. package/src/wallet/provider.ts +16 -9
  77. package/src/wallet/secret-service-probe.ts +487 -0
  78. package/src/zktls/reclaim-js-sdk.ts +50 -0
  79. package/src/zktls/reclaim.ts +57 -23
  80. package/src/zktls/t2-delivery-proof.ts +28 -10
@@ -0,0 +1,370 @@
1
+ # Quickstart — hire a verified service in <10 minutes
2
+
3
+ Get an AI agent from zero to a **hired, oracle-verified, buyer-recoverable** service stream
4
+ on Base Sepolia. Every code block below uses the real, current exports of
5
+ `@absol-labs/agent` — no pseudocode.
6
+
7
+ > _x402 proves the payment, Metrik proves the delivery._
8
+
9
+ **What you get:** an owner-signed spend mandate caps how much/how fast/how long/with whom
10
+ your agent can spend; the agent escrows USDC and pays a seller **only for the delivery an
11
+ independent oracle proves**. In V2, failed or unproven intervals do not advance cumulative
12
+ entitlement. The stream remains active until the buyer closes it or it expires, and reclaim
13
+ follows the checkpoint finalization or escape-window rules.
14
+
15
+ Base Sepolia constants used throughout (chainId **84532**):
16
+
17
+ | Thing | Address |
18
+ | ------ | -------------------------------------------- |
19
+ | Escrow | `0x21948a5E6AE8d9A3D1050791AB6138657Fb54286` |
20
+ | USDC | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
21
+
22
+ A complete, runnable version of this journey lives in
23
+ [`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (the live CI E2E, metrik-agent#32) — treat
24
+ it as the working reference.
25
+
26
+ ---
27
+
28
+ ## 1. Install
29
+
30
+ ```bash
31
+ pnpm add @absol-labs/agent
32
+ ```
33
+
34
+ `@absol-labs/agent` and its `@absol-labs/*` dependencies are published to the **public
35
+ npm registry** — no `.npmrc`, scope registry, or auth token is required to install.
36
+
37
+ **Runtime:** Node **20.x** (the package pins `engines.node` to `>=20 <21`; 20.19+
38
+ recommended). The Coinbase AgentKit action reads decorator metadata via
39
+ `reflect-metadata`, which only exists in the **built** output (`tsc` emits decorator
40
+ metadata; `tsx`/esbuild does not) — so `import "reflect-metadata"` **before** importing
41
+ the package, and for the AgentKit path import from the built package, not raw `.ts`.
42
+
43
+ ---
44
+
45
+ ## 2. Provision a wallet
46
+
47
+ Two supported wallet sources, both resolved through `resolveAgentWallet` /
48
+ `parseAgentWalletEnv`. Metrik never reads or exports a private key in CDP mode.
49
+
50
+ **Injected key (fastest for testnet):**
51
+
52
+ ```ts
53
+ import { privateKeyToAccount } from "viem/accounts";
54
+ import { resolveAgentWallet } from "@absol-labs/agent";
55
+
56
+ const account = privateKeyToAccount(
57
+ process.env.METRIK_AGENT_PRIVATE_KEY as `0x${string}`,
58
+ );
59
+ const wallet = await resolveAgentWallet({ injectedAccount: account });
60
+ // wallet.source === "injected"; wallet.account is a viem Account
61
+ ```
62
+
63
+ **Coinbase CDP Server Wallet v2 (production, remote signing):**
64
+
65
+ ```ts
66
+ import { CdpClient } from "@coinbase/cdp-sdk";
67
+ import { resolveAgentWallet, type CdpClientLike } from "@absol-labs/agent";
68
+
69
+ const cdp = new CdpClient({
70
+ apiKeyId: process.env.CDP_API_KEY_ID,
71
+ apiKeySecret: process.env.CDP_API_KEY_SECRET,
72
+ walletSecret: process.env.CDP_WALLET_SECRET,
73
+ });
74
+ const wallet = await resolveAgentWallet(
75
+ { cdp: { ownerName: process.env.METRIK_AGENT_CDP_OWNER_NAME! } },
76
+ { createCdpClient: () => cdp as unknown as CdpClientLike },
77
+ );
78
+ // under the hood: cdp.evm.getOrCreateAccount({ name: ownerName })
79
+ ```
80
+
81
+ CDP env: `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET`,
82
+ `METRIK_AGENT_CDP_OWNER_NAME` (+ optional `METRIK_AGENT_CDP_SMART_ACCOUNT_NAME`,
83
+ `METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT`). Or set `METRIK_AGENT_PRIVATE_KEY` and let
84
+ `parseAgentWalletEnv(process.env)` pick the injected path automatically.
85
+
86
+ ---
87
+
88
+ ## 3. Sign a spend mandate
89
+
90
+ The mandate is the guardrail: caps + allowlists checked **before any transaction**,
91
+ fail-closed. Build the EIP-712 payload with `createSpendMandateTypedData`, sign it with
92
+ the wallet, then validate the shape with `signedSpendMandateSchema`.
93
+
94
+ ```ts
95
+ import {
96
+ spendMandateSchema,
97
+ createSpendMandateTypedData,
98
+ signedSpendMandateSchema,
99
+ } from "@absol-labs/agent";
100
+ import { generatePrivateKey } from "viem/accounts"; // handy for a random bytes32 mandateId
101
+
102
+ const nowSeconds = Math.floor(Date.now() / 1000);
103
+ const account = wallet.account;
104
+
105
+ const mandate = spendMandateSchema.parse({
106
+ maxPerStreamUsdc: 100_000n, // 0.1 USDC (6 decimals) — max deposit per stream
107
+ maxTotalUsdc: 100_000n, // cumulative cap across all streams
108
+ maxRatePerSecondUsdc: 1n, // max accrual rate the agent may agree to
109
+ maxDurationSeconds: 3600, // max stream lifetime
110
+ allowedOperators: [account.address as `0x${string}`], // omit/empty ⇒ any operator
111
+ expiresAt: nowSeconds + 365 * 24 * 60 * 60,
112
+ });
113
+
114
+ const unsigned = {
115
+ mandateId: generatePrivateKey(), // any 32-byte hex
116
+ owner: account.address as `0x${string}`,
117
+ chainId: 84532,
118
+ issuedAt: nowSeconds,
119
+ mandate,
120
+ };
121
+
122
+ const signature = await account.signTypedData!(
123
+ createSpendMandateTypedData(unsigned),
124
+ );
125
+ const signedMandate = signedSpendMandateSchema.parse({
126
+ ...unsigned,
127
+ signature,
128
+ });
129
+ ```
130
+
131
+ Denials are machine-readable (`MandateDecision.reason`): `exceeds-per-stream-cap`,
132
+ `exceeds-total-cap`, `exceeds-rate-cap`, `exceeds-duration-cap`, `operator-not-allowed`,
133
+ `mandate-expired`, `mandate-revoked`, `invalid-signature`.
134
+
135
+ ---
136
+
137
+ ## 4. Discover a service
138
+
139
+ Before you can hire, you need a seller. `discoverServices` reads the Metrik listings
140
+ registry and returns typed, **verified** services — it recovers each row's operator
141
+ signature and drops any listing whose signature does not match its operator or whose
142
+ `serviceRef` is not the one derived from its signed record. It never returns an
143
+ unverifiable listing.
144
+
145
+ **Zero configuration**: the default read source is
146
+ `https://oracle.metrik.live/listings`, the oracle's public, credential-free,
147
+ CORS-enabled endpoint. You need no API key and no setup.
148
+
149
+ **It fails loudly.** An unreachable, erroring, or non-200 registry throws
150
+ `RegistryUnavailableError` rather than returning `[]`. "The marketplace was never
151
+ reached" and "the marketplace is empty" are different facts, and a caller that reads
152
+ `[]` and gives up has been misled. Catch it only if your code can genuinely handle
153
+ not having reached the marketplace at all.
154
+
155
+ ```ts
156
+ import { discoverServices, RegistryUnavailableError } from "@absol-labs/agent";
157
+
158
+ let services;
159
+ try {
160
+ services = await discoverServices({ limit: 10 });
161
+ } catch (error) {
162
+ if (error instanceof RegistryUnavailableError) {
163
+ // The registry was never read — do NOT treat this as "no services".
164
+ throw error;
165
+ }
166
+ throw error;
167
+ }
168
+
169
+ for (const s of services) {
170
+ console.log(`${s.serviceRef} → ${s.operator} @ ${s.accessUrl} [${s.access}]`);
171
+ }
172
+
173
+ const target = services[0]; // pick one to hire
174
+ ```
175
+
176
+ Each `ServiceListing` is
177
+ `{ serviceRef, operator, publicUrl, access, accessUrl, signed, category }`, with
178
+ `serviceRef`/`operator`/`publicUrl`/`accessUrl` taken from the **signed** record, never
179
+ from the unsigned envelope columns. Invoke against `accessUrl` — for a `gated` listing
180
+ that is the operator-signed gateway origin, and a gated listing with no signed access
181
+ URL is dropped rather than invoked against an unsigned one.
182
+
183
+ Need the registry's full answer — `registryAvailable`, `source`, tier and verification
184
+ summaries, and a note explaining when a non-empty source filtered to nothing? Call
185
+ `discoverServicesDetailed` instead; it returns the SDK result verbatim.
186
+
187
+ > The `category` filter matches only on an authenticated PostgREST read; the public
188
+ > endpoint serves signed records and carries no category column. Supplying `apiKey`
189
+ > also requires an explicit `registryUrl` (a Supabase project URL) — the public
190
+ > default is not a PostgREST base, so that combination is rejected rather than
191
+ > silently 404ing.
192
+
193
+ ---
194
+
195
+ ## 5. Hire a verified service
196
+
197
+ `VerifiedStreamAgentClient.openVerifiedStream` runs the mandate guard, then escrows USDC
198
+ and opens the metered stream through `@absol-labs/sdk`. It throws `MandateDeniedError`
199
+ before any transaction if the hire breaches the mandate. Feed it the `operator` +
200
+ `serviceRef` from a discovered service.
201
+
202
+ ```ts
203
+ import { VerifiedStreamAgentClient } from "@absol-labs/agent";
204
+ import type { StreamProofClientConfig } from "@absol-labs/sdk";
205
+ import { baseSepolia } from "viem/chains";
206
+ import { http } from "viem";
207
+
208
+ const sdkConfig: StreamProofClientConfig = {
209
+ chain: baseSepolia,
210
+ transport: http("https://sepolia.base.org"),
211
+ account,
212
+ escrow: "0x21948a5E6AE8d9A3D1050791AB6138657Fb54286",
213
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
214
+ };
215
+
216
+ const agent = new VerifiedStreamAgentClient(sdkConfig);
217
+
218
+ const hire = await agent.openVerifiedStream({
219
+ operator: target.operator, // seller payout wallet from discovery
220
+ serviceRef: target.serviceRef, // bytes32 the oracle verifies delivery against
221
+ budgetUsdc: 100_000n,
222
+ ratePerSecondUsdc: 1n,
223
+ maxDurationSeconds: 3600,
224
+ signedMandate,
225
+ spentSoFarUsdc: 0n,
226
+ nowSeconds: Math.floor(Date.now() / 1000),
227
+ });
228
+
229
+ console.log(`stream ${hire.streamId} opened, tx=${hire.txHash}`);
230
+ ```
231
+
232
+ Metrik is infra-agnostic (Model A): a stream targets a seller's payout wallet +
233
+ `serviceRef` only — there is no service-network field. Settlement is single-chain
234
+ on Base Sepolia.
235
+
236
+ ---
237
+
238
+ ## 6. Monitor
239
+
240
+ `getStreamStatus` returns the on-chain stream plus derived `claimable`/`reclaimable`:
241
+
242
+ ```ts
243
+ const { stream, claimable, reclaimable } = await agent.getStreamStatus(
244
+ hire.streamId,
245
+ );
246
+
247
+ console.log(stream.status); // "active" | "closed" in StreamEscrowV2
248
+ console.log(stream.accrued); // USDC accrued for oracle-verified delivery
249
+ console.log(stream.deposit); // total escrowed
250
+ console.log(claimable); // claimable by the seller now
251
+ console.log(reclaimable); // reclaimable by the buyer now
252
+ ```
253
+
254
+ Payment only ever accrues for verified seconds. In StreamEscrowV2, a checkpoint advances
255
+ the operator's cumulative entitlement; a failed or unproven interval does not. Failure does
256
+ not change the stream from active to paused: the stream stays active until buyer close or
257
+ expiry. The buyer reclaims after checkpoint finalization, or through the escape-window path
258
+ when checkpoints are unavailable.
259
+
260
+ ---
261
+
262
+ ## 7. Settle
263
+
264
+ **Seller** claims earned USDC (net of protocol fee); **buyer** closes and reclaims the
265
+ unspent balance. All three are mandate-gated.
266
+
267
+ ```ts
268
+ const now = () => Math.floor(Date.now() / 1000);
269
+ const auth = { streamId: hire.streamId, signedMandate, spentSoFarUsdc: 0n };
270
+
271
+ // Seller side — claim what delivery earned:
272
+ await agent.claimStream({ ...auth, nowSeconds: now() });
273
+
274
+ // Buyer side — close, then reclaim every unspent cent:
275
+ await agent.closeStream({ ...auth, nowSeconds: now() });
276
+ const { reclaimResult } = await agent.reclaimStream({
277
+ ...auth,
278
+ nowSeconds: now(),
279
+ });
280
+ // or in one call: agent.reclaimStream({ ...auth, nowSeconds: now(), closeFirst: true })
281
+ ```
282
+
283
+ **Buyer-protection guarantee:** the buyer can recover unspent USDC under the checkpoint /
284
+ escape-window rules, and `claimed + fees + reclaimed == deposit` conserves the escrowed
285
+ funds exactly.
286
+
287
+ ---
288
+
289
+ ## 8. Four ways to integrate
290
+
291
+ Pick the surface that matches your stack — all four sit on the **same** mandate-gated
292
+ client and business logic.
293
+
294
+ **(a) SDK client directly** — the path used above:
295
+
296
+ ```ts
297
+ import { VerifiedStreamAgentClient } from "@absol-labs/agent";
298
+ const agent = new VerifiedStreamAgentClient(sdkConfig);
299
+ await agent.openVerifiedStream({
300
+ /* ... */
301
+ });
302
+ ```
303
+
304
+ **(b) Coinbase AgentKit** — exposes `discover_services`, `hire_verified_service`,
305
+ `check_stream_status`, `reclaim_unspent`, `close_stream` as native actions:
306
+
307
+ ```ts
308
+ import "reflect-metadata";
309
+ import { createMetrikAgentKit } from "@absol-labs/agent/agentkit";
310
+
311
+ const agentKit = await createMetrikAgentKit({
312
+ walletProvider, // a CdpEvmWalletProvider / CdpSmartWalletProvider
313
+ metrik: { signedMandate, sdkConfig }, // chainId defaults to 84532
314
+ });
315
+ // then agentKit.getVercelAITools() or getLangChainTools() — or LangChain directly:
316
+ // import { createLangChainVerifiedStreamTools } from "@absol-labs/agent/langchain";
317
+ ```
318
+
319
+ **(c) MCP** — local stdio for dev, hosted HTTP for deployment:
320
+
321
+ ```bash
322
+ pnpm mcp:stdio # local: one process per caller (src/mcp/stdio.ts)
323
+ pnpm mcp:http # hosted: multi-tenant Streamable HTTP, per-caller bearer auth
324
+ ```
325
+
326
+ Tools: `discover_services`, `hire_verified_service`, `check_stream_status`, `reclaim_unspent`,
327
+ `list_streams` (+ `prove_https_response` when Reclaim creds are set). Env is read via
328
+ `parseMetrikAgentEnv` (`METRIK_AGENT_RPC_URL` / `_ESCROW` / `_USDC` + wallet vars). The
329
+ hosted endpoint requires `Authorization: Bearer <token>` before anything runs — see
330
+ [`docs/mcp-hosted.md`](./mcp-hosted.md).
331
+
332
+ **Spend mandate (required).** An MCP caller (an LLM) cannot produce an EIP-712 signature,
333
+ so the mandate is **not** a tool argument — the server holds it, self-signed from its own
334
+ funding wallet and bounded by env caps you set. A fund-moving MCP server **refuses to
335
+ start** without an explicit spend ceiling:
336
+
337
+ ```bash
338
+ METRIK_AGENT_MANDATE_MAX_PER_STREAM_USDC=100000000 # 100 USDC (atomic, 6dp) per stream
339
+ METRIK_AGENT_MANDATE_MAX_TOTAL_USDC=1000000000 # 1000 USDC cumulative across streams
340
+ METRIK_AGENT_MANDATE_MAX_RATE_PER_SECOND_USDC=1000000 # 1 USDC/sec ceiling
341
+ METRIK_AGENT_MANDATE_MAX_DURATION_SECONDS=86400 # max stream lifetime
342
+ METRIK_AGENT_MANDATE_ALLOWED_OPERATORS=0xabc...,0xdef... # optional CSV allowlist; omit = any
343
+ METRIK_AGENT_MANDATE_TTL_SECONDS=86400 # optional mandate lifetime (default 24h)
344
+ ```
345
+
346
+ The `hire_verified_service` tool then takes only the hire terms (`operator`, `serviceRef`,
347
+ `budgetUsdc`, `ratePerSecondUsdc`, optional `durationSeconds`), all USDC in **atomic
348
+ 6-decimal units**. The cumulative cap (`maxTotalUsdc`) is enforced by default; mandate
349
+ revocation requires wiring a revocation resolver (the server warns once on stderr if none
350
+ is set).
351
+
352
+ **(d) ElizaOS** — a plugin with `DISCOVER_SERVICES`, `HIRE_VERIFIED_SERVICE`,
353
+ `CHECK_STREAM_STATUS`, `RECLAIM_UNSPENT`, `CLOSE_STREAM` actions:
354
+
355
+ ```ts
356
+ import { metrikElizaPlugin } from "@absol-labs/agent/eliza"; // === createMetrikElizaPlugin
357
+
358
+ const plugin = metrikElizaPlugin({ agentClient: agent }); // agent = VerifiedStreamAgentClient
359
+ // register `plugin` on your Eliza character — see docs/eliza.md + examples/metrik-character.ts
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Honesty note
365
+
366
+ Metrik proves **verified delivery** (responded, correctly-shaped, within SLA to real
367
+ traffic) — **not output correctness** (that the model's answer is right). Pitch and treat
368
+ it accordingly. The full journey, run live against the real deployed escrow on Base
369
+ Sepolia, is [`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (`pnpm build:e2e && pnpm
370
+ e2e:cdp`) — the working reference for everything above.
@@ -0,0 +1,160 @@
1
+ # Agent Threat Model
2
+
3
+ This document is the agent-layer security review for Phase 2. It covers the
4
+ autonomous spend surface in `streamproof-agent` and maps each threat to the
5
+ current mitigation, code path, and test coverage.
6
+
7
+ It complements the cross-repo rules in
8
+ [`Absol-Labs/.github/CONTRIBUTING.md`](../../streamproof-dotgithub/CONTRIBUTING.md)
9
+ and the per-repo security pass in [`AGENTS.md`](../AGENTS.md). The same
10
+ non-negotiables apply here:
11
+
12
+ - no fake green paths
13
+ - no secrets in git or logs
14
+ - fail-closed autonomous spend
15
+ - explicit signing only through the configured account or wallet
16
+
17
+ ## Scope
18
+
19
+ In scope:
20
+
21
+ - signed spend mandates in [`src/mandates/mandate.ts`](../src/mandates/mandate.ts)
22
+ - the mandate-gated SDK wrapper in [`src/sdk/client.ts`](../src/sdk/client.ts)
23
+ - x402 verified-stream opens in [`src/x402/facilitator.ts`](../src/x402/facilitator.ts)
24
+ - MCP fund-moving tools in [`src/mcp/server.ts`](../src/mcp/server.ts)
25
+ - zkTLS consumer delivery proofs in [`src/zktls/reclaim.ts`](../src/zktls/reclaim.ts)
26
+ - wallet custody in [`src/wallet/provider.ts`](../src/wallet/provider.ts)
27
+
28
+ Out of scope:
29
+
30
+ - oracle correctness
31
+ - escrow correctness
32
+ - decentralized verifier / slashing / dispute work
33
+
34
+ ## Threat Map
35
+
36
+ | Threat | Why it matters | Current mitigation | Test / evidence | Residual risk |
37
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
+ | Runaway spend | A prompt or tool call opens streams beyond the owner's intent. | Every autonomous fund-moving path calls `checkMandate()` before the SDK can write: `openVerifiedStream()`, `claimStream()`, `closeStream()`, `reclaimStream()`, MCP `hire_verified_service`, and MCP `reclaim_unspent`. Mandates cap per-stream, cumulative spend, rate, duration, and operator. Denial is a hard stop. | [`test/mandate.test.ts`](../test/mandate.test.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts), [`test/x402.int.test.ts`](../test/x402.int.test.ts) | `spentSoFarUsdc` and revocation data are resolver inputs. If an integrator provides stale values, the agent still fails closed on signature/expiry/operator checks, but cumulative-cap freshness depends on the integrator's state source. |
39
+ | Mandate bypass | A caller skips the mandate layer and reaches the SDK write path directly from the agent layer. | The agent wrapper no longer exposes ungated autonomous write helpers. Existing-stream actions re-read the stream and authorize against the actual operator before `claim`, `close`, or `reclaim`. MCP also prechecks before dispatch. | [`src/sdk/client.ts`](../src/sdk/client.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | A caller can still use `@absol-labs/sdk` directly outside this repo. That is expected; this document only claims the agent layer itself is mandate-gated. |
40
+ | Key exposure | The agent leaks a private key through logs, env parsing, tool args, or local persistence. | No MCP tool accepts a raw key. Wallet selection is explicit: injected viem account, CDP Server Wallet v2, or an authenticated Privy embedded EOA provider. CDP/Privy paths remote-sign and never export private keys into Metrik code. `.env.example` contains names only. | [`src/wallet/provider.ts`](../src/wallet/provider.ts), [`test/wallet.test.ts`](../test/wallet.test.ts), [`test/privy-wallet.test.ts`](../test/privy-wallet.test.ts), [`README.md`](../README.md) | Injected-key mode is still as safe as the caller's own host. Privy authentication/session revocation remains the host/provider's responsibility. |
41
+ | Prompt-injection-driven spend | A hostile response or tool output tries to coerce the agent into paying a different operator or budget. | Structured inputs are validated, x402 settlement targets must match the configured chain/escrow, and any spend still must satisfy the owner's signed mandate. | [`src/x402/facilitator.ts`](../src/x402/facilitator.ts), [`test/x402.test.ts`](../test/x402.test.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts) | Prompt injection can still trigger tool invocation attempts. The safety claim is not "the prompt cannot be attacked"; it is "the attack cannot exceed the mandate or redirect settlement silently." |
42
+ | Mandate replay | A previously valid signed mandate is replayed after revocation or outside the intended lifetime. | Mandates are EIP-712 signed over `mandateId`, `owner`, `chainId`, `issuedAt`, expiry, and policy hashes. `checkMandate()` verifies signature, revocation, expiry, then policy. Integrators can revoke by `mandateId` and track cumulative spend. | [`src/mandates/mandate.ts`](../src/mandates/mandate.ts), [`test/mandate.test.ts`](../test/mandate.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | Replay protection is only as strong as revocation and cumulative-spend state freshness. The signed payload itself is bound to one chain and one owner, but revocation is local state today. |
43
+ | False or weak delivery evidence | A consumer submits a proof that is validly signed by the zkTLS system but not bound to the intended request or stream context. | The Reclaim adapter re-verifies every proof locally against the exact URL/method/body/match/redaction spec supplied by the caller, and binds the proof context to buyer/stream metadata before returning it. MCP only exposes the public-request subset so secrets do not leak through tool arguments. | [`src/zktls/reclaim.ts`](../src/zktls/reclaim.ts), [`src/x402/facilitator.ts`](../src/x402/facilitator.ts), [`src/mcp/server.ts`](../src/mcp/server.ts), [`test/reclaim.test.ts`](../test/reclaim.test.ts), [`test/x402.test.ts`](../test/x402.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | L2 is still consumer-supplied evidence. A malicious consumer can withhold proofs, and settlement must still treat L1/oracle evidence as authoritative on failure, ambiguity, and expiry. |
44
+
45
+ ## Fund-Moving Paths
46
+
47
+ These are the autonomous fund-moving paths in the current agent layer:
48
+
49
+ 1. `VerifiedStreamAgentClient.openVerifiedStream()`
50
+ 2. `VerifiedStreamAgentClient.claimStream()`
51
+ 3. `VerifiedStreamAgentClient.closeStream()`
52
+ 4. `VerifiedStreamAgentClient.reclaimStream()`
53
+ 5. MCP `hire_verified_service`
54
+ 6. MCP `reclaim_unspent`
55
+ 7. x402 `open()` via `VerifiedStreamX402Facilitator`
56
+
57
+ All of them now pass a signed mandate check before the SDK can send a write.
58
+
59
+ Read-only paths are not mandate-gated:
60
+
61
+ - `getStreamStatus()`
62
+ - MCP `check_stream_status`
63
+ - MCP `list_streams`
64
+ - MCP `prove_https_response`
65
+
66
+ ## Key Custody Options
67
+
68
+ ### Injected caller account
69
+
70
+ - Source: `METRIK_AGENT_PRIVATE_KEY`
71
+ - Trust model: the caller hosts the signer locally.
72
+ - Safety properties:
73
+ - the agent layer does not request the key through tool args
74
+ - the key is not logged or written to disk by agent code
75
+ - every autonomous write still needs a valid signed mandate
76
+
77
+ ### Coinbase CDP Server Wallet v2
78
+
79
+ - Source: `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET`
80
+ - Trust model: Coinbase holds signing material inside its remote signing boundary.
81
+ - Safety properties:
82
+ - Metrik only receives a signing-capable account object
83
+ - the private key is never exported into this repo
84
+ - the same mandate checks apply before any autonomous write
85
+
86
+ ### Privy embedded user-owned EOA
87
+
88
+ - Source: an authenticated Privy EIP-1193 wallet provider supplied by the host application
89
+ - Trust model: the user owns/authenticates the embedded EOA through Privy; the agent package
90
+ sees only the provider and public wallet address
91
+ - Safety properties:
92
+ - no Privy app secret or wallet key is accepted by the adapter
93
+ - chain and account identity are checked before construction and before each signature
94
+ - every autonomous write still requires the signed Metrik spend mandate
95
+ - Headless boundary:
96
+ - the user attaches a Privy additional signer under a non-empty provider policy
97
+ - the broker stores only a hash of the short-lived agent token and rechecks a signed
98
+ Metrik mandate, decoded calldata, stream ownership/state, expiry, and revocation
99
+ - the first successful write requests provider gas sponsorship; later writes do not
100
+ - Limitations: ERC-4337/EIP-1271 smart accounts are not supported
101
+
102
+ ### Autonomous agent-owned Privy EOA
103
+
104
+ - Source: a P-256 key generated by the agent process and persisted only through
105
+ an injected secure store or the host OS credential manager
106
+ - Trust model: the agent owns the authorization key; the broker owns only the
107
+ Privy app credentials and wallet identifier
108
+ - Safety properties:
109
+ - provisioning sends the broker a public SPKI key and proof-of-possession,
110
+ never the PKCS8 private key
111
+ - a fixed approved Metrik policy is required when the broker creates the EOA
112
+ - provisioning fails closed on a credential store that does not survive a host
113
+ reboot (process memory, or the Linux kernel keyring the OS keyring silently
114
+ falls back to when no Secret Service provider answers), because losing the
115
+ owner key makes any funds the wallet holds permanently unspendable
116
+ - every write uses prepare → local P-256 signature → one-time execute; the
117
+ broker verifies the exact canonical Privy request before forwarding it
118
+ - nonce/public-key binding, short proof/request expiry, strict P-256 parsing,
119
+ idempotency, and restricted Metrik calldata prevent replay and arbitrary RPC
120
+ - Limitations: the broker must provide persistent idempotency/replay storage and
121
+ a real Privy integration. This path does not claim smart-account/EIP-1271 or
122
+ gas sponsorship support. A failed secure store is a hard stop; plaintext
123
+ persistence is intentionally unsupported.
124
+
125
+ ### CDP-owned smart account + paymaster limits
126
+
127
+ - Source: optional smart-account provisioning in the wallet layer plus SDK
128
+ `gasAbstraction`
129
+ - Trust model: the owner signer controls a smart account, and the bundler/paymaster
130
+ can impose additional sponsorship policy.
131
+ - Safety properties:
132
+ - approvals remain bounded to the requested stream budget
133
+ - the SDK exposes a policy hook for session/per-merchant limits
134
+ - paymaster allowlists can restrict sponsored targets such as the escrow and
135
+ EntryPoint
136
+
137
+ ### Session keys
138
+
139
+ `PrivySessionBroker` issues a random, short-lived bearer credential after verifying the
140
+ user's Privy access token, embedded-wallet ownership, and owner-signed Metrik mandate.
141
+ The credential never contains a wallet key. `createPrivySessionProvider` turns it into a
142
+ restricted EIP-1193 boundary for an external agent; revocation and expiry fail closed.
143
+
144
+ ## Security Pass Alignment
145
+
146
+ The current implementation satisfies the agent-layer security pass this way:
147
+
148
+ - Every autonomous fund-moving path calls `checkMandate()` before the SDK write.
149
+ - No private key is silently held, logged, or embedded; signing is explicit via
150
+ the configured injected signer or CDP signer.
151
+ - Budget, rate, duration, and operator limits are enforced in code, not
152
+ merely documented.
153
+ - Secrets stay in env / repo-secret surfaces; `.env.example` is names only.
154
+
155
+ ## Residual Risks
156
+
157
+ - Revocation and cumulative-spend state are still local/integrator-provided.
158
+ - CDP and paymaster trust are external dependencies.
159
+ - zkTLS proofs depend on Reclaim as an external attestation system.
160
+ - L2 remains supplemental evidence; it must not override L1 failure or ambiguity.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "Metrik agent layer: x402 verified-streaming payments, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
@@ -47,6 +47,7 @@
47
47
  "sideEffects": false,
48
48
  "files": [
49
49
  "dist",
50
+ "docs",
50
51
  "src"
51
52
  ],
52
53
  "repository": {
@@ -66,38 +67,50 @@
66
67
  "pnpm": "9.15.x"
67
68
  },
68
69
  "dependencies": {
69
- "@absol-labs/sdk": "^0.7.0",
70
+ "@absol-labs/sdk": "^0.9.0",
70
71
  "@absol-labs/shared": "^0.12.1",
71
- "@coinbase/cdp-sdk": "^1.51.2",
72
72
  "@modelcontextprotocol/sdk": "^1.29.0",
73
73
  "@napi-rs/keyring": "1.3.0",
74
74
  "@privy-io/node": "0.33.0",
75
- "@reclaimprotocol/js-sdk": "^5.6.0",
76
- "@reclaimprotocol/zk-fetch": "^1.0.0",
77
75
  "reflect-metadata": "^0.2.2",
78
76
  "viem": "^2.21.0",
79
77
  "zod": "^3.24.0"
80
78
  },
81
79
  "peerDependencies": {
82
80
  "@coinbase/agentkit": "^0.10.4",
81
+ "@coinbase/cdp-sdk": "^1.51.2",
83
82
  "@elizaos/core": "^1.7.2",
84
- "@langchain/core": "^1.2.0"
83
+ "@langchain/core": "^1.2.0",
84
+ "@reclaimprotocol/js-sdk": "^5.6.0",
85
+ "@reclaimprotocol/zk-fetch": "^1.0.0"
85
86
  },
86
87
  "peerDependenciesMeta": {
87
88
  "@coinbase/agentkit": {
88
89
  "optional": true
89
90
  },
91
+ "@coinbase/cdp-sdk": {
92
+ "optional": true
93
+ },
90
94
  "@elizaos/core": {
91
95
  "optional": true
92
96
  },
93
97
  "@langchain/core": {
94
98
  "optional": true
99
+ },
100
+ "@reclaimprotocol/js-sdk": {
101
+ "optional": true
102
+ },
103
+ "@reclaimprotocol/zk-fetch": {
104
+ "optional": true
95
105
  }
96
106
  },
97
107
  "devDependencies": {
98
108
  "@coinbase/agentkit": "^0.10.4",
109
+ "@coinbase/cdp-sdk": "^1.51.2",
99
110
  "@elizaos/core": "^1.7.2",
100
111
  "@langchain/core": "^1.2.0",
112
+ "@reclaimprotocol/js-sdk": "^5.6.0",
113
+ "@reclaimprotocol/zk-fetch": "^1.0.0",
101
114
  "@swc/core": "^1.10.0",
102
115
  "@types/node": "^20.17.0",
103
116
  "prettier": "^3.4.0",