@absol-labs/agent 0.9.1 → 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.
Files changed (293) hide show
  1. package/README.md +184 -26
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/amounts.d.ts +26 -0
  6. package/dist/cli/amounts.d.ts.map +1 -0
  7. package/dist/cli/amounts.js +71 -0
  8. package/dist/cli/amounts.js.map +1 -0
  9. package/dist/cli/args.d.ts +32 -0
  10. package/dist/cli/args.d.ts.map +1 -0
  11. package/dist/cli/args.js +119 -0
  12. package/dist/cli/args.js.map +1 -0
  13. package/dist/cli/attesters.d.ts +51 -0
  14. package/dist/cli/attesters.d.ts.map +1 -0
  15. package/dist/cli/attesters.js +58 -0
  16. package/dist/cli/attesters.js.map +1 -0
  17. package/dist/cli/bin.d.ts +3 -0
  18. package/dist/cli/bin.d.ts.map +1 -0
  19. package/dist/cli/bin.js +5 -0
  20. package/dist/cli/bin.js.map +1 -0
  21. package/dist/cli/commands/claim.d.ts +13 -0
  22. package/dist/cli/commands/claim.d.ts.map +1 -0
  23. package/dist/cli/commands/claim.js +102 -0
  24. package/dist/cli/commands/claim.js.map +1 -0
  25. package/dist/cli/commands/close.d.ts +3 -0
  26. package/dist/cli/commands/close.d.ts.map +1 -0
  27. package/dist/cli/commands/close.js +55 -0
  28. package/dist/cli/commands/close.js.map +1 -0
  29. package/dist/cli/commands/discover.d.ts +3 -0
  30. package/dist/cli/commands/discover.d.ts.map +1 -0
  31. package/dist/cli/commands/discover.js +183 -0
  32. package/dist/cli/commands/discover.js.map +1 -0
  33. package/dist/cli/commands/invoke.d.ts +3 -0
  34. package/dist/cli/commands/invoke.d.ts.map +1 -0
  35. package/dist/cli/commands/invoke.js +142 -0
  36. package/dist/cli/commands/invoke.js.map +1 -0
  37. package/dist/cli/commands/mcp.d.ts +3 -0
  38. package/dist/cli/commands/mcp.d.ts.map +1 -0
  39. package/dist/cli/commands/mcp.js +113 -0
  40. package/dist/cli/commands/mcp.js.map +1 -0
  41. package/dist/cli/commands/open.d.ts +3 -0
  42. package/dist/cli/commands/open.d.ts.map +1 -0
  43. package/dist/cli/commands/open.js +331 -0
  44. package/dist/cli/commands/open.js.map +1 -0
  45. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  46. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  47. package/dist/cli/commands/reclaim-unverified.js +172 -0
  48. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  49. package/dist/cli/commands/reclaim.d.ts +33 -0
  50. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  51. package/dist/cli/commands/reclaim.js +308 -0
  52. package/dist/cli/commands/reclaim.js.map +1 -0
  53. package/dist/cli/commands/status.d.ts +3 -0
  54. package/dist/cli/commands/status.d.ts.map +1 -0
  55. package/dist/cli/commands/status.js +43 -0
  56. package/dist/cli/commands/status.js.map +1 -0
  57. package/dist/cli/commands/streams.d.ts +3 -0
  58. package/dist/cli/commands/streams.d.ts.map +1 -0
  59. package/dist/cli/commands/streams.js +257 -0
  60. package/dist/cli/commands/streams.js.map +1 -0
  61. package/dist/cli/commands/watch.d.ts +3 -0
  62. package/dist/cli/commands/watch.d.ts.map +1 -0
  63. package/dist/cli/commands/watch.js +55 -0
  64. package/dist/cli/commands/watch.js.map +1 -0
  65. package/dist/cli/config.d.ts +61 -0
  66. package/dist/cli/config.d.ts.map +1 -0
  67. package/dist/cli/config.js +140 -0
  68. package/dist/cli/config.js.map +1 -0
  69. package/dist/cli/context.d.ts +29 -0
  70. package/dist/cli/context.d.ts.map +1 -0
  71. package/dist/cli/context.js +2 -0
  72. package/dist/cli/context.js.map +1 -0
  73. package/dist/cli/deps.d.ts +191 -0
  74. package/dist/cli/deps.d.ts.map +1 -0
  75. package/dist/cli/deps.js +277 -0
  76. package/dist/cli/deps.js.map +1 -0
  77. package/dist/cli/descriptor.d.ts +61 -0
  78. package/dist/cli/descriptor.d.ts.map +1 -0
  79. package/dist/cli/descriptor.js +78 -0
  80. package/dist/cli/descriptor.js.map +1 -0
  81. package/dist/cli/errors.d.ts +48 -0
  82. package/dist/cli/errors.d.ts.map +1 -0
  83. package/dist/cli/errors.js +129 -0
  84. package/dist/cli/errors.js.map +1 -0
  85. package/dist/cli/escape-hatch.d.ts +50 -0
  86. package/dist/cli/escape-hatch.d.ts.map +1 -0
  87. package/dist/cli/escape-hatch.js +45 -0
  88. package/dist/cli/escape-hatch.js.map +1 -0
  89. package/dist/cli/index.d.ts +8 -0
  90. package/dist/cli/index.d.ts.map +1 -0
  91. package/dist/cli/index.js +7 -0
  92. package/dist/cli/index.js.map +1 -0
  93. package/dist/cli/listings.d.ts +15 -0
  94. package/dist/cli/listings.d.ts.map +1 -0
  95. package/dist/cli/listings.js +56 -0
  96. package/dist/cli/listings.js.map +1 -0
  97. package/dist/cli/mandate.d.ts +66 -0
  98. package/dist/cli/mandate.d.ts.map +1 -0
  99. package/dist/cli/mandate.js +61 -0
  100. package/dist/cli/mandate.js.map +1 -0
  101. package/dist/cli/output.d.ts +33 -0
  102. package/dist/cli/output.d.ts.map +1 -0
  103. package/dist/cli/output.js +34 -0
  104. package/dist/cli/output.js.map +1 -0
  105. package/dist/cli/preflight.d.ts +38 -0
  106. package/dist/cli/preflight.d.ts.map +1 -0
  107. package/dist/cli/preflight.js +72 -0
  108. package/dist/cli/preflight.js.map +1 -0
  109. package/dist/cli/privy.d.ts +83 -0
  110. package/dist/cli/privy.d.ts.map +1 -0
  111. package/dist/cli/privy.js +230 -0
  112. package/dist/cli/privy.js.map +1 -0
  113. package/dist/cli/progress.d.ts +36 -0
  114. package/dist/cli/progress.d.ts.map +1 -0
  115. package/dist/cli/progress.js +40 -0
  116. package/dist/cli/progress.js.map +1 -0
  117. package/dist/cli/run.d.ts +25 -0
  118. package/dist/cli/run.d.ts.map +1 -0
  119. package/dist/cli/run.js +173 -0
  120. package/dist/cli/run.js.map +1 -0
  121. package/dist/cli/scan-range.d.ts +55 -0
  122. package/dist/cli/scan-range.d.ts.map +1 -0
  123. package/dist/cli/scan-range.js +57 -0
  124. package/dist/cli/scan-range.js.map +1 -0
  125. package/dist/cli/spend-policy.d.ts +60 -0
  126. package/dist/cli/spend-policy.d.ts.map +1 -0
  127. package/dist/cli/spend-policy.js +65 -0
  128. package/dist/cli/spend-policy.js.map +1 -0
  129. package/dist/cli/status.d.ts +90 -0
  130. package/dist/cli/status.d.ts.map +1 -0
  131. package/dist/cli/status.js +252 -0
  132. package/dist/cli/status.js.map +1 -0
  133. package/dist/cli/timeout.d.ts +30 -0
  134. package/dist/cli/timeout.d.ts.map +1 -0
  135. package/dist/cli/timeout.js +55 -0
  136. package/dist/cli/timeout.js.map +1 -0
  137. package/dist/cli/version.d.ts +12 -0
  138. package/dist/cli/version.d.ts.map +1 -0
  139. package/dist/cli/version.js +22 -0
  140. package/dist/cli/version.js.map +1 -0
  141. package/dist/cli/wallet.d.ts +164 -0
  142. package/dist/cli/wallet.d.ts.map +1 -0
  143. package/dist/cli/wallet.js +234 -0
  144. package/dist/cli/wallet.js.map +1 -0
  145. package/dist/frameworks/crewai.d.ts +10 -0
  146. package/dist/frameworks/crewai.d.ts.map +1 -1
  147. package/dist/frameworks/crewai.js +12 -2
  148. package/dist/frameworks/crewai.js.map +1 -1
  149. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  150. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  151. package/dist/gateway/caller-auth-gateway.js +25 -1
  152. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  153. package/dist/gateway/http-server.d.ts +53 -0
  154. package/dist/gateway/http-server.d.ts.map +1 -1
  155. package/dist/gateway/http-server.js +189 -4
  156. package/dist/gateway/http-server.js.map +1 -1
  157. package/dist/gateway/server-entry.d.ts +1 -0
  158. package/dist/gateway/server-entry.js +1 -0
  159. package/dist/gateway/server-entry.js.map +1 -1
  160. package/dist/gateway/x402-mode.d.ts +123 -0
  161. package/dist/gateway/x402-mode.d.ts.map +1 -0
  162. package/dist/gateway/x402-mode.js +212 -0
  163. package/dist/gateway/x402-mode.js.map +1 -0
  164. package/dist/index.d.ts +6 -4
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +8 -3
  167. package/dist/index.js.map +1 -1
  168. package/dist/mandates/env.d.ts +3 -2
  169. package/dist/mandates/env.d.ts.map +1 -1
  170. package/dist/mandates/env.js +3 -2
  171. package/dist/mandates/env.js.map +1 -1
  172. package/dist/mcp/http-server.d.ts +1 -0
  173. package/dist/mcp/http-server.js +1 -0
  174. package/dist/mcp/http-server.js.map +1 -1
  175. package/dist/mcp/server.d.ts +49 -0
  176. package/dist/mcp/server.d.ts.map +1 -1
  177. package/dist/mcp/server.js +533 -25
  178. package/dist/mcp/server.js.map +1 -1
  179. package/dist/mcp/service-invoker.d.ts +97 -0
  180. package/dist/mcp/service-invoker.d.ts.map +1 -0
  181. package/dist/mcp/service-invoker.js +167 -0
  182. package/dist/mcp/service-invoker.js.map +1 -0
  183. package/dist/mcp/stdio.d.ts +1 -0
  184. package/dist/mcp/stdio.js +1 -0
  185. package/dist/mcp/stdio.js.map +1 -1
  186. package/dist/sdk/client.d.ts +25 -1
  187. package/dist/sdk/client.d.ts.map +1 -1
  188. package/dist/sdk/client.js +20 -1
  189. package/dist/sdk/client.js.map +1 -1
  190. package/dist/sdk/invoke.d.ts +19 -4
  191. package/dist/sdk/invoke.d.ts.map +1 -1
  192. package/dist/sdk/invoke.js +69 -5
  193. package/dist/sdk/invoke.js.map +1 -1
  194. package/dist/x402/delivery-oracle.d.ts +108 -0
  195. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  196. package/dist/x402/delivery-oracle.js +174 -0
  197. package/dist/x402/delivery-oracle.js.map +1 -0
  198. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  199. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  200. package/dist/x402/facilitator-server-entry.js +27 -0
  201. package/dist/x402/facilitator-server-entry.js.map +1 -0
  202. package/dist/x402/facilitator-server.d.ts +73 -0
  203. package/dist/x402/facilitator-server.d.ts.map +1 -0
  204. package/dist/x402/facilitator-server.js +305 -0
  205. package/dist/x402/facilitator-server.js.map +1 -0
  206. package/dist/x402/facilitator.d.ts +69 -17
  207. package/dist/x402/facilitator.d.ts.map +1 -1
  208. package/dist/x402/facilitator.js +76 -33
  209. package/dist/x402/facilitator.js.map +1 -1
  210. package/dist/x402/payment-requirements.d.ts +106 -0
  211. package/dist/x402/payment-requirements.d.ts.map +1 -0
  212. package/dist/x402/payment-requirements.js +138 -0
  213. package/dist/x402/payment-requirements.js.map +1 -0
  214. package/dist/x402/relayer.d.ts +41 -0
  215. package/dist/x402/relayer.d.ts.map +1 -0
  216. package/dist/x402/relayer.js +64 -0
  217. package/dist/x402/relayer.js.map +1 -0
  218. package/dist/x402/resource-index.d.ts +75 -0
  219. package/dist/x402/resource-index.d.ts.map +1 -0
  220. package/dist/x402/resource-index.js +126 -0
  221. package/dist/x402/resource-index.js.map +1 -0
  222. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  223. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  224. package/dist/x402/verified-delivery-facilitator.js +423 -0
  225. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  226. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  227. package/docs/agent-layer.md +14 -9
  228. package/docs/cli.md +705 -0
  229. package/docs/crewai.md +6 -2
  230. package/docs/eliza.md +6 -1
  231. package/docs/mcp-hosted.md +7 -5
  232. package/docs/mcp-reference.md +175 -0
  233. package/docs/quickstart.md +36 -9
  234. package/docs/threat-model.md +4 -3
  235. package/docs/x402.md +648 -0
  236. package/package.json +33 -10
  237. package/src/capability/invocation-capability.ts +17 -1
  238. package/src/cli/amounts.ts +102 -0
  239. package/src/cli/args.ts +150 -0
  240. package/src/cli/attesters.ts +86 -0
  241. package/src/cli/bin.ts +5 -0
  242. package/src/cli/commands/claim.ts +126 -0
  243. package/src/cli/commands/close.ts +69 -0
  244. package/src/cli/commands/discover.ts +228 -0
  245. package/src/cli/commands/invoke.ts +178 -0
  246. package/src/cli/commands/mcp.ts +135 -0
  247. package/src/cli/commands/open.ts +397 -0
  248. package/src/cli/commands/reclaim-unverified.ts +218 -0
  249. package/src/cli/commands/reclaim.ts +377 -0
  250. package/src/cli/commands/status.ts +56 -0
  251. package/src/cli/commands/streams.ts +344 -0
  252. package/src/cli/commands/watch.ts +70 -0
  253. package/src/cli/config.ts +208 -0
  254. package/src/cli/context.ts +30 -0
  255. package/src/cli/deps.ts +528 -0
  256. package/src/cli/descriptor.ts +133 -0
  257. package/src/cli/errors.ts +162 -0
  258. package/src/cli/escape-hatch.ts +92 -0
  259. package/src/cli/index.ts +27 -0
  260. package/src/cli/listings.ts +77 -0
  261. package/src/cli/mandate.ts +126 -0
  262. package/src/cli/output.ts +63 -0
  263. package/src/cli/preflight.ts +88 -0
  264. package/src/cli/privy.ts +396 -0
  265. package/src/cli/progress.ts +75 -0
  266. package/src/cli/run.ts +209 -0
  267. package/src/cli/scan-range.ts +76 -0
  268. package/src/cli/spend-policy.ts +139 -0
  269. package/src/cli/status.ts +381 -0
  270. package/src/cli/timeout.ts +64 -0
  271. package/src/cli/version.ts +21 -0
  272. package/src/cli/wallet.ts +372 -0
  273. package/src/frameworks/crewai.ts +13 -2
  274. package/src/gateway/caller-auth-gateway.ts +77 -6
  275. package/src/gateway/http-server.ts +280 -6
  276. package/src/gateway/server-entry.ts +1 -0
  277. package/src/gateway/x402-mode.ts +350 -0
  278. package/src/index.ts +40 -5
  279. package/src/mandates/env.ts +3 -2
  280. package/src/mcp/http-server.ts +1 -0
  281. package/src/mcp/server.ts +706 -30
  282. package/src/mcp/service-invoker.ts +285 -0
  283. package/src/mcp/stdio.ts +1 -0
  284. package/src/sdk/client.ts +33 -1
  285. package/src/sdk/invoke.ts +90 -12
  286. package/src/x402/delivery-oracle.ts +285 -0
  287. package/src/x402/facilitator-server-entry.ts +35 -0
  288. package/src/x402/facilitator-server.ts +445 -0
  289. package/src/x402/facilitator.ts +125 -46
  290. package/src/x402/payment-requirements.ts +231 -0
  291. package/src/x402/relayer.ts +122 -0
  292. package/src/x402/resource-index.ts +228 -0
  293. package/src/x402/verified-delivery-facilitator.ts +744 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Pure block-range arithmetic for the escrow log scan behind `metrik streams`.
3
+ * No RPC, no clock, no env — so the coverage rules below are unit-testable
4
+ * without a network, which is the point: they decide when an empty result is
5
+ * allowed to be rendered as "you have no streams".
6
+ *
7
+ * ## Why the anchor is the deploy block
8
+ *
9
+ * A fixed lookback ("the last N blocks") cannot describe coverage: a wallet
10
+ * whose stream predates the window is shown an empty list with nothing to
11
+ * distinguish it from a wallet that never opened one. For a payments tool,
12
+ * telling someone their escrowed funds do not exist is the worst failure we
13
+ * have. The escrow has NO history before its deploy block, so that block is the
14
+ * honest anchor — and whether the budget reached it is then a fact the command
15
+ * can state.
16
+ *
17
+ * ## Why the window is 10 000 blocks
18
+ *
19
+ * `buyer` and `operator` are INDEXED on `StreamOpened`, so a per-wallet scan is
20
+ * a server-side topic filter whose response is tiny regardless of range. What
21
+ * public RPCs cap is the per-call BLOCK RANGE, and a topic filter does not lift
22
+ * it: `sepolia.base.org` answers a wider range with `-32614 "eth_getLogs is
23
+ * limited to a 10,000 range"`. So the page size is the cap, not a guess below
24
+ * it.
25
+ */
26
+
27
+ /**
28
+ * getLogs page size. The window is `chunk + 1` blocks (bounds inclusive), so
29
+ * 9 999 is the largest page `sepolia.base.org` accepts.
30
+ */
31
+ export const LOG_SCAN_CHUNK = 9_999n;
32
+
33
+ /**
34
+ * Window budget for one scan. 400 x 10 000 = 4 000 000 blocks, comfortably more
35
+ * than the live escrow's history (~1.06M blocks on 2026-09-17), so the default
36
+ * scan is complete rather than merely large. It is a CEILING, not a fixed cost:
37
+ * windows are built for the actual anchor-to-head span. Exceeding it is not an
38
+ * error — it is reported as `degraded`.
39
+ */
40
+ export const LOG_SCAN_MAX_WINDOWS = 400;
41
+
42
+ export interface ScanRange {
43
+ readonly fromBlock: bigint;
44
+ readonly toBlock: bigint;
45
+ /** Inclusive `[from, to]` windows, in ascending order. */
46
+ readonly windows: readonly (readonly [bigint, bigint])[];
47
+ /** True iff the budget could NOT reach `anchor`, i.e. the scan is partial. */
48
+ readonly degraded: boolean;
49
+ }
50
+
51
+ /**
52
+ * Page `[anchor, head]` into non-overlapping inclusive windows.
53
+ *
54
+ * An RPC that is behind the deployment yields an empty-but-valid range rather
55
+ * than a negative one, and an anchor past the head is clamped: a node still
56
+ * syncing must not produce a nonsense scan.
57
+ */
58
+ export function escrowScanRange(
59
+ head: bigint,
60
+ anchorBlock: bigint,
61
+ chunk: bigint = LOG_SCAN_CHUNK,
62
+ maxWindows: number = LOG_SCAN_MAX_WINDOWS,
63
+ ): ScanRange {
64
+ const anchor =
65
+ anchorBlock < 0n ? 0n : anchorBlock > head ? head : anchorBlock;
66
+ const maxSpan = (chunk + 1n) * BigInt(maxWindows);
67
+ const degraded = head - anchor > maxSpan;
68
+ const fromBlock = degraded ? head - maxSpan : anchor;
69
+
70
+ const windows: (readonly [bigint, bigint])[] = [];
71
+ for (let start = fromBlock; start <= head; start += chunk + 1n) {
72
+ windows.push([start, start + chunk > head ? head : start + chunk]);
73
+ }
74
+
75
+ return { fromBlock, toBlock: head, windows, degraded };
76
+ }
@@ -0,0 +1,139 @@
1
+ import { formatUsdc } from "./amounts.js";
2
+ import { CliError } from "./errors.js";
3
+ import type { PrivySpendPolicy } from "./privy.js";
4
+ import type { RequestedSpend } from "./mandate.js";
5
+ import type { ServerMandateCaps } from "../mandates/env.js";
6
+
7
+ /**
8
+ * Where a hire's spend ceiling comes from, and who enforces it.
9
+ *
10
+ * The CLI has two kinds of wallet, and they differ in WHO holds the key — which
11
+ * is the whole reason the self-signed spend mandate exists at all:
12
+ *
13
+ * - **Key-holding wallet** (a raw private key, a CDP account). The person
14
+ * typing the command holds the key, so nothing outside this process bounds
15
+ * what that key can spend. `signCliMandate()` makes the agent state its own
16
+ * ceiling, and `checkMandate()` enforces it here. That is the best available
17
+ * guarantee when the client is the last line of defence.
18
+ *
19
+ * - **Policy-enforced wallet** (Privy, either shape). The key lives at Privy
20
+ * and is exercised by the broker, which caps every `openStream` against a
21
+ * policy the client CANNOT reach or raise. Re-signing a local mandate on top
22
+ * of that adds nothing: it is an agent attesting to its own limits when a
23
+ * stronger limit is already enforced somewhere the agent cannot touch. Worse,
24
+ * it cannot even be produced — the broker's typed-data policy allows an
25
+ * `InvocationCapability` and nothing else — so insisting on it would block
26
+ * the wallet whose ceiling is the stronger of the two.
27
+ *
28
+ * So for a policy-enforced wallet the CLI does not self-sign a mandate. It
29
+ * SUBSTITUTES the real ceiling rather than dropping the check: it reads the
30
+ * broker's public `/v1/config` and refuses an over-ceiling hire locally, in the
31
+ * same shape as the gas and USDC preflights — fail early with an actionable
32
+ * message instead of paying gas to learn a knowable fact.
33
+ *
34
+ * **Do not "restore" the mandate here for consistency.** Its premise is
35
+ * key-holding, and that premise is false for these wallets. Reinstating it
36
+ * would weaken the property that makes them non-custodial (the broker would
37
+ * have to be widened to sign `SpendMandate`) while making every fund-moving
38
+ * command fail against the broker as deployed.
39
+ */
40
+ export interface SpendCeiling {
41
+ readonly policy: PrivySpendPolicy;
42
+ }
43
+
44
+ /**
45
+ * Refuse a hire the broker will refuse anyway, before any gas is spent.
46
+ *
47
+ * Mirrors the broker's own `openStream` check exactly — `deposit`,
48
+ * `ratePerSecond` and `duration` each against their ceiling — because a
49
+ * divergent copy would either block hires the broker would allow or promise
50
+ * hires it will not.
51
+ */
52
+ export function assertWithinSpendPolicy(
53
+ policy: PrivySpendPolicy,
54
+ spend: RequestedSpend,
55
+ ): void {
56
+ const over: string[] = [];
57
+ if (spend.budgetUsdc > policy.maxTotalUsdc) {
58
+ over.push(
59
+ `--budget ${formatUsdc(spend.budgetUsdc)} exceeds the policy ceiling of ${formatUsdc(policy.maxTotalUsdc)}`,
60
+ );
61
+ }
62
+ if (spend.ratePerSecondUsdc > policy.maxRatePerSecondUsdc) {
63
+ over.push(
64
+ `--rate ${spend.ratePerSecondUsdc} atomic/second exceeds the policy ceiling of ${policy.maxRatePerSecondUsdc} atomic/second`,
65
+ );
66
+ }
67
+ if (spend.durationSeconds > policy.maxDurationSeconds) {
68
+ over.push(
69
+ `--duration ${spend.durationSeconds}s exceeds the policy ceiling of ${policy.maxDurationSeconds}s`,
70
+ );
71
+ }
72
+ if (over.length === 0) return;
73
+
74
+ throw new CliError(
75
+ `this hire is outside the spend policy your wallet is capped at: ${over.join("; ")}`,
76
+ {
77
+ exitCode: 3,
78
+ hint:
79
+ `the ceiling is enforced by the broker at ${policy.brokerUrl} (GET /v1/config), not by this CLI — you cannot raise it from this side, ` +
80
+ "which is exactly what makes the wallet non-custodial. Size the stream to fit, or use a key-holding wallet.",
81
+ },
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Refuse a hire that exceeds an OPERATOR-SET standing ceiling.
87
+ *
88
+ * `METRIK_AGENT_MANDATE_*` is not the self-signed mandate: it is a ceiling the
89
+ * operator deliberately configured, and it can be TIGHTER than the broker
90
+ * policy. A policy-enforced wallet skips the self-signed mandate, so without
91
+ * this the env caps would silently stop applying to exactly the wallets an
92
+ * unattended run is most likely to use. Enforced locally here instead, with the
93
+ * same comparisons `checkMandate()` makes.
94
+ */
95
+ export function assertWithinOperatorCaps(
96
+ caps: ServerMandateCaps,
97
+ spend: RequestedSpend,
98
+ ): void {
99
+ const over: string[] = [];
100
+ if (spend.budgetUsdc > caps.maxPerStreamUsdc) {
101
+ over.push(
102
+ `--budget ${formatUsdc(spend.budgetUsdc)} exceeds METRIK_AGENT_MANDATE max-per-stream ${formatUsdc(caps.maxPerStreamUsdc)}`,
103
+ );
104
+ }
105
+ if (spend.budgetUsdc > caps.maxTotalUsdc) {
106
+ over.push(
107
+ `--budget ${formatUsdc(spend.budgetUsdc)} exceeds METRIK_AGENT_MANDATE max-total ${formatUsdc(caps.maxTotalUsdc)}`,
108
+ );
109
+ }
110
+ if (spend.ratePerSecondUsdc > caps.maxRatePerSecondUsdc) {
111
+ over.push(
112
+ `--rate ${spend.ratePerSecondUsdc} atomic/second exceeds METRIK_AGENT_MANDATE max rate ${caps.maxRatePerSecondUsdc} atomic/second`,
113
+ );
114
+ }
115
+ if (spend.durationSeconds > caps.maxDurationSeconds) {
116
+ over.push(
117
+ `--duration ${spend.durationSeconds}s exceeds METRIK_AGENT_MANDATE max duration ${caps.maxDurationSeconds}s`,
118
+ );
119
+ }
120
+ if (
121
+ caps.allowedOperators.length > 0 &&
122
+ !caps.allowedOperators.some(
123
+ (operator) => operator.toLowerCase() === spend.operator.toLowerCase(),
124
+ )
125
+ ) {
126
+ over.push(
127
+ `operator ${spend.operator} is not in METRIK_AGENT_MANDATE_ALLOWED_OPERATORS`,
128
+ );
129
+ }
130
+ if (over.length === 0) return;
131
+
132
+ throw new CliError(
133
+ `this hire is outside the standing mandate caps in your environment: ${over.join("; ")}`,
134
+ {
135
+ exitCode: 3,
136
+ hint: "these caps come from METRIK_AGENT_MANDATE_* and are yours to change. They are enforced locally for a policy-enforced wallet, which signs no mandate of its own.",
137
+ },
138
+ );
139
+ }
@@ -0,0 +1,381 @@
1
+ import type { CheckpointProof, StreamV2 } from "@absol-labs/sdk";
2
+
3
+ import { formatDuration, formatUsdc } from "./amounts.js";
4
+ import {
5
+ attesterDisclosure,
6
+ operatorAttestsOwnDelivery,
7
+ readAttesterSet,
8
+ type AttesterSet,
9
+ } from "./attesters.js";
10
+ import { describeError } from "./errors.js";
11
+ import {
12
+ describeEscapeHatch,
13
+ escapeHatchWindow,
14
+ formatTimestamp,
15
+ readChainNow,
16
+ type EscapeHatchWindow,
17
+ } from "./escape-hatch.js";
18
+ import type { CliReadClient } from "./deps.js";
19
+
20
+ /**
21
+ * Whether the accrual figures on this screen are FINAL.
22
+ *
23
+ * `reclaimableFor` is answered against a checkpoint's cumulative, and the
24
+ * escrow's own `reclaimWithProof` refuses until a checkpoint's `checkedAt`
25
+ * reaches the stream's stop time (`SettlementWindowOpen`). Between closing a
26
+ * stream and the first checkpoint that covers the close, the latest checkpoint
27
+ * therefore describes a SHORTER stream than the one that exists — so the
28
+ * reclaimable figure computed from it is larger than the amount that can
29
+ * actually be reclaimed, and shrinks when the covering checkpoint lands.
30
+ *
31
+ * A live acceptance run read `reclaimable: 1400` immediately after `close`, `0`
32
+ * two minutes later, and got `NothingToReclaim()` from the reclaim in between.
33
+ * Nobody reads "buyer reclaimable 0.0014 USDC" as "a provisional figure that
34
+ * may fall to zero" — they read it as money they have. So while this is
35
+ * pending, the CLI prints no settled-looking number at all.
36
+ */
37
+ export interface SettlementCoverage {
38
+ /**
39
+ * The instant accrual stops: `closedAt`, or `expiresAt` once it has passed.
40
+ * `null` while the stream is still running, where there is nothing to cover.
41
+ */
42
+ readonly stopAt: number | null;
43
+ /** How far the oracle has published. `null` when it could not be read. */
44
+ readonly coveredUpTo: number | null;
45
+ /** True when a stop time exists and no known checkpoint reaches it. */
46
+ readonly pending: boolean;
47
+ }
48
+
49
+ /**
50
+ * Coverage from the three facts that decide it. Pure, so the rule is testable
51
+ * without an RPC.
52
+ *
53
+ * Unknown coverage counts as PENDING, not as covered: the buyer-favouring error
54
+ * is to withhold a number we cannot stand behind, never to print one we cannot.
55
+ */
56
+ export function settlementCoverage(
57
+ stream: Pick<StreamV2, "closedAt" | "expiresAt">,
58
+ latestCheckpointCheckedAt: number | null,
59
+ nowSeconds: number,
60
+ ): SettlementCoverage {
61
+ const stopAt =
62
+ stream.closedAt > 0
63
+ ? stream.closedAt
64
+ : nowSeconds >= stream.expiresAt
65
+ ? stream.expiresAt
66
+ : null;
67
+ const coveredUpTo =
68
+ latestCheckpointCheckedAt !== null && latestCheckpointCheckedAt > 0
69
+ ? latestCheckpointCheckedAt
70
+ : null;
71
+ return {
72
+ stopAt,
73
+ coveredUpTo,
74
+ pending: stopAt !== null && (coveredUpTo === null || coveredUpTo < stopAt),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * A stream as the CLI reports it.
80
+ *
81
+ * The accrual figures come from the ORACLE's checkpoint proof, not from the
82
+ * stream tuple. `StreamEscrowV2.getStream` reports `claimedCumulative = 0` and
83
+ * `settledCumulative = 0` for a stream's whole active life — `settledCumulative`
84
+ * is written by `reclaim` — so a status built from stream fields would print
85
+ * "earned: 0" for a stream that had been delivering for an hour.
86
+ */
87
+ export interface StreamReport {
88
+ readonly streamId: `0x${string}`;
89
+ readonly stream: StreamV2;
90
+ /** `null` until the oracle has published a checkpoint covering this stream. */
91
+ readonly proof: CheckpointProof | null;
92
+ /** Why there is no proof yet, when applicable. */
93
+ readonly proofUnavailable: string | null;
94
+ readonly claimable: bigint | null;
95
+ readonly reclaimable: bigint | null;
96
+ readonly protocolFeeBps: number | null;
97
+ /**
98
+ * When the buyer's no-proof escape hatch opens, and whether it already has.
99
+ * `null` only when the escrow would not answer.
100
+ *
101
+ * Without this a buyer had no command that could tell them "the proof path is
102
+ * blocked, but the no-proof path opens at T+grace" — the two facts they need
103
+ * together when the oracle has stopped answering.
104
+ */
105
+ readonly escapeHatch: EscapeHatchWindow | null;
106
+ /**
107
+ * How far the oracle has published (`latestCheckpointCheckedAt`). A stale
108
+ * value beside a blocked reclaim IS the explanation for the blocked reclaim.
109
+ */
110
+ readonly latestCheckpointCheckedAt: number | null;
111
+ readonly nowSeconds: number;
112
+ /** `local` when the chain's clock could not be read and the caller's was used. */
113
+ readonly clockSource: "chain" | "local";
114
+ /** Whether `claimable` / `reclaimable` are final. See {@link SettlementCoverage}. */
115
+ readonly settlement: SettlementCoverage;
116
+ /**
117
+ * The escrow's oracle signer set, and therefore whether this stream's
118
+ * operator is also one of the parties attesting to its own delivery.
119
+ * `signers: null` means the set could not be read — which is UNKNOWN, not
120
+ * "no overlap".
121
+ */
122
+ readonly attesters: AttesterSet;
123
+ }
124
+
125
+ export async function readStreamReport(
126
+ read: CliReadClient,
127
+ streamId: `0x${string}`,
128
+ nowSeconds: number,
129
+ ): Promise<StreamReport> {
130
+ const stream = await read.getStreamV2(streamId);
131
+
132
+ // Timing is answered against the chain, because every deadline on this screen
133
+ // — expiry, and the escape hatch in particular — is compared against
134
+ // `block.timestamp` by the escrow, not against this machine's clock.
135
+ const now = await readChainNow(read, nowSeconds);
136
+
137
+ let proof: CheckpointProof | null = null;
138
+ let proofUnavailable: string | null = null;
139
+ try {
140
+ proof = await read.checkpointProof(streamId);
141
+ } catch (error) {
142
+ proofUnavailable = describeError(error);
143
+ }
144
+
145
+ let claimable: bigint | null = null;
146
+ let reclaimable: bigint | null = null;
147
+ if (proof !== null) {
148
+ [claimable, reclaimable] = await Promise.all([
149
+ read.claimableForV2(streamId, proof.cumulativeAmount),
150
+ read.reclaimableForV2(streamId, proof.cumulativeAmount),
151
+ ]);
152
+ }
153
+
154
+ let protocolFeeBps: number | null = null;
155
+ try {
156
+ protocolFeeBps = await read.protocolFeeBps();
157
+ } catch {
158
+ protocolFeeBps = null;
159
+ }
160
+
161
+ let escapeHatch: EscapeHatchWindow | null = null;
162
+ try {
163
+ escapeHatch = escapeHatchWindow(
164
+ stream,
165
+ await read.escapeGraceSeconds(),
166
+ now.seconds,
167
+ );
168
+ } catch {
169
+ escapeHatch = null;
170
+ }
171
+
172
+ let latestCheckpointCheckedAt: number | null = null;
173
+ try {
174
+ latestCheckpointCheckedAt = await read.latestCheckpointCheckedAt();
175
+ } catch {
176
+ latestCheckpointCheckedAt = null;
177
+ }
178
+
179
+ const settlement = settlementCoverage(
180
+ stream,
181
+ latestCheckpointCheckedAt,
182
+ now.seconds,
183
+ );
184
+ const attesters = await readAttesterSet(read);
185
+
186
+ return {
187
+ streamId,
188
+ stream,
189
+ proof,
190
+ proofUnavailable,
191
+ claimable,
192
+ reclaimable,
193
+ protocolFeeBps,
194
+ escapeHatch,
195
+ latestCheckpointCheckedAt,
196
+ nowSeconds: now.seconds,
197
+ clockSource: now.source,
198
+ settlement,
199
+ attesters,
200
+ };
201
+ }
202
+
203
+ export function streamReportJson(report: StreamReport): unknown {
204
+ const { stream, proof } = report;
205
+ return {
206
+ streamId: report.streamId,
207
+ // StreamEscrowV2 has exactly two states. There is no "paused".
208
+ status: stream.status,
209
+ reclaimed: stream.reclaimed,
210
+ buyer: stream.buyer,
211
+ operator: stream.operator,
212
+ serviceRef: stream.serviceRef,
213
+ deposit: stream.deposit,
214
+ ratePerSecond: stream.ratePerSecond,
215
+ claimedCumulative: stream.claimedCumulative,
216
+ settledCumulative: stream.settledCumulative,
217
+ feesPaid: stream.feesPaid,
218
+ openedAt: stream.openedAt,
219
+ expiresAt: stream.expiresAt,
220
+ closedAt: stream.closedAt,
221
+ protocolFeeBps: report.protocolFeeBps,
222
+ verifiedAccrual:
223
+ proof === null
224
+ ? null
225
+ : {
226
+ cumulativeAmount: proof.cumulativeAmount,
227
+ checkpointId: proof.checkpointId,
228
+ checkedAt: proof.checkedAt,
229
+ root: proof.root,
230
+ },
231
+ verifiedAccrualUnavailable: report.proofUnavailable,
232
+ // `null` while settlement is pending. A consumer that reads this field gets
233
+ // either an amount that can actually be obtained or nothing — never a
234
+ // settled-looking number derived from a checkpoint that predates the
235
+ // stream's stop time. The superseded figure is still available, explicitly
236
+ // labelled, under `settlement.provisionalReclaimable`.
237
+ claimable: report.claimable,
238
+ reclaimable: report.settlement.pending ? null : report.reclaimable,
239
+ settlement: {
240
+ pending: report.settlement.pending,
241
+ stopAt: report.settlement.stopAt,
242
+ coveredUpTo: report.settlement.coveredUpTo,
243
+ provisionalReclaimable: report.settlement.pending
244
+ ? report.reclaimable
245
+ : null,
246
+ provisionalClaimable: report.settlement.pending ? report.claimable : null,
247
+ },
248
+ // Who attests. `operatorIsOracleSigner: null` means the signer set could
249
+ // not be read — UNKNOWN, never "no overlap".
250
+ oracleSigners: report.attesters.signers,
251
+ oracleSignersUnavailable: report.attesters.unavailable,
252
+ operatorIsOracleSigner: operatorAttestsOwnDelivery(
253
+ report.attesters,
254
+ stream.operator,
255
+ ),
256
+ latestCheckpointCheckedAt: report.latestCheckpointCheckedAt,
257
+ escapeHatch:
258
+ report.escapeHatch === null
259
+ ? null
260
+ : {
261
+ escapeGraceSeconds: report.escapeHatch.graceSeconds,
262
+ readyAt: report.escapeHatch.readyAt,
263
+ ready: report.escapeHatch.ready,
264
+ secondsUntilReady: report.escapeHatch.secondsRemaining,
265
+ },
266
+ };
267
+ }
268
+
269
+ export function streamReportRows(
270
+ report: StreamReport,
271
+ ): (readonly [string, string])[] {
272
+ const { stream, proof } = report;
273
+ const rows: (readonly [string, string])[] = [
274
+ [
275
+ "status",
276
+ stream.reclaimed ? `${stream.status} (reclaimed)` : stream.status,
277
+ ],
278
+ ["buyer", stream.buyer],
279
+ ["operator", stream.operator],
280
+ ["serviceRef", stream.serviceRef],
281
+ ["deposit", formatUsdc(stream.deposit)],
282
+ ["rate", `${formatUsdc(stream.ratePerSecond)} / second`],
283
+ ["opened", formatTimestamp(stream.openedAt)],
284
+ ["expires", describeDeadline(stream.expiresAt, report.nowSeconds)],
285
+ ];
286
+ if (stream.closedAt > 0) {
287
+ rows.push(["closed", formatTimestamp(stream.closedAt)]);
288
+ }
289
+ rows.push([
290
+ "verified accrual",
291
+ proof === null
292
+ ? `not checkpointed yet (${report.proofUnavailable ?? "no proof"})`
293
+ : `${formatUsdc(proof.cumulativeAmount)} @ checkpoint ${proof.checkpointId} (${formatTimestamp(Number(proof.checkedAt))})`,
294
+ ]);
295
+ // `claim` accepts ANY retained root, so the operator's balance is obtainable
296
+ // now — it is merely not FINAL until the covering checkpoint lands, and it
297
+ // can only grow. `reclaim` requires the LATEST root to reach the stop time,
298
+ // so the buyer's figure is not obtainable at all while that is pending: it is
299
+ // withheld rather than printed as money the buyer does not have.
300
+ rows.push([
301
+ "operator claimable",
302
+ report.claimable === null
303
+ ? "-"
304
+ : report.settlement.pending
305
+ ? `${formatUsdc(report.claimable)} so far — not final until the checkpoint covering the stop time lands`
306
+ : formatUsdc(report.claimable),
307
+ ]);
308
+ rows.push([
309
+ "buyer reclaimable",
310
+ report.settlement.pending
311
+ ? describePendingSettlement(report.settlement)
312
+ : report.reclaimable === null
313
+ ? "-"
314
+ : formatUsdc(report.reclaimable),
315
+ ]);
316
+ rows.push(["fees paid", formatUsdc(stream.feesPaid)]);
317
+ rows.push([
318
+ "oracle checkpoint",
319
+ report.latestCheckpointCheckedAt === null
320
+ ? "-"
321
+ : report.latestCheckpointCheckedAt <= 0
322
+ ? "none posted yet"
323
+ : `latest covers up to ${formatTimestamp(report.latestCheckpointCheckedAt)}`,
324
+ ]);
325
+ // The no-proof path. Printed for every stream, not only blocked ones: a buyer
326
+ // should learn that this exists before they need it.
327
+ rows.push([
328
+ "escape hatch",
329
+ report.escapeHatch === null
330
+ ? "-"
331
+ : `${describeEscapeHatch(report.escapeHatch)} — no-proof reclaim, forfeits the operator's unclaimed earnings`,
332
+ ]);
333
+ // The same row, read from the seller's side. `reclaimUnverified` pays the
334
+ // buyer the WHOLE remaining deposit with no proof, so an operator who has
335
+ // earned but not claimed loses it the moment that window opens. The window is
336
+ // `expiresAt + escapeGraceSeconds` and the grace is immutable on the live
337
+ // escrow, so the only thing an operator can do about it is claim in time —
338
+ // which they cannot do if nothing tells them the deadline exists.
339
+ if (
340
+ report.escapeHatch !== null &&
341
+ report.claimable !== null &&
342
+ report.claimable > 0n
343
+ ) {
344
+ rows.push([
345
+ "operator deadline",
346
+ report.escapeHatch.ready
347
+ ? `${formatUsdc(report.claimable)} is unclaimed and the buyer's no-proof reclaim is ALREADY open — it would take this amount from the operator`
348
+ : `${formatUsdc(report.claimable)} is unclaimed; \`metrik claim\` it before ${formatTimestamp(report.escapeHatch.readyAt)}, after which a buyer's no-proof reclaim can take it`,
349
+ ]);
350
+ }
351
+ if (report.protocolFeeBps !== null) {
352
+ rows.push([
353
+ "protocol fee",
354
+ `${report.protocolFeeBps} bps (read from the escrow)`,
355
+ ]);
356
+ }
357
+ return rows;
358
+ }
359
+
360
+ /**
361
+ * What is being waited for, in the place a number used to be. Names the two
362
+ * timestamps whose ordering decides it, so the reader can see the wait end.
363
+ */
364
+ function describePendingSettlement(settlement: SettlementCoverage): string {
365
+ const stopAt =
366
+ settlement.stopAt === null ? "unknown" : formatTimestamp(settlement.stopAt);
367
+ const covered =
368
+ settlement.coveredUpTo === null
369
+ ? "unknown (the escrow would not answer)"
370
+ : formatTimestamp(settlement.coveredUpTo);
371
+ return `pending final checkpoint — the stream stopped at ${stopAt}, the oracle has published only to ${covered}. Reclaim reverts until a checkpoint reaches the stop time (~15-22s apart).`;
372
+ }
373
+
374
+ function describeDeadline(expiresAt: number, nowSeconds: number): string {
375
+ const remaining = expiresAt - nowSeconds;
376
+ const suffix =
377
+ remaining > 0
378
+ ? `in ${formatDuration(remaining)}`
379
+ : `${formatDuration(-remaining)} ago`;
380
+ return `${formatTimestamp(expiresAt)} — ${suffix}`;
381
+ }
@@ -0,0 +1,64 @@
1
+ import { CliError } from "./errors.js";
2
+
3
+ /**
4
+ * How long any single READ is allowed to take before the CLI gives up on it.
5
+ *
6
+ * A live acceptance run found `status` calls that ran past two minutes with no
7
+ * output, and a `close` that took 8s once and 142s the next time. An unbounded
8
+ * wait is indistinguishable from a wedged command, and the only honest way to
9
+ * tell them apart is to stop waiting and say so.
10
+ *
11
+ * Deliberately generous: Base Sepolia's public endpoint is load-balanced and
12
+ * occasionally slow, and a timeout that fires on a merely-slow node would turn
13
+ * a working command into a spurious failure.
14
+ */
15
+ export const READ_TIMEOUT_MS = 20_000;
16
+
17
+ /**
18
+ * Per-RPC-call budget handed to viem's HTTP transport, with a bounded retry
19
+ * count so one slow endpoint cannot multiply into minutes of silence. viem
20
+ * retries with exponential backoff, so `retryCount` is a multiplier on this.
21
+ */
22
+ export const RPC_TIMEOUT_MS = 15_000;
23
+ export const RPC_RETRY_COUNT = 2;
24
+
25
+ /**
26
+ * Bound a read.
27
+ *
28
+ * ONLY safe for reads. A broadcast must never be abandoned on a timer: the
29
+ * transaction may already be in the mempool, and a CLI that walked away from it
30
+ * would report a failure for something that in fact settled. Write paths keep
31
+ * waiting, and the heartbeat (`./progress.ts`) is what makes that wait legible.
32
+ */
33
+ export async function withReadTimeout<T>(
34
+ operation: () => Promise<T>,
35
+ label: string,
36
+ timeoutMs: number = READ_TIMEOUT_MS,
37
+ ): Promise<T> {
38
+ let timer: ReturnType<typeof setTimeout> | undefined;
39
+ const pending = operation();
40
+ // Attached before the race so an eventual rejection of the abandoned promise
41
+ // is never an unhandled rejection that takes the process down.
42
+ pending.catch(() => {});
43
+ try {
44
+ return await Promise.race([
45
+ pending,
46
+ new Promise<never>((_resolve, reject) => {
47
+ timer = setTimeout(() => {
48
+ reject(
49
+ new CliError(
50
+ `${label} did not answer within ${Math.round(timeoutMs / 1000)}s`,
51
+ {
52
+ exitCode: 4,
53
+ hint: "the endpoint is unreachable or very slow — retry, or point --rpc-url / --oracle-url at a different one.",
54
+ },
55
+ ),
56
+ );
57
+ }, timeoutMs);
58
+ timer.unref?.();
59
+ }),
60
+ ]);
61
+ } finally {
62
+ if (timer !== undefined) clearTimeout(timer);
63
+ }
64
+ }
@@ -0,0 +1,21 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ /**
4
+ * The published version of this package, read from its own manifest.
5
+ *
6
+ * Kept in its own module because both the runner (`--version`) and the MCP
7
+ * config printer need it, and the printer is a command the runner imports — so
8
+ * reading it from `run.ts` would be a cycle.
9
+ *
10
+ * Falls back rather than throwing: a CLI that cannot state its version should
11
+ * still run.
12
+ */
13
+ export function packageVersion(): string {
14
+ try {
15
+ const require = createRequire(import.meta.url);
16
+ const manifest = require("../../package.json") as { version?: string };
17
+ return manifest.version ?? "0.0.0";
18
+ } catch {
19
+ return "0.0.0";
20
+ }
21
+ }