@absol-labs/agent 0.10.0 → 0.11.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 +107 -23
- package/dist/capability/invocation-capability.d.ts +18 -6
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +20 -4
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.js +16 -4
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/invoke.d.ts.map +1 -1
- package/dist/cli/commands/invoke.js +1 -0
- package/dist/cli/commands/invoke.js.map +1 -1
- package/dist/cli/commands/open.d.ts.map +1 -1
- package/dist/cli/commands/open.js +63 -11
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
- package/dist/cli/commands/reclaim-unverified.js +12 -4
- package/dist/cli/commands/reclaim-unverified.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +16 -5
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/streams.js +1 -1
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/deps.d.ts +53 -7
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +65 -15
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +230 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/spend-policy.d.ts +60 -0
- package/dist/cli/spend-policy.d.ts.map +1 -0
- package/dist/cli/spend-policy.js +65 -0
- package/dist/cli/spend-policy.js.map +1 -0
- package/dist/cli/wallet.d.ts +108 -39
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +108 -43
- package/dist/cli/wallet.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +38 -3
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +25 -1
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +53 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +189 -4
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/x402-mode.d.ts +123 -0
- package/dist/gateway/x402-mode.d.ts.map +1 -0
- package/dist/gateway/x402-mode.js +212 -0
- package/dist/gateway/x402-mode.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mandates/env.d.ts +3 -2
- package/dist/mandates/env.d.ts.map +1 -1
- package/dist/mandates/env.js +3 -2
- package/dist/mandates/env.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +513 -24
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service-invoker.d.ts +97 -0
- package/dist/mcp/service-invoker.d.ts.map +1 -0
- package/dist/mcp/service-invoker.js +167 -0
- package/dist/mcp/service-invoker.js.map +1 -0
- package/dist/sdk/invoke.d.ts +19 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +69 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/x402/delivery-oracle.d.ts +108 -0
- package/dist/x402/delivery-oracle.d.ts.map +1 -0
- package/dist/x402/delivery-oracle.js +174 -0
- package/dist/x402/delivery-oracle.js.map +1 -0
- package/dist/x402/facilitator-server-entry.d.ts +3 -0
- package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
- package/dist/x402/facilitator-server-entry.js +27 -0
- package/dist/x402/facilitator-server-entry.js.map +1 -0
- package/dist/x402/facilitator-server.d.ts +73 -0
- package/dist/x402/facilitator-server.d.ts.map +1 -0
- package/dist/x402/facilitator-server.js +305 -0
- package/dist/x402/facilitator-server.js.map +1 -0
- package/dist/x402/facilitator.d.ts +69 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +76 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402/payment-requirements.d.ts +106 -0
- package/dist/x402/payment-requirements.d.ts.map +1 -0
- package/dist/x402/payment-requirements.js +138 -0
- package/dist/x402/payment-requirements.js.map +1 -0
- package/dist/x402/relayer.d.ts +41 -0
- package/dist/x402/relayer.d.ts.map +1 -0
- package/dist/x402/relayer.js +64 -0
- package/dist/x402/relayer.js.map +1 -0
- package/dist/x402/resource-index.d.ts +75 -0
- package/dist/x402/resource-index.d.ts.map +1 -0
- package/dist/x402/resource-index.js +126 -0
- package/dist/x402/resource-index.js.map +1 -0
- package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
- package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
- package/dist/x402/verified-delivery-facilitator.js +423 -0
- package/dist/x402/verified-delivery-facilitator.js.map +1 -0
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +12 -8
- package/docs/cli.md +158 -22
- package/docs/mcp-hosted.md +4 -3
- package/docs/mcp-reference.md +175 -0
- package/docs/quickstart.md +18 -4
- package/docs/threat-model.md +4 -3
- package/docs/x402.md +648 -0
- package/package.json +18 -7
- package/src/capability/invocation-capability.ts +17 -1
- package/src/cli/commands/claim.ts +30 -4
- package/src/cli/commands/close.ts +20 -4
- package/src/cli/commands/invoke.ts +1 -0
- package/src/cli/commands/open.ts +76 -10
- package/src/cli/commands/reclaim-unverified.ts +16 -4
- package/src/cli/commands/reclaim.ts +22 -5
- package/src/cli/commands/streams.ts +1 -1
- package/src/cli/deps.ts +115 -24
- package/src/cli/privy.ts +396 -0
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/wallet.ts +188 -57
- package/src/gateway/caller-auth-gateway.ts +77 -6
- package/src/gateway/http-server.ts +280 -6
- package/src/gateway/x402-mode.ts +350 -0
- package/src/index.ts +40 -5
- package/src/mandates/env.ts +3 -2
- package/src/mcp/server.ts +681 -29
- package/src/mcp/service-invoker.ts +285 -0
- package/src/sdk/invoke.ts +90 -12
- package/src/x402/delivery-oracle.ts +285 -0
- package/src/x402/facilitator-server-entry.ts +35 -0
- package/src/x402/facilitator-server.ts +445 -0
- package/src/x402/facilitator.ts +125 -46
- package/src/x402/payment-requirements.ts +231 -0
- package/src/x402/relayer.ts +122 -0
- package/src/x402/resource-index.ts +228 -0
- package/src/x402/verified-delivery-facilitator.ts +744 -0
package/docs/cli.md
CHANGED
|
@@ -60,47 +60,183 @@ The rule is enforced twice:
|
|
|
60
60
|
|
|
61
61
|
## Wallet
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Three wallet sources, resolved in a **fixed order** — first match wins:
|
|
64
64
|
|
|
65
65
|
| Order | Variables | Wallet |
|
|
66
66
|
| ----- | ------------------------------------------------------------------------------------------ | --------------------------------------- |
|
|
67
67
|
| 1 | `METRIK_AGENT_PRIVATE_KEY` | a raw Base Sepolia test key |
|
|
68
68
|
| 2 | `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET`, `METRIK_AGENT_CDP_OWNER_NAME` | a Coinbase CDP Server Wallet v2 account |
|
|
69
|
+
| 3 | `METRIK_AGENT_PRIVY_*` (below) | a Privy wallet reached through a broker |
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
CLI, the MCP server and the framework adapters identically. The order is
|
|
72
|
-
rather than "whichever is configured": an environment carrying
|
|
73
|
-
which address funds the stream, and that ambiguity is a fund-moving one. When
|
|
74
|
-
present the
|
|
71
|
+
The first two are the same order `parseAgentWalletEnv()` uses, so one environment
|
|
72
|
+
configures the CLI, the MCP server and the framework adapters identically. The order is
|
|
73
|
+
**fixed** rather than "whichever is configured": an environment carrying two is ambiguous
|
|
74
|
+
about which address funds the stream, and that ambiguity is a fund-moving one. When more
|
|
75
|
+
than one is present the earlier entry wins and the CLI says so on stderr rather than
|
|
76
|
+
choosing quietly.
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
deeper:
|
|
78
|
+
Misconfigurations are refused up front, at exit `3`, instead of failing somewhere deeper:
|
|
78
79
|
|
|
79
|
-
- **A half-configured
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
- **A half-configured wallet** ("you set three of the four CDP variables", "you set the
|
|
81
|
+
Privy session token but not the address") is reported as exactly that, naming the
|
|
82
|
+
missing variables. Rendering it as "no wallet is configured" would send the reader
|
|
83
|
+
looking in the wrong place.
|
|
82
84
|
- **A CDP _smart_ account** (`METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT` /
|
|
83
85
|
`METRIK_AGENT_CDP_SMART_ACCOUNT_NAME`) is refused. It is an ERC-4337 account at a
|
|
84
86
|
different address from its owner, reached through a bundler; this CLI broadcasts
|
|
85
87
|
through `--rpc-url`, so it would sign and spend from the **owner** while you funded the
|
|
86
88
|
smart account.
|
|
87
|
-
- **
|
|
88
|
-
later inside viem.
|
|
89
|
+
- **An account that cannot sign** is refused with the reason, rather than accepted and
|
|
90
|
+
failed later inside viem. The check is about capability, not locality: every wallet must
|
|
91
|
+
be able to sign EIP-712 typed data, and only the paths that broadcast through
|
|
92
|
+
`--rpc-url` additionally need a local signer.
|
|
89
93
|
|
|
90
94
|
There is no `--private-key` flag, and there will not be one — nor a flag for any other
|
|
91
95
|
credential: a secret on the command line lands in shell history, in `ps` output, and in
|
|
92
96
|
any CI log that echoes the command. Commands print the derived **address**; the
|
|
93
97
|
credential is never echoed, including by `metrik mcp --print-config`.
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
### Privy
|
|
100
|
+
|
|
101
|
+
A Privy wallet differs from the other two in one structural way: its account has no local
|
|
102
|
+
`signTransaction`. It signs and broadcasts by sending `eth_sendTransaction` /
|
|
103
|
+
`eth_signTypedData_v4` to a provider, so for a Privy wallet the **provider is the wallet
|
|
104
|
+
client's transport** instead of the plain `http(--rpc-url)` a local signer uses. That is
|
|
105
|
+
the whole of the wiring. Private-key and CDP runs are unchanged, byte for byte.
|
|
106
|
+
|
|
107
|
+
There are two shapes. Set one; setting both uses the agent-owned wallet and says so.
|
|
108
|
+
|
|
109
|
+
**Agent-owned (`privy-autonomous`)** — headless, no browser, no user login:
|
|
110
|
+
|
|
111
|
+
| Variable | Required | What it is |
|
|
112
|
+
| ------------------------------- | -------- | ------------------------------------------------------------------------ |
|
|
113
|
+
| `METRIK_AGENT_PRIVY_WALLET_KEY` | yes | the credential-store key of a wallet you already provisioned |
|
|
114
|
+
| `METRIK_AGENT_PRIVY_BROKER_URL` | no | overrides the broker the wallet was provisioned against |
|
|
115
|
+
| `METRIK_WALLET_ENCRYPTION_KEY` | no | selects the encrypted-file store (the durable option on a headless host) |
|
|
116
|
+
|
|
117
|
+
**The CLI never provisions a wallet for you.** A fund-moving command is the wrong place to
|
|
118
|
+
mint one, so if nothing is stored under that key the CLI says so and stops. Provision once,
|
|
119
|
+
deliberately — see [autonomous-privy-wallet.md](./autonomous-privy-wallet.md):
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { provisionMetrikAutonomousWallet } from "@absol-labs/agent";
|
|
123
|
+
|
|
124
|
+
const wallet = await provisionMetrikAutonomousWallet({
|
|
125
|
+
storeKey: "my-agent/base-sepolia",
|
|
126
|
+
});
|
|
127
|
+
console.log(wallet.address); // fund this with test USDC and Base Sepolia ETH
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
export METRIK_AGENT_PRIVY_WALLET_KEY=my-agent/base-sepolia
|
|
132
|
+
metrik invoke 0x… --path /health
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Read the credential-store durability section of that document before funding anything: on
|
|
136
|
+
a headless Linux host the OS store is **not** durable and provisioning is refused, which
|
|
137
|
+
is why `METRIK_WALLET_ENCRYPTION_KEY` exists. The CLI selects the same store the same way
|
|
138
|
+
— set that variable and it reads the encrypted file, unset and it reads the OS store — so
|
|
139
|
+
a mismatch between provisioning and use simply finds no wallet and says so.
|
|
140
|
+
|
|
141
|
+
**User-owned broker session (`privy-session`)** — a scoped session minted from a Privy
|
|
142
|
+
login (today that means the dApp, which holds the user's Privy access token):
|
|
143
|
+
|
|
144
|
+
| Variable | Required | What it is |
|
|
145
|
+
| ---------------------------------- | -------- | ------------------------------------------------------------------ |
|
|
146
|
+
| `METRIK_AGENT_PRIVY_BROKER_URL` | yes | the broker that minted the token; a token is valid nowhere else |
|
|
147
|
+
| `METRIK_AGENT_PRIVY_SESSION_TOKEN` | yes | the token from the broker's `POST /v1/sessions` |
|
|
148
|
+
| `METRIK_AGENT_PRIVY_ADDRESS` | yes | the embedded wallet address the session is bound to |
|
|
149
|
+
| `METRIK_AGENT_PRIVY_APP_ID` | yes | the **public** Privy app id — the broker serves it at `/v1/config` |
|
|
150
|
+
|
|
151
|
+
#### Which broker the CLI talks to
|
|
152
|
+
|
|
153
|
+
The agent-owned path defaults to the broker URL the wallet was provisioned against, which
|
|
154
|
+
for `provisionMetrikAutonomousWallet()` is the Metrik broker at
|
|
155
|
+
`https://wallet.137.23.50.249.sslip.io`. Its `GET /health` and `GET /v1/config` answer
|
|
156
|
+
publicly; `/v1/config` carries only the public app id, the public policy id and the fixed
|
|
157
|
+
policy ceilings, and nothing there is a secret. The Privy **authorization key** stays on
|
|
158
|
+
the broker and is never served.
|
|
159
|
+
|
|
160
|
+
If you would rather not trust that deployment, run your own: it is
|
|
161
|
+
`metrik-privy-broker` from this package (`src/wallet/privy-broker-server.ts`), it needs
|
|
162
|
+
your own `PRIVY_APP_ID` / `PRIVY_APP_SECRET` / authorization key / policy id, and you then
|
|
163
|
+
point `METRIK_AGENT_PRIVY_BROKER_URL` at it. A wallet provisioned against one broker
|
|
164
|
+
cannot be authorized by another — only the broker holding that wallet's Privy
|
|
165
|
+
authorization key can sign for it — so switching brokers means provisioning a new wallet.
|
|
166
|
+
|
|
167
|
+
#### The spend mandate, and why a Privy wallet does not sign one
|
|
168
|
+
|
|
169
|
+
Every fund-moving command in this package normally self-signs an EIP-712
|
|
170
|
+
`SpendMandate` first. Read why, from `src/cli/mandate.ts`: _"the person typing
|
|
171
|
+
the command IS the mandate owner: they hold the key and they just stated the
|
|
172
|
+
exact spend on the command line."_ The mandate exists because, for a key-holding
|
|
173
|
+
wallet, the client is the last line of defence — so the agent is made to state
|
|
174
|
+
its own ceiling and is held to it.
|
|
175
|
+
|
|
176
|
+
**For a Privy wallet that premise is false.** The user does not hold the key: it
|
|
177
|
+
lives at Privy and is exercised by the broker, which caps every `openStream`
|
|
178
|
+
against a policy the client cannot reach and cannot raise. A self-signed mandate
|
|
179
|
+
on top of that is an agent attesting to its own limits when a stronger limit is
|
|
180
|
+
already enforced somewhere the agent cannot touch.
|
|
181
|
+
|
|
182
|
+
So for a policy-enforced wallet the CLI **does not sign a mandate**, and
|
|
183
|
+
**substitutes the real ceiling instead of dropping the check**: it reads the
|
|
184
|
+
broker's public `GET /v1/config` and refuses an over-ceiling hire locally, in the
|
|
185
|
+
same shape as the gas and USDC preflights — fail early with an actionable
|
|
186
|
+
message rather than paying gas to learn a knowable fact. `open --dry-run`
|
|
187
|
+
reports the ceiling and whether the hire fits; a real `open` refuses.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
mandate not signed — this wallet's ceiling is enforced by its broker, which is stronger
|
|
191
|
+
spend policy 1 USDC (1000000 atomic) total, 1000 atomic/second, 1h max — enforced by https://wallet.137.23.50.249.sslip.io
|
|
192
|
+
fits policy yes
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Two things this deliberately does NOT do:
|
|
196
|
+
|
|
197
|
+
- **It does not widen the broker.** Both brokers restrict `eth_signTypedData_v4`
|
|
198
|
+
to `primaryType: "InvocationCapability"`. Teaching them to sign a
|
|
199
|
+
`SpendMandate` would weaken the exact property that makes the wallet
|
|
200
|
+
non-custodial, to reinstate a check that is the weaker of the two.
|
|
201
|
+
- **It does not retire `METRIK_AGENT_MANDATE_*`.** Those caps are a standing
|
|
202
|
+
ceiling the operator deliberately chose, and can be TIGHTER than the broker
|
|
203
|
+
policy. They still apply, enforced locally, because no mandate is signed to
|
|
204
|
+
carry them.
|
|
205
|
+
|
|
206
|
+
Do not "restore" the mandate for consistency. It would both weaken the guarantee
|
|
207
|
+
and break every fund-moving command against the broker as deployed.
|
|
208
|
+
|
|
209
|
+
#### What works, and what does not
|
|
210
|
+
|
|
211
|
+
`open`, `close`, `reclaim`, `reclaim-unverified` and `invoke` all work with a
|
|
212
|
+
Privy wallet.
|
|
213
|
+
|
|
214
|
+
`metrik claim` does not, and that is not a Privy defect: `claim` is the
|
|
215
|
+
**operator's** side, and a broker policy allowlists the **buyer** actions only
|
|
216
|
+
(`openStream`, `close`, `reclaim`, `reclaimUnverified`, and a bounded USDC
|
|
217
|
+
approval to the escrow). An operator claims with the operator's own key. The CLI
|
|
218
|
+
says this before it runs rather than letting you discover it at the broker.
|
|
219
|
+
|
|
220
|
+
Privy support is Base Sepolia (84532) only, EOA only. No ERC-4337, no EIP-1271,
|
|
221
|
+
no gas sponsorship claim.
|
|
222
|
+
|
|
223
|
+
#### Trust model, honestly
|
|
224
|
+
|
|
225
|
+
A raw `METRIK_AGENT_PRIVATE_KEY` is the simplest thing that can possibly work and the CLI
|
|
226
|
+
holds the whole key: anything that reads your environment can move every token in that
|
|
227
|
+
wallet, forever.
|
|
228
|
+
|
|
229
|
+
Privy is not "no custody" — it is **different** custody. The signing key lives at Privy and
|
|
230
|
+
is exercised by the broker, so the parties who can move funds are Privy and whoever runs
|
|
231
|
+
the broker, together. What the CLI holds is a P-256 authorization key (agent-owned) or a
|
|
232
|
+
short-lived session token (session) — credentials that authorize a **policy-bounded**
|
|
233
|
+
request, not credentials that can drain the wallet. Leaking one is bounded by the broker's
|
|
234
|
+
policy: Base Sepolia only, the escrow and USDC contracts only, zero native value, and the
|
|
235
|
+
installed ceilings (`/v1/config`). Losing the agent-owned P-256 key is the mirror risk: the
|
|
236
|
+
wallet can never be authorized again and anything it holds is unspendable.
|
|
237
|
+
|
|
238
|
+
Pick a raw key if you are the only party and you want no third party in the path. Pick
|
|
239
|
+
Privy if you want a policy boundary between the machine running the agent and the money.
|
|
104
240
|
|
|
105
241
|
`invoke` needs the key too, even though it broadcasts nothing: it signs an **off-chain
|
|
106
242
|
EIP-712 invocation capability** as the stream's buyer. `open --dry-run` is the one path
|
package/docs/mcp-hosted.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Hosted MCP over Streamable HTTP
|
|
2
2
|
|
|
3
3
|
The agent lane ships two MCP transports over the **same** tool set
|
|
4
|
-
(`
|
|
5
|
-
and the optional
|
|
4
|
+
(`discover_services`, `hire_verified_service`, `invoke_service`,
|
|
5
|
+
`check_stream_status`, `reclaim_unspent`, `list_streams`, and the optional
|
|
6
|
+
`prove_https_response` — see [`mcp-reference.md`](./mcp-reference.md)):
|
|
6
7
|
|
|
7
8
|
- **stdio** (`src/mcp/stdio.ts`) — local dev, one process per caller. Started with
|
|
8
9
|
the published `metrik-mcp` executable (`npx --package @absol-labs/agent metrik-mcp`),
|
|
@@ -79,7 +80,7 @@ network but need distinct wallets override only the wallet vars in their `env`.
|
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
82
|
export METRIK_AGENT_RPC_URL="https://base-sepolia.example/rpc"
|
|
82
|
-
export METRIK_AGENT_ESCROW="
|
|
83
|
+
export METRIK_AGENT_ESCROW="0x0f09f36Ccc05A7c9882F438721C08De314dFd46C" # StreamEscrowV2 — NOT the retired V1 escrow
|
|
83
84
|
export METRIK_AGENT_USDC="0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
84
85
|
# ... wallet vars (private key or CDP) ...
|
|
85
86
|
export METRIK_MCP_TENANTS='[{"id":"acme","token":"REPLACE_WITH_SECRET"}]'
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# MCP tool reference
|
|
2
|
+
|
|
3
|
+
The canonical description of what `@absol-labs/agent`'s MCP server offers an
|
|
4
|
+
agent, what each tool costs, and what it refuses to do. Both transports — stdio
|
|
5
|
+
(`metrik-mcp`) and hosted Streamable HTTP (`metrik-mcp-http`, see
|
|
6
|
+
[`mcp-hosted.md`](./mcp-hosted.md)) — serve exactly this set.
|
|
7
|
+
|
|
8
|
+
The names and descriptions here are the ones in `METRIK_MCP_TOOLS`
|
|
9
|
+
(`src/mcp/server.ts`); a clean-install check asserts the published `metrik-mcp`
|
|
10
|
+
binary really offers them (`scripts/check-packed-package.mjs`).
|
|
11
|
+
|
|
12
|
+
## What you are buying: TIME, not calls
|
|
13
|
+
|
|
14
|
+
A stream rents an endpoint that the oracle keeps probing. **The meter runs on
|
|
15
|
+
availability that VERIFIED, not on the requests you send.** Cost is
|
|
16
|
+
`ratePerSecondUsdc x verified seconds`, capped at the escrowed budget. One
|
|
17
|
+
request and a thousand requests cost the same second, and the clock keeps
|
|
18
|
+
running while you send none.
|
|
19
|
+
|
|
20
|
+
A reviewer who opened a stream, made a single `invoke_service` call and was
|
|
21
|
+
billed for 302 seconds was billed exactly as designed. `reclaim_unspent` stops
|
|
22
|
+
the clock; nothing else does, and **nothing pushes money back on its own**.
|
|
23
|
+
|
|
24
|
+
The escrow proves _delivery_, not _correctness_. An endpoint that answers, on
|
|
25
|
+
time, in the right shape, is what gets paid — whether its answer is any good is
|
|
26
|
+
an economic question, not a cryptographic one.
|
|
27
|
+
|
|
28
|
+
## The tools
|
|
29
|
+
|
|
30
|
+
| Tool | Moves funds | What it does |
|
|
31
|
+
| ----------------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
32
|
+
| `discover_services` | no | Lists marketplace services whose operator signature verifies. |
|
|
33
|
+
| `hire_verified_service` | **yes** | Escrows USDC and opens a per-second stream. **Starts the meter.** |
|
|
34
|
+
| `invoke_service` | no | Calls the service the stream is paying for. Adds no cost. |
|
|
35
|
+
| `check_stream_status` | no | Reads live status, settled amount, claimable/reclaimable. |
|
|
36
|
+
| `reclaim_unspent` | **yes** | Closes the stream (**stops the meter**) and returns unspent USDC to the buyer. |
|
|
37
|
+
| `list_streams` | no | Lists the streams this server opened. |
|
|
38
|
+
| `prove_https_response` | no | Buyer-side zkTLS proof of a response. Registered only with Reclaim credentials. |
|
|
39
|
+
|
|
40
|
+
### `discover_services`
|
|
41
|
+
|
|
42
|
+
`category?`, `limit?`. Returns `serviceRef`, `operator`, `publicUrl`, `access`,
|
|
43
|
+
`accessUrl` per listing.
|
|
44
|
+
|
|
45
|
+
The registry is an **untrusted transport**: every `serviceRef` is re-derived
|
|
46
|
+
from the signed record and the operator's signature recovered, so a compromised
|
|
47
|
+
registry can neither invent an operator to pay nor redirect an invocation. Rows
|
|
48
|
+
that fail verification are dropped. An unreachable registry is an **error**, not
|
|
49
|
+
an empty list — "zero services" and "could not reach the marketplace" must never
|
|
50
|
+
read the same.
|
|
51
|
+
|
|
52
|
+
### `hire_verified_service`
|
|
53
|
+
|
|
54
|
+
`operator`, `serviceRef`, `budgetUsdc`, `ratePerSecondUsdc`, `durationSeconds?`.
|
|
55
|
+
All USDC amounts are **atomic 6-decimal units** (`1 USDC = "1000000"`).
|
|
56
|
+
|
|
57
|
+
Bounded by the server's own signed spend mandate (per-stream, cumulative, rate,
|
|
58
|
+
duration, operator allowlist, expiry), configured through
|
|
59
|
+
`METRIK_AGENT_MANDATE_*`. An MCP caller cannot produce an EIP-712 signature, so
|
|
60
|
+
the mandate is never a tool argument — the server holds it and fails closed
|
|
61
|
+
without one.
|
|
62
|
+
|
|
63
|
+
`operator` is the wallet that gets paid, and it arrives as a separate argument
|
|
64
|
+
from `serviceRef`. The pair is cross-checked against the verified registry, and
|
|
65
|
+
the check **fails closed**: the hire proceeds only on a positive match.
|
|
66
|
+
|
|
67
|
+
A contradicted pairing escrows a buyer's USDC to a stranger for someone else's
|
|
68
|
+
service, and nothing downstream catches it — the escrow does not know who owns a
|
|
69
|
+
`serviceRef`, and the oracle verifies the real endpoint while the money accrues
|
|
70
|
+
elsewhere. A registry read that fails is no evidence the pairing is sound:
|
|
71
|
+
settlement does not depend on that HTTP endpoint being reachable from the MCP
|
|
72
|
+
host, so during an outage the bug would be fully live. An unlisted `serviceRef`
|
|
73
|
+
is refused for the same reason — a hostile registry cannot forge a match, but it
|
|
74
|
+
can suppress a row, which would otherwise downgrade a contradiction into a pass.
|
|
75
|
+
|
|
76
|
+
Refusing strands nothing: no USDC has moved and the same call succeeds once the
|
|
77
|
+
registry answers. If you must accept that risk,
|
|
78
|
+
`METRIK_AGENT_ALLOW_UNVERIFIED_OPERATOR=true` downgrades "could not verify" to a
|
|
79
|
+
warning on the result. It is operator-set input only — never a tool argument,
|
|
80
|
+
because the caller being asked to relax a fund-safety check is the LLM the check
|
|
81
|
+
exists to constrain — and it never overrides a positive contradiction.
|
|
82
|
+
|
|
83
|
+
The result carries a `metering` block: the rate, the budget, the maximum this
|
|
84
|
+
stream can ever spend, and what starts and stops the clock.
|
|
85
|
+
|
|
86
|
+
### `invoke_service`
|
|
87
|
+
|
|
88
|
+
`streamId`, `path?` (default `/`), `method?` (default `GET`), `headers?`,
|
|
89
|
+
`body?`, `ttlSeconds?` (default 60, max 300).
|
|
90
|
+
|
|
91
|
+
Signs a single-use EIP-712 `InvocationCapability` as the stream's buyer — scoped
|
|
92
|
+
to exactly this method and path — and calls the service through the operator's
|
|
93
|
+
caller-auth gateway.
|
|
94
|
+
|
|
95
|
+
- **The origin is not a parameter.** It is resolved from the operator-signed
|
|
96
|
+
listing for the stream's own `serviceRef`. An LLM-chosen base URL is precisely
|
|
97
|
+
the injection this refuses; there is no override.
|
|
98
|
+
- **The path cannot leave that origin.** The URL is resolved first and the
|
|
99
|
+
result is then proven to carry the signed origin and stay under the signed
|
|
100
|
+
base path — a prefix check on the raw path is not sufficient, because the URL
|
|
101
|
+
parser reads a backslash as a host separator for http(s). Backslashes and
|
|
102
|
+
control characters are rejected outright as well. Resolution happens **before**
|
|
103
|
+
the capability is signed, so a path that escapes never causes a buyer-signed
|
|
104
|
+
credential to exist.
|
|
105
|
+
- **Fails closed before any network call** if the stream is not active, has
|
|
106
|
+
expired, or does not belong to this server's wallet — each with an error that
|
|
107
|
+
names the next step.
|
|
108
|
+
- A gateway rejection is surfaced with its machine-readable reason
|
|
109
|
+
(`stream-not-funded`, `capability-expired`, `nonce-replayed`, …) and a next
|
|
110
|
+
step, rather than a bare `403`.
|
|
111
|
+
- Responses are capped at 64 KiB with `bodyTruncated` reported explicitly.
|
|
112
|
+
- It signs no transaction and moves no funds.
|
|
113
|
+
|
|
114
|
+
### `check_stream_status`
|
|
115
|
+
|
|
116
|
+
`streamId`. Returns the stream, `claimable`, `reclaimable`, and a `metering`
|
|
117
|
+
block. A stream id that does not exist on the configured escrow is reported as
|
|
118
|
+
such rather than as "closed" — an unset escrow slot decodes as an all-zero
|
|
119
|
+
struct instead of reverting.
|
|
120
|
+
|
|
121
|
+
### `reclaim_unspent`
|
|
122
|
+
|
|
123
|
+
`streamId`, `closeFirst?`. Closing is what stops the meter.
|
|
124
|
+
|
|
125
|
+
Authorized on the mandate owner's signature **alone** — never gated by mandate
|
|
126
|
+
expiry, revocation, or caps, because those must never strand a buyer's own
|
|
127
|
+
funds. It also works for a stream this server has no local record of (the
|
|
128
|
+
escrow itself enforces buyer-only recovery), so losing in-memory state cannot
|
|
129
|
+
stand between a buyer and their escrow.
|
|
130
|
+
|
|
131
|
+
### `list_streams`
|
|
132
|
+
|
|
133
|
+
No arguments. Every active stream listed is still accruing cost per verified
|
|
134
|
+
second, whether or not it is being invoked.
|
|
135
|
+
|
|
136
|
+
### `prove_https_response`
|
|
137
|
+
|
|
138
|
+
Registered **only** when Reclaim credentials are configured
|
|
139
|
+
(`RECLAIM_APP_ID` + `RECLAIM_APP_SECRET`). Generates a buyer-side zkTLS proof of an HTTPS response.
|
|
140
|
+
|
|
141
|
+
**Public listings only.** The Reclaim attestor fetches the URL itself, from its
|
|
142
|
+
own network position, and cannot carry this stream's single-use caller-auth
|
|
143
|
+
capability — so against a gated listing the only thing it could prove is its own
|
|
144
|
+
`403`. The tool says that instead of producing one. Use `invoke_service` for
|
|
145
|
+
gated services; that delivery is covered by the oracle's own verification.
|
|
146
|
+
|
|
147
|
+
The proof target is pinned to the stream's verified listing origin. A proof of
|
|
148
|
+
some other origin, stamped with this stream's delivery binding, would be
|
|
149
|
+
evidence of a delivery that did not happen here.
|
|
150
|
+
|
|
151
|
+
This is an L2 signal to be cross-checked with oracle/L1 evidence — not a
|
|
152
|
+
substitute for it.
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
`metrik mcp --print-config` prints the `mcpServers` block to paste into an MCP
|
|
157
|
+
client, already carrying every variable the server fails closed without. Secrets
|
|
158
|
+
in it are placeholders by design.
|
|
159
|
+
|
|
160
|
+
| Variable | Required | Meaning |
|
|
161
|
+
| ---------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `METRIK_AGENT_RPC_URL` | **yes** | Base Sepolia RPC. |
|
|
163
|
+
| `METRIK_AGENT_ESCROW` | **yes** | The **StreamEscrowV2** address. The retired V1 escrow is refused. |
|
|
164
|
+
| `METRIK_AGENT_USDC` | **yes** | Settlement token. |
|
|
165
|
+
| `METRIK_AGENT_ESCROW_GENERATION` | no | `v2` (default, and the only supported value). Declared, not probed. |
|
|
166
|
+
| `METRIK_AGENT_CHAIN_ID` | no | Defaults to `84532`; nothing else is accepted. |
|
|
167
|
+
| `METRIK_AGENT_REGISTRY_URL` | no | Registry the invocation origin is verified against. |
|
|
168
|
+
| `METRIK_AGENT_ALLOW_UNVERIFIED_OPERATOR` | no | `true` downgrades an unverifiable operator pairing from a refusal to a warning. Off by default. |
|
|
169
|
+
| `METRIK_AGENT_MCP_STATE_FILE` | no | Persists the stream registry across restarts. |
|
|
170
|
+
| `METRIK_AGENT_MANDATE_*` | **yes** | The four spend ceilings. No ceiling, no server. |
|
|
171
|
+
| wallet vars | **yes** | `METRIK_AGENT_PRIVATE_KEY`, or the CDP/Privy set. |
|
|
172
|
+
|
|
173
|
+
The escrow generation is a **declared fact, not an inference**: the server
|
|
174
|
+
refuses the retired V1 address by name, because a wrong-generation read does not
|
|
175
|
+
revert — it returns confident nonsense about someone's money.
|
package/docs/quickstart.md
CHANGED
|
@@ -16,9 +16,15 @@ Base Sepolia constants used throughout (chainId **84532**):
|
|
|
16
16
|
|
|
17
17
|
| Thing | Address |
|
|
18
18
|
| ------ | -------------------------------------------- |
|
|
19
|
-
| Escrow | `
|
|
19
|
+
| Escrow | `0x0f09f36Ccc05A7c9882F438721C08De314dFd46C` |
|
|
20
20
|
| USDC | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
|
|
21
21
|
|
|
22
|
+
The escrow above is **StreamEscrowV2**, the checkpoint-settled contract the
|
|
23
|
+
oracle and the dApp actually settle against. The retired V1 metered escrow
|
|
24
|
+
(`0x21948a5E…`) is not a valid target: its stream tuple diverges from V2's at
|
|
25
|
+
index 5, so a V2 read of it does not revert — it decodes other fields into
|
|
26
|
+
correctly-named ones and reports a confident wrong answer.
|
|
27
|
+
|
|
22
28
|
A complete, runnable version of this journey lives in
|
|
23
29
|
[`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (the live CI E2E, metrik-agent#32) — treat
|
|
24
30
|
it as the working reference.
|
|
@@ -209,7 +215,10 @@ const sdkConfig: StreamProofClientConfig = {
|
|
|
209
215
|
chain: baseSepolia,
|
|
210
216
|
transport: http("https://sepolia.base.org"),
|
|
211
217
|
account,
|
|
212
|
-
escrow: "
|
|
218
|
+
escrow: "0x0f09f36Ccc05A7c9882F438721C08De314dFd46C",
|
|
219
|
+
// Selects the checkpoint decoder. Without it the SDK reads through the legacy
|
|
220
|
+
// V1 ABI, and `claimable()`/`reclaimable()` revert against StreamEscrowV2.
|
|
221
|
+
escrowV2: "0x0f09f36Ccc05A7c9882F438721C08De314dFd46C",
|
|
213
222
|
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
214
223
|
};
|
|
215
224
|
|
|
@@ -336,8 +345,13 @@ pnpm mcp:stdio # same server, from inside this repository (src/mcp/stdio.ts)
|
|
|
336
345
|
pnpm mcp:http # hosted: multi-tenant Streamable HTTP, per-caller bearer auth
|
|
337
346
|
```
|
|
338
347
|
|
|
339
|
-
Tools: `discover_services`, `hire_verified_service`, `
|
|
340
|
-
`list_streams` (+ `prove_https_response` when
|
|
348
|
+
Tools: `discover_services`, `hire_verified_service`, `invoke_service`,
|
|
349
|
+
`check_stream_status`, `reclaim_unspent`, `list_streams` (+ `prove_https_response` when
|
|
350
|
+
Reclaim creds are set) — full reference in [`docs/mcp-reference.md`](./mcp-reference.md).
|
|
351
|
+
`invoke_service` is the "use" half: it signs a stream-scoped capability and calls the
|
|
352
|
+
operator-signed origin, so an agent never needs an out-of-band credential. Note the meter
|
|
353
|
+
runs on verified SECONDS, not on calls — invoking more often is free, and invoking never
|
|
354
|
+
costs the same. Env is read via
|
|
341
355
|
`parseMetrikAgentEnv` (`METRIK_AGENT_RPC_URL` / `_ESCROW` / `_USDC` + wallet vars). The
|
|
342
356
|
hosted endpoint requires `Authorization: Bearer <token>` before anything runs — see
|
|
343
357
|
[`docs/mcp-hosted.md`](./mcp-hosted.md).
|
package/docs/threat-model.md
CHANGED
|
@@ -20,7 +20,8 @@ In scope:
|
|
|
20
20
|
|
|
21
21
|
- signed spend mandates in [`src/mandates/mandate.ts`](../src/mandates/mandate.ts)
|
|
22
22
|
- the mandate-gated SDK wrapper in [`src/sdk/client.ts`](../src/sdk/client.ts)
|
|
23
|
-
-
|
|
23
|
+
- Metrik `402` verified-stream opens in [`src/x402/facilitator.ts`](../src/x402/facilitator.ts)
|
|
24
|
+
(a Metrik-native challenge flow, not the x402 protocol — see [docs/x402.md](./x402.md))
|
|
24
25
|
- MCP fund-moving tools in [`src/mcp/server.ts`](../src/mcp/server.ts)
|
|
25
26
|
- zkTLS consumer delivery proofs in [`src/zktls/reclaim.ts`](../src/zktls/reclaim.ts)
|
|
26
27
|
- wallet custody in [`src/wallet/provider.ts`](../src/wallet/provider.ts)
|
|
@@ -38,7 +39,7 @@ Out of scope:
|
|
|
38
39
|
| 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
40
|
| 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
41
|
| 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,
|
|
42
|
+
| 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, challenge 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
43
|
| 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
44
|
| 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
|
|
|
@@ -52,7 +53,7 @@ These are the autonomous fund-moving paths in the current agent layer:
|
|
|
52
53
|
4. `VerifiedStreamAgentClient.reclaimStream()`
|
|
53
54
|
5. MCP `hire_verified_service`
|
|
54
55
|
6. MCP `reclaim_unspent`
|
|
55
|
-
7.
|
|
56
|
+
7. Metrik `402` `open()` via `VerifiedStreamFacilitator`
|
|
56
57
|
|
|
57
58
|
All of them now pass a signed mandate check before the SDK can send a write.
|
|
58
59
|
|