@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.
- package/README.md +19 -4
- package/dist/discovery/registry.d.ts +110 -305
- package/dist/discovery/registry.d.ts.map +1 -1
- package/dist/discovery/registry.js +141 -318
- package/dist/discovery/registry.js.map +1 -1
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +23 -6
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +103 -2
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +176 -19
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +12 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +45 -1
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/wallet/autonomous-wallet-store.d.ts +123 -0
- package/dist/wallet/autonomous-wallet-store.d.ts.map +1 -0
- package/dist/wallet/autonomous-wallet-store.js +318 -0
- package/dist/wallet/autonomous-wallet-store.js.map +1 -0
- package/dist/wallet/autonomous-wallet.d.ts +14 -39
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +12 -145
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/cdp-sdk.d.ts +23 -0
- package/dist/wallet/cdp-sdk.d.ts.map +1 -0
- package/dist/wallet/cdp-sdk.js +27 -0
- package/dist/wallet/cdp-sdk.js.map +1 -0
- package/dist/wallet/encrypted-file-credential-store.d.ts +41 -0
- package/dist/wallet/encrypted-file-credential-store.d.ts.map +1 -0
- package/dist/wallet/encrypted-file-credential-store.js +221 -0
- package/dist/wallet/encrypted-file-credential-store.js.map +1 -0
- package/dist/wallet/provider.d.ts +1 -1
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +8 -3
- package/dist/wallet/provider.js.map +1 -1
- package/dist/wallet/secret-service-probe.d.ts +56 -0
- package/dist/wallet/secret-service-probe.d.ts.map +1 -0
- package/dist/wallet/secret-service-probe.js +407 -0
- package/dist/wallet/secret-service-probe.js.map +1 -0
- package/dist/zktls/reclaim-js-sdk.d.ts +24 -0
- package/dist/zktls/reclaim-js-sdk.d.ts.map +1 -0
- package/dist/zktls/reclaim-js-sdk.js +29 -0
- package/dist/zktls/reclaim-js-sdk.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +14 -2
- package/dist/zktls/reclaim.d.ts.map +1 -1
- package/dist/zktls/reclaim.js +29 -6
- package/dist/zktls/reclaim.js.map +1 -1
- package/dist/zktls/t2-delivery-proof.d.ts +8 -1
- package/dist/zktls/t2-delivery-proof.d.ts.map +1 -1
- package/dist/zktls/t2-delivery-proof.js +22 -6
- package/dist/zktls/t2-delivery-proof.js.map +1 -1
- package/docs/agent-layer.md +150 -0
- package/docs/autonomous-privy-wallet.md +133 -0
- package/docs/crewai.md +70 -0
- package/docs/eliza.md +109 -0
- package/docs/langchain.md +63 -0
- package/docs/mcp-hosted.md +137 -0
- package/docs/privy-embedded-wallet.md +102 -0
- package/docs/quickstart.md +370 -0
- package/docs/threat-model.md +160 -0
- package/package.json +19 -6
- package/src/discovery/registry.ts +242 -414
- package/src/frameworks/agentkit.ts +24 -5
- package/src/gateway/caller-auth-gateway.ts +281 -15
- package/src/gateway/http-server.ts +64 -0
- package/src/index.ts +24 -0
- package/src/wallet/autonomous-wallet-store.ts +487 -0
- package/src/wallet/autonomous-wallet.ts +57 -224
- package/src/wallet/cdp-sdk.ts +33 -0
- package/src/wallet/encrypted-file-credential-store.ts +341 -0
- package/src/wallet/provider.ts +16 -9
- package/src/wallet/secret-service-probe.ts +487 -0
- package/src/zktls/reclaim-js-sdk.ts +50 -0
- package/src/zktls/reclaim.ts +57 -23
- 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.
|
|
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.
|
|
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",
|