@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,228 @@
1
+ import { RegistryUnavailableError } from "../../discovery/registry.js";
2
+ import { integerFlag, stringFlag } from "../args.js";
3
+ import {
4
+ attesterDisclosure,
5
+ attesterSetUnavailableWarning,
6
+ operatorAttestsOwnDelivery,
7
+ readAttesterSet,
8
+ type AttesterSet,
9
+ } from "../attesters.js";
10
+ import { listingDetail, orderListings } from "../descriptor.js";
11
+ import { CliError } from "../errors.js";
12
+ import type { CommandContext, CommandSpec } from "../context.js";
13
+
14
+ const TIERS = new Set([0, 1, 2]);
15
+
16
+ export const discoverCommand: CommandSpec = {
17
+ name: "discover",
18
+ summary: "List cryptographically verified services in the Metrik registry",
19
+ usage:
20
+ "metrik discover [--min-tier 0|1|2] [--category NAME] [--operator 0x…] [--limit N]",
21
+ flags: ["min-tier", "category", "limit", "operator"],
22
+ signs: false,
23
+ async run(context: CommandContext): Promise<void> {
24
+ const { args, config, out, deps } = context;
25
+ const minTier = integerFlag(args, "min-tier");
26
+ if (minTier !== undefined && !TIERS.has(minTier)) {
27
+ throw new CliError(`--min-tier must be 0, 1 or 2 (got ${minTier})`, {
28
+ exitCode: 2,
29
+ });
30
+ }
31
+ const limit = integerFlag(args, "limit");
32
+ const category = stringFlag(args, "category");
33
+ const operator = stringFlag(args, "operator");
34
+
35
+ let result;
36
+ try {
37
+ result = await deps.discover({
38
+ registryUrl: config.registryUrl,
39
+ ...(category === undefined ? {} : { category }),
40
+ logger: { warn: (...parts) => out.note(parts.map(String).join(" ")) },
41
+ });
42
+ } catch (error) {
43
+ if (error instanceof RegistryUnavailableError) {
44
+ // Deliberately NOT an empty list: "the registry was unreachable" and
45
+ // "the marketplace is empty" are different facts and must not be
46
+ // rendered the same way.
47
+ throw new CliError(
48
+ `the service registry at ${config.registryUrl} could not be read`,
49
+ {
50
+ exitCode: 4,
51
+ hint: "this is a registry outage, not an empty marketplace — no listing was dropped by it.",
52
+ cause: error,
53
+ },
54
+ );
55
+ }
56
+ throw error;
57
+ }
58
+
59
+ let services = result.services.filter(
60
+ (service) => minTier === undefined || service.tier >= minTier,
61
+ );
62
+ if (operator !== undefined) {
63
+ const wanted = operator.toLowerCase();
64
+ services = services.filter(
65
+ (service) => service.operator.toLowerCase() === wanted,
66
+ );
67
+ }
68
+ // Strongest-declared-verification first, then serviceRef. The registry's own
69
+ // order is an implementation detail of whatever inserted the rows, so
70
+ // `--limit 1` used to hand a newcomer whichever listing happened to be first
71
+ // — including a listing that exists precisely to fail its canaries.
72
+ services = orderListings(services);
73
+ if (limit !== undefined) services = services.slice(0, limit);
74
+
75
+ // Who attests to the delivery these listings are "verified" for. `discover`
76
+ // advertises tiers and "cryptographically verified", so it is the command
77
+ // that owes the buyer the composition of the attesting set. Read soft: a
78
+ // registry listing is still worth printing when the escrow will not answer,
79
+ // and the unknown is reported as unknown.
80
+ const attesters: AttesterSet =
81
+ services.length === 0
82
+ ? { signers: null, unavailable: "no listing to check" }
83
+ : await readAttesterSet(deps.createReadClient(config));
84
+
85
+ out.result({
86
+ registryUrl: config.registryUrl,
87
+ registryAvailable: result.registryAvailable,
88
+ source: result.source,
89
+ count: services.length,
90
+ oracleSigners: attesters.signers,
91
+ oracleSignersUnavailable: attesters.unavailable,
92
+ services: services.map((service) => {
93
+ // Everything below comes from the signed record whose signature was
94
+ // already recovered and matched — never from an unsigned column.
95
+ const detail = listingDetail(service.signed);
96
+ return {
97
+ serviceRef: service.serviceRef,
98
+ operator: service.operator,
99
+ name: service.name,
100
+ category: service.category,
101
+ tier: service.tier,
102
+ access: service.access,
103
+ publicUrl: service.publicUrl,
104
+ accessUrl: service.accessUrl,
105
+ verification: service.verification,
106
+ authModel: detail.authModel,
107
+ endpoints: detail.endpoints,
108
+ example: detail.example,
109
+ rates: detail.rates,
110
+ // `true` = the party being paid is also one of the parties attesting
111
+ // to delivery. `null` = the signer set could not be read, which is
112
+ // UNKNOWN and must not be treated as `false`.
113
+ operatorIsOracleSigner: operatorAttestsOwnDelivery(
114
+ attesters,
115
+ service.operator,
116
+ ),
117
+ // The operator's own signed words. Metrik neither verifies nor
118
+ // enforces them — see the note printed under the listings.
119
+ slaClaim: detail.rates?.slaDescription ?? null,
120
+ slaClaimVerified: false,
121
+ };
122
+ }),
123
+ });
124
+
125
+ if (services.length === 0) {
126
+ out.print(
127
+ `No listings matched (registry served ${result.services.length}, filters removed the rest).`,
128
+ );
129
+ return;
130
+ }
131
+
132
+ out.print(
133
+ `${services.length} verified listing${services.length === 1 ? "" : "s"} from ${config.registryUrl}, strongest declared verification first`,
134
+ );
135
+ for (const service of services) {
136
+ const detail = listingDetail(service.signed);
137
+ out.print("");
138
+ out.print(`${service.name ?? "(unnamed)"} [tier ${service.tier}]`);
139
+ out.table([
140
+ ["serviceRef", service.serviceRef],
141
+ ["operator", service.operator],
142
+ ["access", service.access],
143
+ ["url", service.accessUrl ?? service.publicUrl],
144
+ ["category", service.category ?? "-"],
145
+ ...(detail.rates === null
146
+ ? []
147
+ : ([
148
+ [
149
+ "rate band",
150
+ describeRateBand(
151
+ detail.rates.minRatePerSecond,
152
+ detail.rates.maxRatePerSecond,
153
+ ),
154
+ ],
155
+ ] as const)),
156
+ ]);
157
+ if (detail.endpoints.length > 0) {
158
+ out.print(" endpoints");
159
+ out.table(
160
+ detail.endpoints.map(
161
+ (endpoint) =>
162
+ [
163
+ ` ${endpoint.method} ${endpoint.path}`,
164
+ endpoint.description ?? endpoint.name,
165
+ ] as const,
166
+ ),
167
+ );
168
+ }
169
+ if (detail.example !== null) {
170
+ // The operator's own signed words about this service. It is also how a
171
+ // listing that exists to FAIL announces itself.
172
+ if (detail.example.description !== null) {
173
+ out.print(` example ${detail.example.description}`);
174
+ }
175
+ if (detail.example.request !== null) {
176
+ out.print(
177
+ ` request ${detail.example.endpoint} ${JSON.stringify(detail.example.request)}`,
178
+ );
179
+ }
180
+ if (detail.example.response !== null) {
181
+ out.print(` response ${JSON.stringify(detail.example.response)}`);
182
+ }
183
+ }
184
+ if (detail.rates?.slaDescription != null) {
185
+ // Labelled at the point of display, not only in a footnote. One live
186
+ // listing claims "two consecutive failed intervals auto-pause the
187
+ // stream"; StreamEscrowV2 has no paused state at all, so that claim is
188
+ // false and nothing in the protocol makes it true.
189
+ out.print(
190
+ ` sla ${detail.rates.slaDescription} [operator's own claim - NOT verified or enforced by Metrik]`,
191
+ );
192
+ }
193
+ // Only an actual overlap is reported per listing. The "could not read the
194
+ // signer set" case is one fact about the escrow, not one per row, and is
195
+ // printed once below.
196
+ if (operatorAttestsOwnDelivery(attesters, service.operator) === true) {
197
+ out.note(
198
+ `${service.serviceRef}: ${attesterDisclosure(attesters, service.operator) ?? ""}`,
199
+ );
200
+ }
201
+ }
202
+ const setUnavailable = attesterSetUnavailableWarning(attesters);
203
+ if (setUnavailable !== null) out.note(setUnavailable);
204
+ out.print("");
205
+ out.print(
206
+ "SLA text above is the operator's own signed marketing copy. Metrik verifies DELIVERY, not any promise made about it: the escrow has no paused state, nothing auto-refunds, and no SLA string changes what accrues.",
207
+ );
208
+ out.print(
209
+ "Hire one: metrik open --service <serviceRef> --rate <usdc/s> --budget <usdc> --duration <seconds>",
210
+ );
211
+ out.print(
212
+ "The rate band above is in USDC BASE UNITS per second — pass it with `--units atomic`.",
213
+ );
214
+ },
215
+ };
216
+
217
+ /**
218
+ * The operator's accepted `ratePerSecond` window, in USDC base units per second
219
+ * exactly as signed. Rendered as integers, never converted to decimal USDC: a
220
+ * band of 100-10000 base units is 0.0001-0.01 USDC, and rounding it for display
221
+ * is how someone opens a stream the operator will not accept.
222
+ */
223
+ function describeRateBand(min: string | null, max: string | null): string {
224
+ if (min === null && max === null) return "not declared";
225
+ if (min === null) return `up to ${max} base units / second`;
226
+ if (max === null) return `from ${min} base units / second`;
227
+ return `${min} - ${max} base units / second (atomic)`;
228
+ }
@@ -0,0 +1,178 @@
1
+ import {
2
+ httpMethodSchema,
3
+ type HttpMethod,
4
+ } from "../../capability/invocation-capability.js";
5
+ import {
6
+ InvokeBuyerMismatchError,
7
+ InvokeStreamExpiredError,
8
+ InvokeStreamNotActiveError,
9
+ } from "../../sdk/invoke.js";
10
+ import { integerFlag, stringFlag, stringFlags } from "../args.js";
11
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
12
+ import { CliError, usageError } from "../errors.js";
13
+ import { resolveListing } from "../listings.js";
14
+ import { resolveCliWallet } from "../wallet.js";
15
+ import type { CommandContext, CommandSpec } from "../context.js";
16
+
17
+ export const invokeCommand: CommandSpec = {
18
+ name: "invoke",
19
+ summary:
20
+ "Call the service you are paying for, authorized by the stream itself",
21
+ usage:
22
+ 'metrik invoke <streamId> [--path / (default)] [--method GET] [--body \'{"text":"hi"}\'] [--header "K: V"] [--ttl 60 (default)] [--base-url URL]',
23
+ flags: ["path", "method", "body", "header", "ttl", "base-url"],
24
+ // Broadcasts nothing. It DOES need the key: the invocation capability is an
25
+ // off-chain EIP-712 signature by the stream's buyer.
26
+ signs: false,
27
+ requiresKey: true,
28
+ async run(context: CommandContext): Promise<void> {
29
+ const { args, config, env, out, deps } = context;
30
+ const [streamIdArg] = args.positionals;
31
+ if (streamIdArg === undefined) {
32
+ throw usageError("metrik invoke requires a <streamId>");
33
+ }
34
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
35
+ const path = stringFlag(args, "path") ?? "/";
36
+ if (!path.startsWith("/") || path.startsWith("//")) {
37
+ throw usageError(
38
+ `--path must be origin-relative and start with a single slash (got "${path}")`,
39
+ );
40
+ }
41
+ const methodResult = httpMethodSchema.safeParse(
42
+ (stringFlag(args, "method") ?? "GET").toUpperCase(),
43
+ );
44
+ if (!methodResult.success) {
45
+ throw usageError(
46
+ `--method must be one of the HTTP methods a capability can authorize (got "${stringFlag(args, "method")}")`,
47
+ );
48
+ }
49
+ const method: HttpMethod = methodResult.data;
50
+ const body = stringFlag(args, "body");
51
+ // Lifetime of the signed capability. Left unset, the capability signer's own
52
+ // default applies (see `DEFAULT_CAPABILITY_TTL_SECONDS`).
53
+ const ttlSeconds = integerFlag(args, "ttl");
54
+ const baseUrlOverride = stringFlag(args, "base-url");
55
+
56
+ const headers = parseHeaders(stringFlags(args, "header"));
57
+ if (body !== undefined && !hasHeader(headers, "content-type")) {
58
+ headers["content-type"] = "application/json";
59
+ }
60
+
61
+ const { account } = await resolveCliWallet(env, {
62
+ purpose: "capability",
63
+ note: out.note,
64
+ rpcUrl: config.rpcUrl,
65
+ });
66
+ const read = deps.createReadClient(config);
67
+ // No transaction is signed here — but the capability is bound to
68
+ // {chainId, escrow}, so a wrong-chain RPC would mint a capability the
69
+ // gateway can never accept. Fail before the network call, not after.
70
+ await assertTestnetRpc(config, () => read.getChainId());
71
+
72
+ const stream = await read.getStreamV2(streamId);
73
+ const listing =
74
+ baseUrlOverride === undefined
75
+ ? await resolveListing(context, stream.serviceRef, { required: true })
76
+ : null;
77
+ if (baseUrlOverride !== undefined) {
78
+ out.note(
79
+ "warning: --base-url bypasses verified listing routing; the origin is not signature-checked.",
80
+ );
81
+ }
82
+
83
+ let result;
84
+ try {
85
+ result = await deps.invoke(
86
+ streamId,
87
+ {
88
+ method,
89
+ path,
90
+ headers,
91
+ ...(body === undefined ? {} : { body }),
92
+ },
93
+ {
94
+ streamReader: deps.createInvokeStreamReader(config),
95
+ buyer: account,
96
+ domain: { chainId: config.chainId, verifyingContract: config.escrow },
97
+ ...(listing === null ? {} : { listing }),
98
+ ...(baseUrlOverride === undefined
99
+ ? {}
100
+ : { serviceBaseUrl: baseUrlOverride }),
101
+ ...(ttlSeconds === undefined ? {} : { ttlSeconds }),
102
+ nowSeconds: deps.nowSeconds(),
103
+ },
104
+ );
105
+ } catch (error) {
106
+ throw explainInvokeFailure(error, account.address);
107
+ }
108
+
109
+ const responseBody = await result.response.text();
110
+ out.result({
111
+ streamId,
112
+ method,
113
+ path,
114
+ status: result.response.status,
115
+ ok: result.response.ok,
116
+ capabilityNonce: result.capability.nonce,
117
+ capabilityExpiry: result.capability.expiry,
118
+ headers: Object.fromEntries(result.response.headers.entries()),
119
+ body: responseBody,
120
+ });
121
+ out.print(`${method} ${path} -> ${result.response.status}`);
122
+ out.print("");
123
+ out.print(responseBody);
124
+
125
+ if (!result.response.ok) {
126
+ throw new CliError(`the service returned ${result.response.status}`, {
127
+ exitCode: 4,
128
+ });
129
+ }
130
+ },
131
+ };
132
+
133
+ function explainInvokeFailure(error: unknown, signer: string): unknown {
134
+ if (error instanceof InvokeBuyerMismatchError) {
135
+ return new CliError(
136
+ "this stream does not belong to the configured wallet",
137
+ {
138
+ exitCode: 3,
139
+ hint: `the capability is signed by ${signer}; only the stream's buyer can invoke it.`,
140
+ cause: error,
141
+ },
142
+ );
143
+ }
144
+ if (error instanceof InvokeStreamNotActiveError) {
145
+ return new CliError("the stream is closed, so it authorizes nothing", {
146
+ exitCode: 4,
147
+ cause: error,
148
+ });
149
+ }
150
+ if (error instanceof InvokeStreamExpiredError) {
151
+ return new CliError("the stream has expired, so it authorizes nothing", {
152
+ exitCode: 4,
153
+ hint: "reclaim the unspent budget with `metrik reclaim <streamId>`.",
154
+ cause: error,
155
+ });
156
+ }
157
+ return error;
158
+ }
159
+
160
+ function parseHeaders(raw: readonly string[]): Record<string, string> {
161
+ const headers: Record<string, string> = {};
162
+ for (const entry of raw) {
163
+ const separator = entry.indexOf(":");
164
+ if (separator <= 0) {
165
+ throw usageError(`--header must be "Name: value" (got "${entry}")`);
166
+ }
167
+ headers[entry.slice(0, separator).trim().toLowerCase()] = entry
168
+ .slice(separator + 1)
169
+ .trim();
170
+ }
171
+ return headers;
172
+ }
173
+
174
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
175
+ return Object.keys(headers).some(
176
+ (key) => key.toLowerCase() === name.toLowerCase(),
177
+ );
178
+ }
@@ -0,0 +1,135 @@
1
+ import { booleanFlag, stringFlag } from "../args.js";
2
+ import { packageVersion } from "../version.js";
3
+ import { CDP_ENV_VARS, PRIVATE_KEY_ENV } from "../wallet.js";
4
+ import type { CommandContext, CommandSpec } from "../context.js";
5
+
6
+ /** The published executable an MCP host starts. Declared in `package.json#bin`. */
7
+ const MCP_BIN = "metrik-mcp";
8
+ const PACKAGE_NAME = "@absol-labs/agent";
9
+
10
+ /** Default key under `mcpServers`. Overridable so two wallets can coexist. */
11
+ const DEFAULT_SERVER_NAME = "metrik";
12
+
13
+ /**
14
+ * Every `METRIK_AGENT_MANDATE_*` variable the MCP server requires, with the
15
+ * placeholder emitted when the current environment does not set it.
16
+ *
17
+ * The server refuses to start without all four (`ServerMandateConfigError`):
18
+ * there is no implicit unbounded mandate, and a printed config that omitted
19
+ * them would be a config that fails closed on first launch — which is the
20
+ * failure this command exists to prevent.
21
+ */
22
+ const MANDATE_VARS: readonly (readonly [string, string])[] = [
23
+ [
24
+ "METRIK_AGENT_MANDATE_MAX_PER_STREAM_USDC",
25
+ "<max USDC base units per stream>",
26
+ ],
27
+ ["METRIK_AGENT_MANDATE_MAX_TOTAL_USDC", "<max USDC base units in total>"],
28
+ [
29
+ "METRIK_AGENT_MANDATE_MAX_RATE_PER_SECOND_USDC",
30
+ "<max USDC base units per second>",
31
+ ],
32
+ [
33
+ "METRIK_AGENT_MANDATE_MAX_DURATION_SECONDS",
34
+ "<max stream duration in seconds>",
35
+ ],
36
+ ];
37
+
38
+ export const mcpCommand: CommandSpec = {
39
+ name: "mcp",
40
+ summary: "Print the MCP client config that runs this package's MCP server",
41
+ usage: "metrik mcp [--print-config] [--name metrik]",
42
+ flags: ["print-config", "name"],
43
+ signs: false,
44
+ async run(context: CommandContext): Promise<void> {
45
+ const { args, config, env, out } = context;
46
+ // A bare `metrik mcp` used to fail with exit 2 demanding --print-config,
47
+ // while the top-level help said this command "prints the MCP client
48
+ // config". Refusing to do the one thing the command advertises taught a
49
+ // reviewer that the tool was broken. It now does it — and says plainly, on
50
+ // stderr, that it printed a config rather than starting a server, which is
51
+ // the confusion the refusal was guarding against.
52
+ if (!booleanFlag(args, "print-config")) {
53
+ out.note(
54
+ "metrik mcp printed the MCP CLIENT CONFIG below; it did NOT start a server. The server is the separate `metrik-mcp` executable, which your MCP host launches. The explicit form of this command is `metrik mcp --print-config`.",
55
+ );
56
+ }
57
+ const name = stringFlag(args, "name") ?? DEFAULT_SERVER_NAME;
58
+
59
+ // Pinned to THIS version. `npx --package @absol-labs/agent metrik-mcp`
60
+ // would silently follow `latest` on every host restart, which is not what
61
+ // an MCP host that signs spend mandates should do.
62
+ const version = packageVersion();
63
+
64
+ const document = {
65
+ mcpServers: {
66
+ [name]: {
67
+ command: "npx",
68
+ args: ["-y", "--package", `${PACKAGE_NAME}@${version}`, MCP_BIN],
69
+ env: {
70
+ // The MCP server has NO defaults for these three — it fails closed
71
+ // when any is missing. They are filled from this CLI's resolved
72
+ // settlement config, so the printed block matches the deployment
73
+ // the CLI is already pointed at.
74
+ METRIK_AGENT_CHAIN_ID: String(config.chainId),
75
+ METRIK_AGENT_RPC_URL: config.rpcUrl,
76
+ METRIK_AGENT_ESCROW: config.escrow,
77
+ METRIK_AGENT_USDC: config.usdc,
78
+ ...walletEnv(env),
79
+ ...mandateEnv(env),
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ out.result(document);
86
+ out.print(
87
+ `Paste this into your MCP client's config (Claude Desktop: claude_desktop_config.json), then replace every <…> placeholder:`,
88
+ );
89
+ out.print("");
90
+ out.print(JSON.stringify(document, null, 2));
91
+ out.print("");
92
+ out.print(
93
+ `The host runs \`npx -y --package ${PACKAGE_NAME}@${version} ${MCP_BIN}\` — the published executable, pinned to this version. Nothing in this repository is referenced.`,
94
+ );
95
+ out.print(
96
+ "Secrets are placeholders by design: this command never reads a key out of your environment and prints it back.",
97
+ );
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Wallet variables, shaped to match how the environment running this command is
103
+ * already configured — CDP if that is what is set, a raw key otherwise.
104
+ *
105
+ * Values are ALWAYS placeholders. `metrik` prints derived addresses and never
106
+ * echoes a credential, and a config block is the single most likely thing to be
107
+ * pasted into a chat window or an issue.
108
+ */
109
+ function walletEnv(env: NodeJS.ProcessEnv): Record<string, string> {
110
+ const usesCdp =
111
+ env[PRIVATE_KEY_ENV] === undefined &&
112
+ CDP_ENV_VARS.some((variable) => (env[variable] ?? "").length > 0);
113
+ if (usesCdp) {
114
+ return Object.fromEntries(
115
+ CDP_ENV_VARS.map((variable) => [variable, `<your ${variable}>`]),
116
+ );
117
+ }
118
+ return {
119
+ [PRIVATE_KEY_ENV]: "<0x… a funded Base Sepolia test key>",
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Mandate ceilings. These are policy, not secrets, so a configured value is
125
+ * carried through verbatim — the point is a block that starts the server, and
126
+ * re-typing four caps from memory is where a wrong ceiling comes from.
127
+ */
128
+ function mandateEnv(env: NodeJS.ProcessEnv): Record<string, string> {
129
+ return Object.fromEntries(
130
+ MANDATE_VARS.map(([variable, placeholder]) => [
131
+ variable,
132
+ env[variable] ?? placeholder,
133
+ ]),
134
+ );
135
+ }