@prismnetwork/mcp 0.5.0 → 0.7.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 +6 -1
- package/package.json +2 -2
- package/server.mjs +206 -59
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
|
|
|
22
22
|
| `prism_receipts` | no |
|
|
23
23
|
| `prism_wallet` | yes |
|
|
24
24
|
| `prism_leases` | yes |
|
|
25
|
+
| `prism_infer` | yes |
|
|
25
26
|
| `prism_lease_and_run` | yes |
|
|
26
27
|
| `prism_lease` | yes |
|
|
27
28
|
| `prism_run` | yes |
|
|
@@ -39,6 +40,10 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
|
|
|
39
40
|
- `prism_receipts`: recent settled receipts from the public proof feed, with the
|
|
40
41
|
settlement transaction on Robinhood Chain.
|
|
41
42
|
- `prism_leases`: this wallet's leases and their state.
|
|
43
|
+
- `prism_infer`: buy one LLM generation from the managed inference endpoint,
|
|
44
|
+
paying the quoted USDG price from this wallet (about 0.01 USDG). Waits
|
|
45
|
+
through a cold start; an unconsumed payment is kept and reused on the next
|
|
46
|
+
call instead of paying twice.
|
|
42
47
|
- `prism_lease_and_run`: lease a GPU, run a command, return the output (one shot).
|
|
43
48
|
- `prism_lease`: lease a GPU and keep it; returns a `lease_id` and SSH access.
|
|
44
49
|
- `prism_run`: run a command on an existing lease.
|
|
@@ -60,7 +65,7 @@ derived from the agent's wallet inside this server process; Prism receives
|
|
|
60
65
|
ciphertext and holds no way to read it.
|
|
61
66
|
|
|
62
67
|
Each item names the weakest workspace trust class it may ever be released into,
|
|
63
|
-
and new items default to `confidential
|
|
68
|
+
and new items default to `confidential`, above anything the network serves
|
|
64
69
|
today. `prism_vault_release` is therefore refused on current capacity instead of
|
|
65
70
|
handing a secret to a host that can read it. Lowering an item's floor is a
|
|
66
71
|
deliberate act, and allowed releases are recorded against the account.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prismnetwork/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for leasing and running on Prism Network GPUs.",
|
|
5
5
|
"mcpName": "io.github.prismnetwork-tech/mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
-
"@prismnetwork/agent-sdk": "^0.
|
|
19
|
+
"@prismnetwork/agent-sdk": "^0.5.0",
|
|
20
20
|
"viem": "^2"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
package/server.mjs
CHANGED
|
@@ -9,59 +9,85 @@ import { DEFAULT_IMAGE, DEFAULT_TRUST_FLOOR, PrismAgent, TRUST_CLASSES } from "@
|
|
|
9
9
|
|
|
10
10
|
const IMAGE = process.env.PRISM_DEFAULT_IMAGE ?? DEFAULT_IMAGE;
|
|
11
11
|
|
|
12
|
-
function requireEnv(name) {
|
|
13
|
-
const value = process.env[name];
|
|
14
|
-
if (!value) throw new Error(`${name} is required`);
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
12
|
const PUBLIC_API = process.env.PRISM_PUBLIC_API ?? "https://api.prismnetwork.tech";
|
|
13
|
+
// The live lease escrow. Overridable, but its absence must not silently
|
|
14
|
+
// disable the wallet the way a missing key does.
|
|
15
|
+
const DEFAULT_ESCROW = "0x62C042265991bEa17B07229322A01850974626dA";
|
|
16
|
+
// Matches the limit the SDK and the control plane enforce, so a command that
|
|
17
|
+
// cannot run is rejected before an escrow is funded.
|
|
18
|
+
const MAX_COMMAND_BYTES = 8 * 1024;
|
|
19
19
|
|
|
20
20
|
// Refusing to start without a wallet meant nobody could ask what a GPU costs
|
|
21
21
|
// without first producing a private key, which is a strange thing to demand of
|
|
22
22
|
// someone deciding whether to use you at all.
|
|
23
23
|
let agent = null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
let walletProblem = "PRISM_AGENT_KEY is not set";
|
|
25
|
+
if (process.env.PRISM_AGENT_KEY) {
|
|
26
|
+
try {
|
|
27
|
+
agent = new PrismAgent({
|
|
28
|
+
privateKey: process.env.PRISM_AGENT_KEY,
|
|
29
|
+
escrow: process.env.PRISM_ESCROW ?? DEFAULT_ESCROW,
|
|
30
|
+
apiBase: process.env.PRISM_API_BASE ?? "https://prismnetwork.tech",
|
|
31
|
+
rpcUrl: process.env.PRISM_RPC_URL,
|
|
32
|
+
});
|
|
33
|
+
walletProblem = null;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
walletProblem = `PRISM_AGENT_KEY is set but unusable: ${err?.message ?? err}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!agent) {
|
|
32
39
|
console.error(
|
|
33
|
-
|
|
34
|
-
"Set PRISM_AGENT_KEY and PRISM_ESCROW to lease.",
|
|
40
|
+
`prism mcp: no wallet configured (${walletProblem}), so capacity and pricing are readable and leasing is not.`,
|
|
35
41
|
);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
function requireWallet(tool) {
|
|
44
|
+
function requireWallet(tool, reason = "spends money") {
|
|
39
45
|
if (!agent) {
|
|
40
46
|
throw new Error(
|
|
41
|
-
`${tool}
|
|
47
|
+
`${tool} ${reason} and needs a wallet. None is configured: ${walletProblem}. Fix the environment and restart the server.`,
|
|
42
48
|
);
|
|
43
49
|
}
|
|
44
50
|
return agent;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
+
function requireCommand(value) {
|
|
54
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
55
|
+
throw new Error("command is required: the shell command to run on the GPU, e.g. 'nvidia-smi'.");
|
|
56
|
+
}
|
|
57
|
+
if (Buffer.byteLength(value, "utf8") > MAX_COMMAND_BYTES) {
|
|
58
|
+
throw new Error(`command exceeds the ${MAX_COMMAND_BYTES / 1024} KiB limit.`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maxDeposit(args) {
|
|
64
|
+
const cap = args.max_usdg ?? 1;
|
|
65
|
+
if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
|
|
66
|
+
throw new Error("max_usdg must be a positive number of USDG.");
|
|
67
|
+
}
|
|
68
|
+
return Math.round(cap * 1e6);
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
const PROOF_FEED = process.env.PRISM_PROOF_URL ?? "https://prismnetwork.tech/api/proof";
|
|
56
72
|
|
|
57
73
|
async function publicJson(url, what) {
|
|
58
|
-
const response = await fetch(url, {
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
headers: { accept: "application/json" },
|
|
76
|
+
signal: AbortSignal.timeout(10_000),
|
|
77
|
+
});
|
|
59
78
|
if (!response.ok) throw new Error(`prism ${what} unavailable (${response.status})`);
|
|
60
|
-
|
|
79
|
+
const body = await response.json().catch(() => null);
|
|
80
|
+
if (body === null) throw new Error(`prism ${what} answered with something that is not JSON`);
|
|
81
|
+
return body;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
const leases = new Map();
|
|
64
|
-
|
|
85
|
+
// Unconsumed inference payments, keyed by endpoint and price, so a failed
|
|
86
|
+
// generation is retried with the same paid header instead of paying again.
|
|
87
|
+
const pendingInference = new Map();
|
|
88
|
+
// null in, null out: a missing price must never render as 0.000000 USDG.
|
|
89
|
+
const usdg = (micros) =>
|
|
90
|
+
micros == null || !Number.isFinite(Number(micros)) ? null : `${(Number(micros) / 1e6).toFixed(6)} USDG`;
|
|
65
91
|
|
|
66
92
|
function sweepExpiredLeases() {
|
|
67
93
|
const now = Date.now();
|
|
@@ -111,7 +137,7 @@ const TOOLS = [
|
|
|
111
137
|
inputSchema: {
|
|
112
138
|
type: "object",
|
|
113
139
|
properties: {
|
|
114
|
-
limit: { type: "integer", description: "Max receipts to return (default 10)." },
|
|
140
|
+
limit: { type: "integer", description: "Max receipts to return (default 10, max 50)." },
|
|
115
141
|
},
|
|
116
142
|
},
|
|
117
143
|
},
|
|
@@ -122,17 +148,42 @@ const TOOLS = [
|
|
|
122
148
|
},
|
|
123
149
|
{
|
|
124
150
|
name: "prism_batch_run",
|
|
125
|
-
description: "Fund a lease that runs one command with no interactive access at all: the node executes it and reports the signed output. Matches only suppliers at trust class 'isolated' or above, so it can find no supplier when none is online
|
|
151
|
+
description: "Fund a lease that runs one command with no interactive access at all: the node executes it and reports the signed output. Matches only suppliers at trust class 'isolated' or above, so it can find no supplier when none is online; prefer prism_lease_and_run for broad availability. Output is capped at 64 KiB per stream.",
|
|
126
152
|
inputSchema: {
|
|
127
153
|
type: "object",
|
|
128
154
|
properties: {
|
|
129
155
|
command: { type: "string", description: "Shell command to run (max 8 KiB)." },
|
|
130
156
|
duration_seconds: { type: "integer", description: "Paid window in seconds (default 900, max 21600). A command still running at the end is killed and reported exit 124." },
|
|
131
157
|
min_vram_mib: { type: "integer", description: "Minimum GPU memory in MiB (default 16000)." },
|
|
158
|
+
max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
|
|
132
159
|
},
|
|
133
160
|
required: ["command"],
|
|
134
161
|
},
|
|
135
162
|
},
|
|
163
|
+
{
|
|
164
|
+
name: "prism_batch_result",
|
|
165
|
+
description: "Read the output of a batch lease by lease_id, once its node has reported. Use it to recover a result after prism_batch_run timed out.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
lease_id: { type: "integer", description: "The lease_id from prism_batch_run's output or error." },
|
|
170
|
+
},
|
|
171
|
+
required: ["lease_id"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "prism_infer",
|
|
176
|
+
description: "Buy one LLM generation from Prism's managed inference endpoint. Pays the quoted USDG price from this wallet (about 0.01 USDG), waits through a cold start when no box is warm (up to a few minutes), and returns the generation with token usage. Cheaper and simpler than leasing when all you need is a completion.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
prompt: { type: "string", description: "The prompt to generate from (max 32 KiB)." },
|
|
181
|
+
model: { type: "string", description: "Model to use; defaults to the endpoint's first offered model." },
|
|
182
|
+
max_usdg: { type: "number", description: "Refuse if the quoted price exceeds this (default 0.05)." },
|
|
183
|
+
},
|
|
184
|
+
required: ["prompt"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
136
187
|
{
|
|
137
188
|
name: "prism_lease_and_run",
|
|
138
189
|
description: "Lease a GPU, run one shell command on it, and return the output. The lease stays alive (use prism_run for more commands, prism_end_lease to release). Prefer this for a single command; use prism_lease when you'll run several.",
|
|
@@ -147,6 +198,7 @@ const TOOLS = [
|
|
|
147
198
|
enum: TRUST_CLASSES,
|
|
148
199
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
149
200
|
},
|
|
201
|
+
max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
|
|
150
202
|
},
|
|
151
203
|
required: ["command"],
|
|
152
204
|
},
|
|
@@ -164,6 +216,7 @@ const TOOLS = [
|
|
|
164
216
|
enum: TRUST_CLASSES,
|
|
165
217
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
166
218
|
},
|
|
219
|
+
max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
|
|
167
220
|
},
|
|
168
221
|
},
|
|
169
222
|
},
|
|
@@ -191,7 +244,7 @@ const TOOLS = [
|
|
|
191
244
|
},
|
|
192
245
|
{
|
|
193
246
|
name: "prism_vault_store",
|
|
194
|
-
description: "Store private data
|
|
247
|
+
description: "Store private data (a card, an identity document, an API credential) encrypted under a key derived from this agent's wallet on this machine. Prism receives ciphertext only and cannot read it. Use this instead of writing a secret into a workspace or a file. Returns an item_id; the value is not recoverable without the wallet.",
|
|
195
248
|
inputSchema: {
|
|
196
249
|
type: "object",
|
|
197
250
|
properties: {
|
|
@@ -213,7 +266,7 @@ const TOOLS = [
|
|
|
213
266
|
},
|
|
214
267
|
{
|
|
215
268
|
name: "prism_vault_read",
|
|
216
|
-
description: "Decrypt and return one vault item, in this process, using the wallet-derived key. The plaintext exists only here
|
|
269
|
+
description: "Decrypt and return one vault item, in this process, using the wallet-derived key. The plaintext exists only here; do not echo it into a leased workspace, a log, or a message.",
|
|
217
270
|
inputSchema: {
|
|
218
271
|
type: "object",
|
|
219
272
|
properties: { item_id: { type: "string", description: "The item_id from prism_vault_store or prism_vault_list." } },
|
|
@@ -250,12 +303,16 @@ async function handle(name, args) {
|
|
|
250
303
|
}
|
|
251
304
|
if (name === "prism_list_gpus") {
|
|
252
305
|
const minTrust = args.min_trust ?? "open";
|
|
306
|
+
if (!TRUST_CLASSES.includes(minTrust)) {
|
|
307
|
+
throw new Error(`min_trust must be one of ${TRUST_CLASSES.join(", ")}`);
|
|
308
|
+
}
|
|
253
309
|
let offers;
|
|
254
310
|
if (agent) {
|
|
255
|
-
await ensureAuth();
|
|
256
311
|
offers = await agent.offers({ minTrust });
|
|
257
312
|
} else {
|
|
258
|
-
|
|
313
|
+
const url = new URL("/v1/offers", PUBLIC_API);
|
|
314
|
+
url.searchParams.set("min_trust", minTrust);
|
|
315
|
+
offers = await publicJson(url, "offers");
|
|
259
316
|
}
|
|
260
317
|
return {
|
|
261
318
|
available: offers.length,
|
|
@@ -265,6 +322,9 @@ async function handle(name, args) {
|
|
|
265
322
|
price_per_second: usdg(o.rate_per_second),
|
|
266
323
|
price_per_hour: usdg(o.rate_per_second * 3600),
|
|
267
324
|
trust: o.trust_class,
|
|
325
|
+
// A staker-only offer will not match a wallet without the stake, so an
|
|
326
|
+
// unstaked renter should budget from the non-staker rows.
|
|
327
|
+
...(o.staker_only ? { staker_only: true } : {}),
|
|
268
328
|
})),
|
|
269
329
|
};
|
|
270
330
|
}
|
|
@@ -278,21 +338,27 @@ async function handle(name, args) {
|
|
|
278
338
|
sourced_low_per_hour: usdg(g.sourced_low_micros_per_hour),
|
|
279
339
|
sourced_median_per_hour: usdg(g.sourced_median_micros_per_hour),
|
|
280
340
|
sourced_high_per_hour: usdg(g.sourced_high_micros_per_hour),
|
|
281
|
-
settled_mean_per_hour:
|
|
341
|
+
settled_mean_per_hour: usdg(g.settled_mean_micros_per_hour),
|
|
282
342
|
settled_leases: g.settled_leases,
|
|
283
343
|
})),
|
|
284
344
|
};
|
|
285
345
|
}
|
|
286
346
|
if (name === "prism_receipts") {
|
|
287
347
|
const feed = await publicJson(PROOF_FEED, "proof feed");
|
|
288
|
-
|
|
348
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit <= 0)) {
|
|
349
|
+
throw new Error("limit must be a positive integer (max 50)");
|
|
350
|
+
}
|
|
351
|
+
const limit = Math.min(args.limit ?? 10, 50);
|
|
289
352
|
return {
|
|
290
353
|
generated_at: feed.generated_at,
|
|
291
354
|
receipts: (feed.receipts ?? []).slice(0, limit).map((r) => ({
|
|
355
|
+
// The feed numbers leases per escrow deployment, so lease_id repeats
|
|
356
|
+
// across deployments; receipt_id is the unique handle.
|
|
357
|
+
receipt_id: r.receipt_id,
|
|
292
358
|
lease_id: r.lease_id,
|
|
293
359
|
outcome: r.outcome,
|
|
294
360
|
gpu_model: r.gpu_model,
|
|
295
|
-
trust: r.trust_class,
|
|
361
|
+
trust: r.trust_class ?? null,
|
|
296
362
|
runtime_seconds: r.runtime_seconds,
|
|
297
363
|
charged: usdg(r.charged_base_units),
|
|
298
364
|
refunded: usdg(r.refunded_base_units),
|
|
@@ -302,7 +368,6 @@ async function handle(name, args) {
|
|
|
302
368
|
}
|
|
303
369
|
if (name === "prism_leases") {
|
|
304
370
|
requireWallet(name);
|
|
305
|
-
await ensureAuth();
|
|
306
371
|
const all = await agent.leases();
|
|
307
372
|
return {
|
|
308
373
|
total: all.length,
|
|
@@ -320,13 +385,14 @@ async function handle(name, args) {
|
|
|
320
385
|
};
|
|
321
386
|
}
|
|
322
387
|
if (name === "prism_batch_run") {
|
|
323
|
-
|
|
388
|
+
requireCommand(args.command);
|
|
324
389
|
requireWallet(name);
|
|
325
|
-
|
|
390
|
+
const cap = maxDeposit(args);
|
|
326
391
|
const batch = await agent.lease({
|
|
327
392
|
image: IMAGE,
|
|
328
393
|
durationSeconds: args.duration_seconds ?? 900,
|
|
329
394
|
minVramMib: args.min_vram_mib ?? 16000,
|
|
395
|
+
maxDeposit: cap,
|
|
330
396
|
command: args.command,
|
|
331
397
|
});
|
|
332
398
|
return {
|
|
@@ -338,35 +404,114 @@ async function handle(name, args) {
|
|
|
338
404
|
truncated: batch.result?.truncated ?? false,
|
|
339
405
|
};
|
|
340
406
|
}
|
|
407
|
+
if (name === "prism_batch_result") {
|
|
408
|
+
requireWallet(name, "reads this wallet's leases");
|
|
409
|
+
const id = leaseId(args.lease_id);
|
|
410
|
+
return { lease_id: id, result: await agent.result(id) };
|
|
411
|
+
}
|
|
412
|
+
if (name === "prism_infer") {
|
|
413
|
+
if (typeof args.prompt !== "string" || args.prompt.trim() === "") {
|
|
414
|
+
throw new Error("prompt is required.");
|
|
415
|
+
}
|
|
416
|
+
requireWallet(name);
|
|
417
|
+
const base = (process.env.PRISM_INFERENCE_URL ?? "https://api.prismnetwork.tech/inference").replace(/\/$/, "");
|
|
418
|
+
const offer = await publicJson(`${base}/v1/models`, "inference endpoint");
|
|
419
|
+
const model = args.model ?? offer.models?.[0];
|
|
420
|
+
if (!model || (Array.isArray(offer.models) && !offer.models.includes(model))) {
|
|
421
|
+
throw new Error(`model must be one of ${offer.models?.join(", ") ?? "(endpoint offered none)"}`);
|
|
422
|
+
}
|
|
423
|
+
const price = BigInt(offer.price_micros ?? 0);
|
|
424
|
+
const cap = args.max_usdg ?? 0.05;
|
|
425
|
+
if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
|
|
426
|
+
throw new Error("max_usdg must be a positive number of USDG.");
|
|
427
|
+
}
|
|
428
|
+
if (price <= 0n || price > BigInt(Math.round(cap * 1e6))) {
|
|
429
|
+
throw new Error(`the endpoint quotes ${usdg(price)} per generation, past the ${cap} USDG cap.`);
|
|
430
|
+
}
|
|
431
|
+
const pendingKey = `${base}:${price}`;
|
|
432
|
+
let pending = pendingInference.get(pendingKey);
|
|
433
|
+
if (!pending) {
|
|
434
|
+
const paymentTx = await agent.transferUsdg(offer.pay_to, price);
|
|
435
|
+
const signature = await agent.account.signMessage({ message: paymentTx });
|
|
436
|
+
pending = {
|
|
437
|
+
tx: paymentTx,
|
|
438
|
+
header: Buffer.from(JSON.stringify({ txHash: paymentTx, signature })).toString("base64"),
|
|
439
|
+
};
|
|
440
|
+
pendingInference.set(pendingKey, pending);
|
|
441
|
+
}
|
|
442
|
+
// A cold endpoint holds the request through provisioning; when it answers
|
|
443
|
+
// 503 instead, the payment is not consumed and the same header retries.
|
|
444
|
+
const deadline = Date.now() + 600_000;
|
|
445
|
+
for (;;) {
|
|
446
|
+
const res = await fetch(`${base}/v1/inference`, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: { "content-type": "application/json", "x-payment": pending.header },
|
|
449
|
+
body: JSON.stringify({ model, prompt: args.prompt }),
|
|
450
|
+
signal: AbortSignal.timeout(620_000),
|
|
451
|
+
});
|
|
452
|
+
const body = await res.json().catch(() => null);
|
|
453
|
+
if (res.status === 200 && body) {
|
|
454
|
+
pendingInference.delete(pendingKey);
|
|
455
|
+
return { ...body, paid: usdg(price), payment_tx: pending.tx };
|
|
456
|
+
}
|
|
457
|
+
const last = body?.detail ?? body?.error ?? `status ${res.status}`;
|
|
458
|
+
// 503 means the box is warming; a 402 for a payment that is merely too
|
|
459
|
+
// young (confirmations still landing, receipt not yet visible) heals by
|
|
460
|
+
// itself. Everything else is final.
|
|
461
|
+
const retryable =
|
|
462
|
+
res.status === 503 ||
|
|
463
|
+
(res.status === 402 && ["insufficient_confirmations", "tx_not_found"].includes(body?.error));
|
|
464
|
+
if (!retryable || Date.now() > deadline) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`inference failed: ${last}. The payment (tx ${pending.tx}) was not consumed and is kept; the next prism_infer call retries with it instead of paying again.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
await new Promise((r) => setTimeout(r, 15_000));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
341
472
|
if (name === "prism_lease_and_run" || name === "prism_lease") {
|
|
342
|
-
if (name === "prism_lease_and_run"
|
|
473
|
+
if (name === "prism_lease_and_run") requireCommand(args.command);
|
|
343
474
|
requireWallet(name);
|
|
344
|
-
|
|
475
|
+
const cap = maxDeposit(args);
|
|
345
476
|
sweepExpiredLeases();
|
|
346
477
|
const lease = await agent.lease({
|
|
347
478
|
image: IMAGE,
|
|
348
479
|
durationSeconds: args.duration_seconds ?? 900,
|
|
349
480
|
minVramMib: args.min_vram_mib ?? 16000,
|
|
481
|
+
maxDeposit: cap,
|
|
350
482
|
minTrustClass: args.min_trust_class ?? "open",
|
|
351
483
|
});
|
|
352
484
|
leases.set(lease.leaseId, lease);
|
|
353
485
|
const summary = {
|
|
354
486
|
lease_id: lease.leaseId,
|
|
487
|
+
funding_tx: lease.fundingHash,
|
|
355
488
|
ssh: { host: lease.access.ssh_host, port: lease.access.ssh_port, user: lease.access.ssh_user },
|
|
356
489
|
trust: lease.quote?.trust_class ?? "open",
|
|
357
490
|
expires_at: lease.access.expires_at,
|
|
358
491
|
};
|
|
359
492
|
if (name === "prism_lease") return summary;
|
|
360
|
-
|
|
361
|
-
|
|
493
|
+
// The lease is paid for by this point; an SSH failure must still hand the
|
|
494
|
+
// caller everything it bought.
|
|
495
|
+
try {
|
|
496
|
+
const out = await agent.run(lease, args.command);
|
|
497
|
+
return { ...summary, command: args.command, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return {
|
|
500
|
+
...summary,
|
|
501
|
+
error: `the lease is funded but the command could not run: ${err?.message ?? err}`,
|
|
502
|
+
next: `the lease stays open; try prism_run with lease_id ${lease.leaseId}, or prism_end_lease`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
362
505
|
}
|
|
363
506
|
if (name === "prism_run") {
|
|
364
|
-
|
|
507
|
+
requireCommand(args.command);
|
|
365
508
|
const id = leaseId(args.lease_id);
|
|
366
509
|
const lease = leases.get(id);
|
|
367
510
|
if (!lease) throw new Error(`no active lease ${id} in this session`);
|
|
511
|
+
const timeoutSeconds = args.timeout_seconds ?? 120;
|
|
368
512
|
const out = await agent.run(lease, args.command, {
|
|
369
|
-
timeoutMs:
|
|
513
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
514
|
+
connectRetries: 6,
|
|
370
515
|
});
|
|
371
516
|
return { lease_id: id, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
|
|
372
517
|
}
|
|
@@ -380,14 +525,13 @@ async function handle(name, args) {
|
|
|
380
525
|
return { lease_id: id, released: Boolean(lease) };
|
|
381
526
|
}
|
|
382
527
|
if (name.startsWith("prism_vault_")) return handleVault(name, args);
|
|
383
|
-
throw new Error(`unknown tool ${name}`);
|
|
528
|
+
throw new Error(`unknown tool ${name}. Valid tools: ${TOOLS.map((t) => t.name).join(", ")}`);
|
|
384
529
|
}
|
|
385
530
|
|
|
386
531
|
// The vault key is derived here from the wallet signature and stays in this
|
|
387
532
|
// process. Nothing in this function sends a key or a plaintext to Prism.
|
|
388
533
|
async function handleVault(name, args) {
|
|
389
|
-
const vault = requireWallet(name).vault;
|
|
390
|
-
await ensureAuth();
|
|
534
|
+
const vault = requireWallet(name, "derives the vault key from the wallet").vault;
|
|
391
535
|
if (!vault.unlocked) await vault.unlock();
|
|
392
536
|
|
|
393
537
|
if (name === "prism_vault_store") {
|
|
@@ -433,23 +577,26 @@ async function handleVault(name, args) {
|
|
|
433
577
|
throw new Error(`unknown tool ${name}`);
|
|
434
578
|
}
|
|
435
579
|
|
|
436
|
-
|
|
437
|
-
function ensureAuth() {
|
|
438
|
-
authPromise ??= agent.authenticate().catch((err) => {
|
|
439
|
-
authPromise = null;
|
|
440
|
-
throw err;
|
|
441
|
-
});
|
|
442
|
-
return authPromise;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const server = new Server({ name: "prism", version: "0.5.0" }, { capabilities: { tools: {} } });
|
|
580
|
+
const server = new Server({ name: "prism", version: "0.7.0" }, { capabilities: { tools: {} } });
|
|
446
581
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
447
582
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
448
583
|
try {
|
|
449
584
|
const result = await handle(request.params.name, request.params.arguments ?? {});
|
|
450
585
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
451
586
|
} catch (err) {
|
|
452
|
-
|
|
587
|
+
const body = err?.body ?? {};
|
|
588
|
+
const detail = [
|
|
589
|
+
body.cause,
|
|
590
|
+
body.hint,
|
|
591
|
+
body.required != null ? `required ${body.required}` : null,
|
|
592
|
+
body.max != null ? `cap ${body.max}` : null,
|
|
593
|
+
body.lease_id != null ? `lease_id ${body.lease_id}` : null,
|
|
594
|
+
body.funding_hash ? `funding_tx ${body.funding_hash}` : null,
|
|
595
|
+
]
|
|
596
|
+
.filter(Boolean)
|
|
597
|
+
.join("; ");
|
|
598
|
+
const text = `error: ${err?.message ?? err}${detail ? ` (${detail})` : ""}`;
|
|
599
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
453
600
|
}
|
|
454
601
|
});
|
|
455
602
|
|