@absol-labs/agent 0.9.1 → 0.10.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 +77 -3
- package/dist/cli/amounts.d.ts +26 -0
- package/dist/cli/amounts.d.ts.map +1 -0
- package/dist/cli/amounts.js +71 -0
- package/dist/cli/amounts.js.map +1 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +119 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/attesters.d.ts +51 -0
- package/dist/cli/attesters.d.ts.map +1 -0
- package/dist/cli/attesters.js +58 -0
- package/dist/cli/attesters.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +13 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +86 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +3 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.js +43 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +183 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/invoke.d.ts +3 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -0
- package/dist/cli/commands/invoke.js +141 -0
- package/dist/cli/commands/invoke.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +113 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/open.d.ts +3 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +279 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
- package/dist/cli/commands/reclaim-unverified.js +164 -0
- package/dist/cli/commands/reclaim-unverified.js.map +1 -0
- package/dist/cli/commands/reclaim.d.ts +33 -0
- package/dist/cli/commands/reclaim.d.ts.map +1 -0
- package/dist/cli/commands/reclaim.js +297 -0
- package/dist/cli/commands/reclaim.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +43 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/streams.d.ts +3 -0
- package/dist/cli/commands/streams.d.ts.map +1 -0
- package/dist/cli/commands/streams.js +257 -0
- package/dist/cli/commands/streams.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +55 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/config.d.ts +61 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +140 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +29 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/deps.d.ts +145 -0
- package/dist/cli/deps.d.ts.map +1 -0
- package/dist/cli/deps.js +227 -0
- package/dist/cli/deps.js.map +1 -0
- package/dist/cli/descriptor.d.ts +61 -0
- package/dist/cli/descriptor.d.ts.map +1 -0
- package/dist/cli/descriptor.js +78 -0
- package/dist/cli/descriptor.js.map +1 -0
- package/dist/cli/errors.d.ts +48 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +129 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/escape-hatch.d.ts +50 -0
- package/dist/cli/escape-hatch.d.ts.map +1 -0
- package/dist/cli/escape-hatch.js +45 -0
- package/dist/cli/escape-hatch.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/listings.d.ts +15 -0
- package/dist/cli/listings.d.ts.map +1 -0
- package/dist/cli/listings.js +56 -0
- package/dist/cli/listings.js.map +1 -0
- package/dist/cli/mandate.d.ts +66 -0
- package/dist/cli/mandate.d.ts.map +1 -0
- package/dist/cli/mandate.js +61 -0
- package/dist/cli/mandate.js.map +1 -0
- package/dist/cli/output.d.ts +33 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +34 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/preflight.d.ts +38 -0
- package/dist/cli/preflight.d.ts.map +1 -0
- package/dist/cli/preflight.js +72 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/progress.d.ts +36 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +40 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/run.d.ts +25 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +173 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/scan-range.d.ts +55 -0
- package/dist/cli/scan-range.d.ts.map +1 -0
- package/dist/cli/scan-range.js +57 -0
- package/dist/cli/scan-range.js.map +1 -0
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +252 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/timeout.d.ts +30 -0
- package/dist/cli/timeout.d.ts.map +1 -0
- package/dist/cli/timeout.js +55 -0
- package/dist/cli/timeout.js.map +1 -0
- package/dist/cli/version.d.ts +12 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +22 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/wallet.d.ts +95 -0
- package/dist/cli/wallet.d.ts.map +1 -0
- package/dist/cli/wallet.js +169 -0
- package/dist/cli/wallet.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +10 -0
- package/dist/frameworks/crewai.d.ts.map +1 -1
- package/dist/frameworks/crewai.js +12 -2
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/server-entry.d.ts +1 -0
- package/dist/gateway/server-entry.js +1 -0
- package/dist/gateway/server-entry.js.map +1 -1
- package/dist/mcp/http-server.d.ts +1 -0
- package/dist/mcp/http-server.js +1 -0
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +20 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/sdk/client.d.ts +25 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +20 -1
- package/dist/sdk/client.js.map +1 -1
- package/docs/agent-layer.md +2 -1
- package/docs/cli.md +569 -0
- package/docs/crewai.md +6 -2
- package/docs/eliza.md +6 -1
- package/docs/mcp-hosted.md +3 -2
- package/docs/quickstart.md +18 -5
- package/package.json +17 -5
- package/src/cli/amounts.ts +102 -0
- package/src/cli/args.ts +150 -0
- package/src/cli/attesters.ts +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/commands/claim.ts +100 -0
- package/src/cli/commands/close.ts +53 -0
- package/src/cli/commands/discover.ts +228 -0
- package/src/cli/commands/invoke.ts +177 -0
- package/src/cli/commands/mcp.ts +135 -0
- package/src/cli/commands/open.ts +331 -0
- package/src/cli/commands/reclaim-unverified.ts +206 -0
- package/src/cli/commands/reclaim.ts +360 -0
- package/src/cli/commands/status.ts +56 -0
- package/src/cli/commands/streams.ts +344 -0
- package/src/cli/commands/watch.ts +70 -0
- package/src/cli/config.ts +208 -0
- package/src/cli/context.ts +30 -0
- package/src/cli/deps.ts +437 -0
- package/src/cli/descriptor.ts +133 -0
- package/src/cli/errors.ts +162 -0
- package/src/cli/escape-hatch.ts +92 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/listings.ts +77 -0
- package/src/cli/mandate.ts +126 -0
- package/src/cli/output.ts +63 -0
- package/src/cli/preflight.ts +88 -0
- package/src/cli/progress.ts +75 -0
- package/src/cli/run.ts +209 -0
- package/src/cli/scan-range.ts +76 -0
- package/src/cli/status.ts +381 -0
- package/src/cli/timeout.ts +64 -0
- package/src/cli/version.ts +21 -0
- package/src/cli/wallet.ts +241 -0
- package/src/frameworks/crewai.ts +13 -2
- package/src/gateway/server-entry.ts +1 -0
- package/src/mcp/http-server.ts +1 -0
- package/src/mcp/server.ts +25 -1
- package/src/mcp/stdio.ts +1 -0
- package/src/sdk/client.ts +33 -1
|
@@ -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,177 @@
|
|
|
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
|
+
});
|
|
65
|
+
const read = deps.createReadClient(config);
|
|
66
|
+
// No transaction is signed here — but the capability is bound to
|
|
67
|
+
// {chainId, escrow}, so a wrong-chain RPC would mint a capability the
|
|
68
|
+
// gateway can never accept. Fail before the network call, not after.
|
|
69
|
+
await assertTestnetRpc(config, () => read.getChainId());
|
|
70
|
+
|
|
71
|
+
const stream = await read.getStreamV2(streamId);
|
|
72
|
+
const listing =
|
|
73
|
+
baseUrlOverride === undefined
|
|
74
|
+
? await resolveListing(context, stream.serviceRef, { required: true })
|
|
75
|
+
: null;
|
|
76
|
+
if (baseUrlOverride !== undefined) {
|
|
77
|
+
out.note(
|
|
78
|
+
"warning: --base-url bypasses verified listing routing; the origin is not signature-checked.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let result;
|
|
83
|
+
try {
|
|
84
|
+
result = await deps.invoke(
|
|
85
|
+
streamId,
|
|
86
|
+
{
|
|
87
|
+
method,
|
|
88
|
+
path,
|
|
89
|
+
headers,
|
|
90
|
+
...(body === undefined ? {} : { body }),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
streamReader: deps.createInvokeStreamReader(config),
|
|
94
|
+
buyer: account,
|
|
95
|
+
domain: { chainId: config.chainId, verifyingContract: config.escrow },
|
|
96
|
+
...(listing === null ? {} : { listing }),
|
|
97
|
+
...(baseUrlOverride === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { serviceBaseUrl: baseUrlOverride }),
|
|
100
|
+
...(ttlSeconds === undefined ? {} : { ttlSeconds }),
|
|
101
|
+
nowSeconds: deps.nowSeconds(),
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw explainInvokeFailure(error, account.address);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const responseBody = await result.response.text();
|
|
109
|
+
out.result({
|
|
110
|
+
streamId,
|
|
111
|
+
method,
|
|
112
|
+
path,
|
|
113
|
+
status: result.response.status,
|
|
114
|
+
ok: result.response.ok,
|
|
115
|
+
capabilityNonce: result.capability.nonce,
|
|
116
|
+
capabilityExpiry: result.capability.expiry,
|
|
117
|
+
headers: Object.fromEntries(result.response.headers.entries()),
|
|
118
|
+
body: responseBody,
|
|
119
|
+
});
|
|
120
|
+
out.print(`${method} ${path} -> ${result.response.status}`);
|
|
121
|
+
out.print("");
|
|
122
|
+
out.print(responseBody);
|
|
123
|
+
|
|
124
|
+
if (!result.response.ok) {
|
|
125
|
+
throw new CliError(`the service returned ${result.response.status}`, {
|
|
126
|
+
exitCode: 4,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function explainInvokeFailure(error: unknown, signer: string): unknown {
|
|
133
|
+
if (error instanceof InvokeBuyerMismatchError) {
|
|
134
|
+
return new CliError(
|
|
135
|
+
"this stream does not belong to the configured wallet",
|
|
136
|
+
{
|
|
137
|
+
exitCode: 3,
|
|
138
|
+
hint: `the capability is signed by ${signer}; only the stream's buyer can invoke it.`,
|
|
139
|
+
cause: error,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (error instanceof InvokeStreamNotActiveError) {
|
|
144
|
+
return new CliError("the stream is closed, so it authorizes nothing", {
|
|
145
|
+
exitCode: 4,
|
|
146
|
+
cause: error,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (error instanceof InvokeStreamExpiredError) {
|
|
150
|
+
return new CliError("the stream has expired, so it authorizes nothing", {
|
|
151
|
+
exitCode: 4,
|
|
152
|
+
hint: "reclaim the unspent budget with `metrik reclaim <streamId>`.",
|
|
153
|
+
cause: error,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseHeaders(raw: readonly string[]): Record<string, string> {
|
|
160
|
+
const headers: Record<string, string> = {};
|
|
161
|
+
for (const entry of raw) {
|
|
162
|
+
const separator = entry.indexOf(":");
|
|
163
|
+
if (separator <= 0) {
|
|
164
|
+
throw usageError(`--header must be "Name: value" (got "${entry}")`);
|
|
165
|
+
}
|
|
166
|
+
headers[entry.slice(0, separator).trim().toLowerCase()] = entry
|
|
167
|
+
.slice(separator + 1)
|
|
168
|
+
.trim();
|
|
169
|
+
}
|
|
170
|
+
return headers;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
174
|
+
return Object.keys(headers).some(
|
|
175
|
+
(key) => key.toLowerCase() === name.toLowerCase(),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -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
|
+
}
|