@prismnetwork/mcp 0.8.4 → 0.9.1
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 +31 -14
- package/budget.mjs +61 -2
- package/package.json +15 -13
- package/server.mjs +252 -103
package/README.md
CHANGED
|
@@ -13,9 +13,6 @@ claude mcp add prism -- npx -y @prismnetwork/mcp
|
|
|
13
13
|
Then ask what you can rent. `prism_list_gpus` answers from live capacity with
|
|
14
14
|
real prices. Leasing spends money, so those tools ask for a wallet and say so.
|
|
15
15
|
|
|
16
|
-
Packaged for Claude Code and Codex as the [Prism plugin](https://plugins.prismnetwork.tech),
|
|
17
|
-
which wires this server up and prompts for the wallet at install time.
|
|
18
|
-
|
|
19
16
|
## Tools
|
|
20
17
|
|
|
21
18
|
| Tool | Wallet |
|
|
@@ -27,6 +24,9 @@ which wires this server up and prompts for the wallet at install time.
|
|
|
27
24
|
| `prism_wallet` | yes |
|
|
28
25
|
| `prism_leases` | yes |
|
|
29
26
|
| `prism_infer` | yes |
|
|
27
|
+
| `prism_infer_batch` | yes |
|
|
28
|
+
| `prism_confidential_infer` | yes |
|
|
29
|
+
| `prism_verify_attestation` | no |
|
|
30
30
|
| `prism_lease_and_run` | yes |
|
|
31
31
|
| `prism_lease` | yes |
|
|
32
32
|
| `prism_run` | yes |
|
|
@@ -38,8 +38,8 @@ which wires this server up and prompts for the wallet at install time.
|
|
|
38
38
|
| `prism_vault_delete` | yes |
|
|
39
39
|
| `prism_vault_release` | yes |
|
|
40
40
|
|
|
41
|
-
- `prism_budget`: the spending limits
|
|
42
|
-
|
|
41
|
+
- `prism_budget`: the spending limits this server enforces, what it has spent
|
|
42
|
+
in the last 24 hours, and the recent charges.
|
|
43
43
|
- `prism_wallet`: the agent's address and USDG/ETH balances.
|
|
44
44
|
- `prism_list_gpus`: GPUs available to lease, with price per second and per hour.
|
|
45
45
|
- `prism_price_index`: sourced and settled pricing per GPU model, for cost estimates.
|
|
@@ -50,8 +50,28 @@ which wires this server up and prompts for the wallet at install time.
|
|
|
50
50
|
paying the quoted USDG price from this wallet (about 0.01 USDG). Waits
|
|
51
51
|
through a cold start; an unconsumed payment is kept and reused on the next
|
|
52
52
|
call instead of paying twice.
|
|
53
|
+
- `prism_infer_batch`: buy many generations in one paid call, at the
|
|
54
|
+
single-generation price times the number of prompts. Each prompt runs whole on
|
|
55
|
+
a rented GPU and they are spread across every GPU the endpoint holds, so a
|
|
56
|
+
list of prompts finishes far sooner than the same prompts sent one at a time.
|
|
57
|
+
Returns every answer in order with a Merkle receipt naming the leases that did
|
|
58
|
+
the work.
|
|
59
|
+
- `prism_confidential_infer`: buy one generation that runs inside a GPU TEE.
|
|
60
|
+
The message contents are encrypted to a key the enclave's own attestation
|
|
61
|
+
commits to, so this server's relay carries ciphertext and cannot read the
|
|
62
|
+
prompt or the answer. Returns the answer, the cost and a receipt id the
|
|
63
|
+
workload signed over the exact bytes of the exchange.
|
|
64
|
+
- `prism_verify_attestation`: check one of those generations against its
|
|
65
|
+
receipt and the hardware behind it. The server remembers the digests of the
|
|
66
|
+
last few confidential calls, so the verdict is bound to the bytes it actually
|
|
67
|
+
sent and received. Every check is reported with its result, including the two
|
|
68
|
+
that cannot be established today: private-key custody, and where TLS
|
|
69
|
+
terminates.
|
|
53
70
|
- `prism_lease_and_run`: lease a GPU, run a command, return the output (one shot).
|
|
54
71
|
- `prism_lease`: lease a GPU and keep it; returns a `lease_id` and SSH access.
|
|
72
|
+
The SSH block carries `host_key_fingerprint` and `host_key_claim` when the
|
|
73
|
+
network can say which machine should answer, and says so plainly when it
|
|
74
|
+
cannot. Check it before connecting by hand; `prism_run` checks it for you.
|
|
55
75
|
- `prism_run`: run a command on an existing lease.
|
|
56
76
|
- `prism_batch_run`: fund a lease that runs one command with no interactive
|
|
57
77
|
access; the node reports the signed output. Matches only suppliers at trust
|
|
@@ -73,6 +93,10 @@ Two ceilings bound what this server can spend, and a refusal quotes both.
|
|
|
73
93
|
| `PRISM_DAILY_BUDGET_USDG` | 5 | Everything in a rolling 24 hours. `0` removes the ceiling. |
|
|
74
94
|
| `PRISM_LEDGER_PATH` | `~/.prism/spend.json` | Where the spend is recorded. |
|
|
75
95
|
|
|
96
|
+
A `max_usdg` above `PRISM_MAX_USDG` is clamped back to it. The argument is
|
|
97
|
+
written by the agent being bounded, so it is treated as a request for a lower
|
|
98
|
+
ceiling and never as permission for a higher one.
|
|
99
|
+
|
|
76
100
|
Spend is written before the money moves, so a crash between funding an escrow
|
|
77
101
|
and answering is counted rather than forgiven, and a restart does not hand back
|
|
78
102
|
a fresh day's allowance. Only an attempt that provably never reached the chain
|
|
@@ -111,8 +135,7 @@ Point your MCP client (Claude Desktop / Code) at the published server:
|
|
|
111
135
|
"args": ["-y", "@prismnetwork/mcp"],
|
|
112
136
|
"env": {
|
|
113
137
|
"PRISM_AGENT_KEY": "0x<agent wallet private key>",
|
|
114
|
-
"
|
|
115
|
-
"PRISM_DAILY_BUDGET_USDG": "5"
|
|
138
|
+
"PRISM_ESCROW": "0x62C042265991bEa17B07229322A01850974626dA"
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
}
|
|
@@ -125,17 +148,11 @@ Or add it to Claude Code in one line:
|
|
|
125
148
|
|
|
126
149
|
```sh
|
|
127
150
|
claude mcp add prism \
|
|
151
|
+
--env PRISM_ESCROW=0x62C042265991bEa17B07229322A01850974626dA \
|
|
128
152
|
--env PRISM_AGENT_KEY=0x<agent wallet private key> \
|
|
129
|
-
--env PRISM_DAILY_BUDGET_USDG=5 \
|
|
130
153
|
-- npx -y @prismnetwork/mcp
|
|
131
154
|
```
|
|
132
155
|
|
|
133
|
-
Codex takes the same server:
|
|
134
|
-
|
|
135
|
-
```sh
|
|
136
|
-
codex mcp add prism --env PRISM_AGENT_KEY=0x<agent wallet private key> -- npx -y @prismnetwork/mcp
|
|
137
|
-
```
|
|
138
|
-
|
|
139
156
|
## Timing
|
|
140
157
|
|
|
141
158
|
`prism_lease` and `prism_lease_and_run` block while a GPU provisions (usually one to four minutes, occasionally longer on a slow host). Configure your MCP client to allow long tool calls.
|
package/budget.mjs
CHANGED
|
@@ -78,6 +78,17 @@ export function readBudget(env = process.env) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// What a single call is allowed to spend, given what it asked for. The caller's
|
|
82
|
+
// figure is written by the thing being bounded, so it lowers the operator's
|
|
83
|
+
// ceiling and can never lift it.
|
|
84
|
+
export function callCeiling(maxUsdg, ceilingMicros) {
|
|
85
|
+
if (maxUsdg === undefined || maxUsdg === null) return ceilingMicros;
|
|
86
|
+
if (typeof maxUsdg !== "number" || !Number.isFinite(maxUsdg) || maxUsdg <= 0) {
|
|
87
|
+
throw new BudgetError("max_usdg must be a positive number of USDG.");
|
|
88
|
+
}
|
|
89
|
+
return Math.min(Math.round(maxUsdg * MICROS), ceilingMicros);
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
// A lock rather than last-write-wins, because two clients sharing one wallet is
|
|
82
93
|
// the case this file exists for. A lock older than LOCK_STALE_MS belonged to a
|
|
83
94
|
// process that died; breaking it is safe and not breaking it wedges the wallet.
|
|
@@ -242,16 +253,64 @@ export class SpendLedger {
|
|
|
242
253
|
// Replaces the reserved figure with what was actually committed on-chain and
|
|
243
254
|
// pins the receipt to it, so the ledger reads like a statement rather than a
|
|
244
255
|
// list of intentions.
|
|
256
|
+
//
|
|
257
|
+
// A payment the endpoint never consumed is redeemed by the next attempt at the
|
|
258
|
+
// same request, so one transaction can settle more than one reservation.
|
|
259
|
+
// Booking each would charge the day twice for money that moved once: a
|
|
260
|
+
// reference already on file keeps its entry and this one is released.
|
|
245
261
|
settle(id, { micros, reference } = {}) {
|
|
246
262
|
if (!id) return false;
|
|
247
263
|
return withLock(this.path, () => {
|
|
248
264
|
const state = readState(this.path);
|
|
249
265
|
const entry = state.entries.find((e) => e.id === id);
|
|
250
266
|
if (!entry) return false;
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
const booked = reference ? state.entries.find((e) => e.id !== id && e.reference === reference) : undefined;
|
|
268
|
+
const target = booked ?? entry;
|
|
269
|
+
if (Number.isFinite(micros) && micros >= 0) target.micros = micros;
|
|
270
|
+
if (reference) target.reference = reference;
|
|
271
|
+
if (booked) state.entries = state.entries.filter((e) => e.id !== id);
|
|
253
272
|
writeState(this.path, state, Date.now());
|
|
254
273
|
return true;
|
|
255
274
|
}, this.lockWaitMs);
|
|
256
275
|
}
|
|
257
276
|
}
|
|
277
|
+
|
|
278
|
+
/// Records a spend before the money moves, then reconciles it against what
|
|
279
|
+
/// happened. A failure that never reached the chain is given back; anything
|
|
280
|
+
/// that funded an escrow or paid an endpoint keeps its entry and gains the
|
|
281
|
+
/// transaction that proves it, because a ledger that forgets a spend is worse
|
|
282
|
+
/// than no ledger at all.
|
|
283
|
+
export async function recordSpend(book, tool, micros, run) {
|
|
284
|
+
const id = book.commit({ tool, micros });
|
|
285
|
+
// Reconciling is bookkeeping and must never be the reason a caller loses a
|
|
286
|
+
// machine it paid for, or the reason a failure is reported as the wrong
|
|
287
|
+
// failure. A ledger that cannot be written says so on stderr and the
|
|
288
|
+
// committed figure stands, which errs towards counting the spend.
|
|
289
|
+
const reconcile = (action, ...args) => {
|
|
290
|
+
try {
|
|
291
|
+
book[action](id, ...args);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error(`prism mcp: could not ${action} the ledger entry for ${tool}: ${err?.message ?? err}`);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
try {
|
|
297
|
+
const { value, settledMicros, reference } = await run();
|
|
298
|
+
reconcile("settle", { micros: settledMicros, reference });
|
|
299
|
+
return value;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
const paid = err?.body?.funding_hash ?? err?.body?.payment_tx;
|
|
302
|
+
if (paid) {
|
|
303
|
+
reconcile("settle", { reference: paid });
|
|
304
|
+
} else if (err?.code === "chain_error") {
|
|
305
|
+
// Signed, broadcast, and then something went wrong reading it back, which
|
|
306
|
+
// is not the same as never having reached the chain. Handing the
|
|
307
|
+
// reservation back would let one wallet fund escrow after escrow through
|
|
308
|
+
// an rpc having a bad hour while the day's ceiling reported nothing
|
|
309
|
+
// spent, so the entry stands at what it reserved.
|
|
310
|
+
reconcile("settle");
|
|
311
|
+
} else {
|
|
312
|
+
reconcile("revert");
|
|
313
|
+
}
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prismnetwork/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "MCP server for leasing and running on Prism Network GPUs.",
|
|
5
|
-
"mcpName": "
|
|
5
|
+
"mcpName": "io.github.winter0x/mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"prism-mcp": "server.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"server.mjs",
|
|
12
11
|
"budget.mjs",
|
|
12
|
+
"server.mjs",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"engines": {
|
|
@@ -17,31 +17,33 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
20
|
-
"@
|
|
20
|
+
"@phala/dcap-qvl": "^0.6.1",
|
|
21
|
+
"@prismnetwork/agent-sdk": "^0.7.3",
|
|
22
|
+
"jose": "^6",
|
|
21
23
|
"viem": "^2"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
26
|
+
"prism",
|
|
27
|
+
"mcp",
|
|
28
|
+
"gpu",
|
|
24
29
|
"agent",
|
|
25
30
|
"claude",
|
|
26
|
-
"
|
|
27
|
-
"compute",
|
|
28
|
-
"gpu",
|
|
29
|
-
"gpu-rental",
|
|
30
|
-
"mcp",
|
|
31
|
-
"prism",
|
|
32
|
-
"x402"
|
|
31
|
+
"compute"
|
|
33
32
|
],
|
|
34
33
|
"homepage": "https://prismnetwork.tech",
|
|
35
34
|
"repository": {
|
|
36
35
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
36
|
+
"url": "git+https://github.com/winter0x/prism.git",
|
|
38
37
|
"directory": "mcp"
|
|
39
38
|
},
|
|
40
39
|
"bugs": {
|
|
41
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "https://github.com/winter0x/prism/issues"
|
|
42
41
|
},
|
|
43
42
|
"license": "Apache-2.0",
|
|
44
43
|
"publishConfig": {
|
|
45
44
|
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "node --test"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/server.mjs
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
// Prism Network MCP server: lets an MCP client (Claude, agents) see and lease
|
|
3
3
|
// real GPUs. Looking is free and needs no configuration. Leasing spends money,
|
|
4
4
|
// so it needs a wallet: PRISM_AGENT_KEY, PRISM_ESCROW.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_IMAGE,
|
|
11
|
+
DEFAULT_TRUST_FLOOR,
|
|
12
|
+
hostKeyPolicy,
|
|
13
|
+
PrismAgent,
|
|
14
|
+
TRUST_CLASSES,
|
|
15
|
+
verifyConfidential,
|
|
16
|
+
} from "@prismnetwork/agent-sdk";
|
|
17
|
+
import { BudgetError, SpendLedger, callCeiling, readBudget, recordSpend, stripUnexpanded } from "./budget.mjs";
|
|
10
18
|
|
|
11
19
|
stripUnexpanded(process.env);
|
|
12
20
|
|
|
@@ -72,35 +80,9 @@ function requireLedger(tool) {
|
|
|
72
80
|
return ledger;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
// a funded lease is worse than no ledger at all.
|
|
79
|
-
async function spending(tool, micros, run) {
|
|
80
|
-
const book = requireLedger(tool);
|
|
81
|
-
const id = book.commit({ tool, micros });
|
|
82
|
-
// Reconciling is bookkeeping and must never be the reason a caller loses a
|
|
83
|
-
// machine it paid for, or the reason a failure is reported as the wrong
|
|
84
|
-
// failure. A ledger that cannot be written says so on stderr and the
|
|
85
|
-
// committed figure stands, which errs towards counting the spend.
|
|
86
|
-
const reconcile = (action, ...args) => {
|
|
87
|
-
try {
|
|
88
|
-
book[action](id, ...args);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error(`prism mcp: could not ${action} the ledger entry for ${tool}: ${err?.message ?? err}`);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
const { value, settledMicros, reference } = await run();
|
|
95
|
-
reconcile("settle", { micros: settledMicros, reference });
|
|
96
|
-
return value;
|
|
97
|
-
} catch (err) {
|
|
98
|
-
const funded = err?.body?.funding_hash;
|
|
99
|
-
if (funded) reconcile("settle", { reference: funded });
|
|
100
|
-
else reconcile("revert");
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
83
|
+
// The ledger has to be usable before anything is spent, and the refusal names
|
|
84
|
+
// the tool that asked.
|
|
85
|
+
const spending = (tool, micros, run) => recordSpend(requireLedger(tool), tool, micros, run);
|
|
104
86
|
|
|
105
87
|
function requireCommand(value) {
|
|
106
88
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -112,6 +94,16 @@ function requireCommand(value) {
|
|
|
112
94
|
return value;
|
|
113
95
|
}
|
|
114
96
|
|
|
97
|
+
// What the caller can check the machine against if they open their own session.
|
|
98
|
+
// A lease that publishes nothing says so, rather than leaving the field out and
|
|
99
|
+
// letting its absence read as "fine".
|
|
100
|
+
function hostKey(access) {
|
|
101
|
+
const policy = hostKeyPolicy(access);
|
|
102
|
+
return policy.fingerprint === null
|
|
103
|
+
? { host_key: "unpublished: this lease cannot tell you which machine answers" }
|
|
104
|
+
: { host_key_fingerprint: policy.fingerprint, host_key_claim: policy.mode };
|
|
105
|
+
}
|
|
106
|
+
|
|
115
107
|
// What the escrow actually holds, which is what the day's budget should count.
|
|
116
108
|
// Booking the caller's ceiling instead charged a 0.2 USDG lease against a 0.5
|
|
117
109
|
// USDG cap, so a 5 USDG day bought ten leases where it could afford twenty-five.
|
|
@@ -121,15 +113,10 @@ function escrowed(quote) {
|
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
// The per-call ceiling is the operator's, not the model's: an omitted max_usdg
|
|
124
|
-
// takes
|
|
125
|
-
//
|
|
116
|
+
// takes PRISM_MAX_USDG rather than a hardcoded number, and a stated one above it
|
|
117
|
+
// is clamped back down to it.
|
|
126
118
|
function maxDeposit(tool, args) {
|
|
127
|
-
|
|
128
|
-
const cap = args.max_usdg;
|
|
129
|
-
if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
|
|
130
|
-
throw new Error("max_usdg must be a positive number of USDG.");
|
|
131
|
-
}
|
|
132
|
-
return Math.round(cap * 1e6);
|
|
119
|
+
return callCeiling(args.max_usdg, requireLedger(tool).maxPerCallMicros);
|
|
133
120
|
}
|
|
134
121
|
|
|
135
122
|
const PROOF_FEED = process.env.PRISM_PROOF_URL ?? "https://prismnetwork.tech/api/proof";
|
|
@@ -146,13 +133,57 @@ async function publicJson(url, what) {
|
|
|
146
133
|
}
|
|
147
134
|
|
|
148
135
|
const leases = new Map();
|
|
149
|
-
// Unconsumed inference payments, keyed by endpoint and price, so a failed
|
|
150
|
-
// generation is retried with the same paid header instead of paying again.
|
|
151
|
-
const pendingInference = new Map();
|
|
152
136
|
// null in, null out: a missing price must never render as 0.000000 USDG.
|
|
153
137
|
const usdg = (micros) =>
|
|
154
138
|
micros == null || !Number.isFinite(Number(micros)) ? null : `${(Number(micros) / 1e6).toFixed(6)} USDG`;
|
|
155
139
|
|
|
140
|
+
/// The SDK pays once and keeps the payment until the endpoint actually serves,
|
|
141
|
+
/// so a generation that never happened is retried with the payment already made.
|
|
142
|
+
async function payAndPost({ base, path, price, payTo, body, tool }) {
|
|
143
|
+
const served = await agent.payAndPost({ base, path, price, payTo, body, caller: tool });
|
|
144
|
+
return { ...JSON.parse(served.bytes.toString("utf8")), paid: usdg(price), payment_tx: served.tx };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// What the last few confidential calls sent and received, as digests only, so
|
|
148
|
+
// prism_verify_attestation binds its verdict to the real bytes of a call
|
|
149
|
+
// without keeping anyone's prompt in memory.
|
|
150
|
+
const confidentialCalls = new Map();
|
|
151
|
+
const CONFIDENTIAL_HISTORY = 16;
|
|
152
|
+
|
|
153
|
+
const sha256Prefixed = (bytes) => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
154
|
+
|
|
155
|
+
function rememberConfidentialCall(receiptId, record) {
|
|
156
|
+
if (!receiptId) return;
|
|
157
|
+
confidentialCalls.set(receiptId, record);
|
|
158
|
+
while (confidentialCalls.size > CONFIDENTIAL_HISTORY) {
|
|
159
|
+
confidentialCalls.delete(confidentialCalls.keys().next().value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const inferenceBase = () =>
|
|
164
|
+
(process.env.PRISM_INFERENCE_URL ?? "https://api.prismnetwork.tech/inference").replace(/\/$/, "");
|
|
165
|
+
|
|
166
|
+
/// What the endpoint currently offers, and the model this call should use.
|
|
167
|
+
async function inferenceOffer(base, requested) {
|
|
168
|
+
const offer = await publicJson(`${base}/v1/models`, "inference endpoint");
|
|
169
|
+
const model = requested ?? offer.models?.[0];
|
|
170
|
+
if (!model || (Array.isArray(offer.models) && !offer.models.includes(model))) {
|
|
171
|
+
throw new Error(`model must be one of ${offer.models?.join(", ") ?? "(endpoint offered none)"}`);
|
|
172
|
+
}
|
|
173
|
+
return { offer, model, unit: BigInt(offer.price_micros ?? 0) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The quoted price against the lower of what this call asked for and what the
|
|
177
|
+
// operator allows, so the refusal names whichever ceiling stopped it.
|
|
178
|
+
function withinCap(tool, price, maxUsdg, fallback, what) {
|
|
179
|
+
const operator = requireLedger(tool).maxPerCallMicros;
|
|
180
|
+
const cap = callCeiling(maxUsdg ?? fallback, operator);
|
|
181
|
+
if (price <= 0n || price > BigInt(cap)) {
|
|
182
|
+
const named = cap === operator ? "PRISM_MAX_USDG" : "max_usdg";
|
|
183
|
+
throw new Error(`the endpoint quotes ${usdg(price)} ${what}, past the ${named} cap of ${usdg(cap)}.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
156
187
|
function sweepExpiredLeases() {
|
|
157
188
|
const now = Date.now();
|
|
158
189
|
for (const [id, lease] of leases) {
|
|
@@ -240,7 +271,7 @@ const TOOLS = [
|
|
|
240
271
|
command: { type: "string", description: "Shell command to run (max 8 KiB)." },
|
|
241
272
|
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." },
|
|
242
273
|
min_vram_mib: { type: "integer", description: "Minimum GPU memory in MiB (default 16000)." },
|
|
243
|
-
max_usdg: { type: "number", description: "
|
|
274
|
+
max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." },
|
|
244
275
|
},
|
|
245
276
|
required: ["command"],
|
|
246
277
|
},
|
|
@@ -267,13 +298,64 @@ const TOOLS = [
|
|
|
267
298
|
properties: {
|
|
268
299
|
prompt: { type: "string", description: "The prompt to generate from (max 32 KiB)." },
|
|
269
300
|
model: { type: "string", description: "Model to use; defaults to the endpoint's first offered model." },
|
|
270
|
-
max_usdg: { type: "number", description: "Refuse if the quoted price exceeds this (default 0.05)." },
|
|
301
|
+
max_usdg: { type: "number", description: "Refuse if the quoted price exceeds this (default 0.05). The operator's PRISM_MAX_USDG binds it either way." },
|
|
271
302
|
},
|
|
272
303
|
required: ["prompt"],
|
|
273
304
|
},
|
|
274
305
|
...spends,
|
|
275
306
|
annotations: { title: "Buy one LLM generation", ...spends.annotations },
|
|
276
307
|
},
|
|
308
|
+
{
|
|
309
|
+
name: "prism_infer_batch",
|
|
310
|
+
description: "Buy many LLM generations from Prism's managed inference endpoint in one paid call. Every prompt runs whole on a rented GPU, spread across every GPU the endpoint holds, so a list of prompts finishes far sooner than the same prompts sent one at a time. Costs the single-generation price times the number of prompts. Returns every answer in order plus a Merkle receipt naming the leases that did the work. Use it for evals, dataset passes, rollouts, or anything with more than a handful of independent prompts.",
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
prompts: {
|
|
315
|
+
type: "array",
|
|
316
|
+
items: { type: "string" },
|
|
317
|
+
minItems: 1,
|
|
318
|
+
maxItems: 64,
|
|
319
|
+
description: "Independent prompts, answered in the order given (each max 32 KiB).",
|
|
320
|
+
},
|
|
321
|
+
model: { type: "string", description: "Model to use; defaults to the endpoint's first offered model." },
|
|
322
|
+
max_usdg: { type: "number", description: "Refuse if the quoted total exceeds this (default 0.5). The operator's PRISM_MAX_USDG binds it either way." },
|
|
323
|
+
},
|
|
324
|
+
required: ["prompts"],
|
|
325
|
+
},
|
|
326
|
+
...spends,
|
|
327
|
+
annotations: { title: "Buy many LLM generations", ...spends.annotations },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "prism_confidential_infer",
|
|
331
|
+
description: "Buy one LLM generation that runs inside a GPU TEE, with the message contents encrypted end to end to a key the enclave's own attestation commits to, so Prism's relay in between carries ciphertext and cannot read the prompt or the answer. Costs a little more than prism_infer. Returns the answer, the cost, and a receipt id the workload signed over the exact bytes of the exchange; pass that id to prism_verify_attestation to check the whole chain. Use it for anything the operator of an ordinary endpoint should not be able to read.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
prompt: { type: "string", description: "The prompt to generate from." },
|
|
336
|
+
model: { type: "string", description: "Confidential model to use; defaults to the endpoint's first." },
|
|
337
|
+
max_tokens: { type: "integer", description: "Cap on generated tokens (default 512). The price is quoted against this cap." },
|
|
338
|
+
max_usdg: { type: "number", description: "Refuse if the quoted price exceeds this (default 0.25). The operator's PRISM_MAX_USDG binds it either way." },
|
|
339
|
+
e2ee: { type: "boolean", description: "Encrypt message contents to the attested enclave key (default true). Turn it off only when the relay is allowed to read the prompt." },
|
|
340
|
+
},
|
|
341
|
+
required: ["prompt"],
|
|
342
|
+
},
|
|
343
|
+
...spends,
|
|
344
|
+
annotations: { title: "Buy one confidential LLM generation", ...spends.annotations },
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "prism_verify_attestation",
|
|
348
|
+
description: "Check a confidential generation against its signed receipt and the hardware behind it: the TDX quote verifies to Intel's root and commits to the key set that signed the receipt, the boot log replays to the measurement in that quote, the receipt covers the exact bytes of this call, the upstream that ran the model was itself verified, and the GPU is attested by NVIDIA. Returns every check with its result, including the ones that cannot be established today. Needs no wallet.",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
receipt_id: { type: "string", description: "The receipt_id from prism_confidential_infer." },
|
|
353
|
+
model: { type: "string", description: "Model to fetch GPU evidence for; defaults to the one recorded for this receipt." },
|
|
354
|
+
},
|
|
355
|
+
required: ["receipt_id"],
|
|
356
|
+
},
|
|
357
|
+
annotations: { title: "Check a confidential generation", ...reads },
|
|
358
|
+
},
|
|
277
359
|
{
|
|
278
360
|
name: "prism_lease_and_run",
|
|
279
361
|
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.",
|
|
@@ -288,7 +370,7 @@ const TOOLS = [
|
|
|
288
370
|
enum: TRUST_CLASSES,
|
|
289
371
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
290
372
|
},
|
|
291
|
-
max_usdg: { type: "number", description: "
|
|
373
|
+
max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." },
|
|
292
374
|
},
|
|
293
375
|
required: ["command"],
|
|
294
376
|
},
|
|
@@ -308,7 +390,7 @@ const TOOLS = [
|
|
|
308
390
|
enum: TRUST_CLASSES,
|
|
309
391
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
310
392
|
},
|
|
311
|
-
max_usdg: { type: "number", description: "
|
|
393
|
+
max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." },
|
|
312
394
|
},
|
|
313
395
|
},
|
|
314
396
|
...spends,
|
|
@@ -525,68 +607,126 @@ async function handle(name, args) {
|
|
|
525
607
|
throw new Error("prompt is required.");
|
|
526
608
|
}
|
|
527
609
|
requireWallet(name);
|
|
528
|
-
const base = (
|
|
529
|
-
const offer = await
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
610
|
+
const base = inferenceBase();
|
|
611
|
+
const { offer, model, unit } = await inferenceOffer(base, args.model);
|
|
612
|
+
withinCap(name, unit, args.max_usdg, 0.05, "per generation");
|
|
613
|
+
return spending(name, Number(unit), async () => {
|
|
614
|
+
const value = await payAndPost({
|
|
615
|
+
base,
|
|
616
|
+
path: "/v1/inference",
|
|
617
|
+
price: unit,
|
|
618
|
+
payTo: offer.pay_to,
|
|
619
|
+
body: { model, prompt: args.prompt },
|
|
620
|
+
tool: "prism_infer",
|
|
621
|
+
});
|
|
622
|
+
return { value, settledMicros: Number(unit), reference: value.payment_tx };
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (name === "prism_infer_batch") {
|
|
626
|
+
const prompts = args.prompts;
|
|
627
|
+
if (!Array.isArray(prompts) || !prompts.length) {
|
|
628
|
+
throw new Error("prompts must be a non-empty array of strings.");
|
|
533
629
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
|
|
537
|
-
throw new Error("max_usdg must be a positive number of USDG.");
|
|
630
|
+
if (prompts.some((p) => typeof p !== "string" || p.trim() === "")) {
|
|
631
|
+
throw new Error("every prompt must be a non-empty string.");
|
|
538
632
|
}
|
|
539
|
-
if (
|
|
540
|
-
throw new Error(`
|
|
633
|
+
if (prompts.length > 64) {
|
|
634
|
+
throw new Error(`a batch takes at most 64 prompts; ${prompts.length} were given.`);
|
|
541
635
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
header: Buffer.from(JSON.stringify({ txHash: paymentTx, signature })).toString("base64"),
|
|
556
|
-
},
|
|
557
|
-
};
|
|
636
|
+
requireWallet(name);
|
|
637
|
+
const base = inferenceBase();
|
|
638
|
+
const { offer, model, unit } = await inferenceOffer(base, args.model);
|
|
639
|
+
const price = unit * BigInt(prompts.length);
|
|
640
|
+
withinCap(name, price, args.max_usdg, 0.5, `for ${prompts.length} generations`);
|
|
641
|
+
return spending(name, Number(price), async () => {
|
|
642
|
+
const value = await payAndPost({
|
|
643
|
+
base,
|
|
644
|
+
path: "/v1/batch",
|
|
645
|
+
price,
|
|
646
|
+
payTo: offer.pay_to,
|
|
647
|
+
body: { model, prompts },
|
|
648
|
+
tool: "prism_infer_batch",
|
|
558
649
|
});
|
|
559
|
-
|
|
650
|
+
return { value, settledMicros: Number(price), reference: value.payment_tx };
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (name === "prism_confidential_infer") {
|
|
654
|
+
if (typeof args.prompt !== "string" || args.prompt.trim() === "") {
|
|
655
|
+
throw new Error("prompt is required.");
|
|
560
656
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
657
|
+
requireWallet(name);
|
|
658
|
+
const base = inferenceBase();
|
|
659
|
+
// The endpoint quotes inside the SDK, so the day is charged the ceiling up
|
|
660
|
+
// front and corrected to the quoted price once the call has been served.
|
|
661
|
+
const cap = callCeiling(args.max_usdg ?? 0.25, requireLedger(name).maxPerCallMicros);
|
|
662
|
+
const run = await spending(name, cap, async () => {
|
|
663
|
+
const served = await agent.confidentialInfer({
|
|
664
|
+
prompt: args.prompt,
|
|
665
|
+
model: args.model,
|
|
666
|
+
maxTokens: args.max_tokens ?? 512,
|
|
667
|
+
maxUsdg: cap / 1e6,
|
|
668
|
+
e2ee: args.e2ee ?? true,
|
|
669
|
+
endpoint: base,
|
|
570
670
|
});
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
671
|
+
return { value: served, settledMicros: Number(served.priceMicros), reference: served.tx };
|
|
672
|
+
});
|
|
673
|
+
rememberConfidentialCall(run.receiptId, {
|
|
674
|
+
base,
|
|
675
|
+
model: run.model,
|
|
676
|
+
e2ee: run.e2ee,
|
|
677
|
+
keysetDigest: run.keysetDigest,
|
|
678
|
+
responseHash: sha256Prefixed(run.bytes.response),
|
|
679
|
+
requestHash: sha256Prefixed(run.bytes.request),
|
|
680
|
+
restoredRequestHash: run.bytes.restoredRequest ? sha256Prefixed(run.bytes.restoredRequest) : null,
|
|
681
|
+
});
|
|
682
|
+
return {
|
|
683
|
+
model: run.model,
|
|
684
|
+
content: run.content,
|
|
685
|
+
usage: run.usage,
|
|
686
|
+
receipt_id: run.receiptId,
|
|
687
|
+
paid: usdg(run.priceMicros),
|
|
688
|
+
payment_tx: run.tx,
|
|
689
|
+
e2ee: run.e2ee
|
|
690
|
+
? "on: the prompt and the answer were encrypted to the enclave's attested key, and the relay carried ciphertext"
|
|
691
|
+
: "off: the relay could read this prompt",
|
|
692
|
+
next: `prism_verify_attestation with receipt_id ${run.receiptId} checks the hardware, the receipt and the GPU behind this answer`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (name === "prism_verify_attestation") {
|
|
696
|
+
if (typeof args.receipt_id !== "string" || args.receipt_id.trim() === "") {
|
|
697
|
+
throw new Error("receipt_id is required: the id prism_confidential_infer returned.");
|
|
589
698
|
}
|
|
699
|
+
// Without the bytes of the call, a receipt still proves what the workload
|
|
700
|
+
// signed, but not that it signed this exchange. That is `incomplete`, and it
|
|
701
|
+
// is a different thing to tell an agent than a failure.
|
|
702
|
+
const remembered = confidentialCalls.get(args.receipt_id) ?? {};
|
|
703
|
+
const bound = remembered.responseHash != null;
|
|
704
|
+
const result = await verifyConfidential({
|
|
705
|
+
base: remembered.base ?? inferenceBase(),
|
|
706
|
+
receiptId: args.receipt_id,
|
|
707
|
+
model: args.model ?? remembered.model,
|
|
708
|
+
e2ee: Boolean(remembered.e2ee),
|
|
709
|
+
requestHash: remembered.requestHash ?? null,
|
|
710
|
+
responseHash: remembered.responseHash ?? null,
|
|
711
|
+
restoredRequestHash: remembered.restoredRequestHash ?? null,
|
|
712
|
+
expectedKeysetDigest: remembered.keysetDigest ?? null,
|
|
713
|
+
});
|
|
714
|
+
const mark = { pass: "ok", fail: "FAIL", skip: "skip" };
|
|
715
|
+
return {
|
|
716
|
+
receipt_id: args.receipt_id,
|
|
717
|
+
verdict: result.verdict,
|
|
718
|
+
verdict_means: {
|
|
719
|
+
verified: "every check that ran passed, and the only skips are the documented ones",
|
|
720
|
+
incomplete: "nothing failed, and evidence some check needed was not available here",
|
|
721
|
+
failed: "a check failed",
|
|
722
|
+
}[result.verdict],
|
|
723
|
+
bound_to_this_session: bound,
|
|
724
|
+
...(bound
|
|
725
|
+
? {}
|
|
726
|
+
: { unbound_because: "this server has no record of that call, so the request and response checks could not run" }),
|
|
727
|
+
measured_source: result.provenance,
|
|
728
|
+
checks: result.checks.map((c) => `${mark[c.status]} ${c.title}${c.detail ? `: ${c.detail}` : ""}`),
|
|
729
|
+
};
|
|
590
730
|
}
|
|
591
731
|
if (name === "prism_lease_and_run" || name === "prism_lease") {
|
|
592
732
|
if (name === "prism_lease_and_run") requireCommand(args.command);
|
|
@@ -607,7 +747,16 @@ async function handle(name, args) {
|
|
|
607
747
|
const summary = {
|
|
608
748
|
lease_id: lease.leaseId,
|
|
609
749
|
funding_tx: lease.fundingHash,
|
|
610
|
-
|
|
750
|
+
// `prism_run` checks this itself. It is in the summary because the
|
|
751
|
+
// caller is being handed an address they may connect to by hand, and an
|
|
752
|
+
// address with no key to check is an invitation to accept whatever
|
|
753
|
+
// answers.
|
|
754
|
+
ssh: {
|
|
755
|
+
host: lease.access.ssh_host,
|
|
756
|
+
port: lease.access.ssh_port,
|
|
757
|
+
user: lease.access.ssh_user,
|
|
758
|
+
...hostKey(lease.access),
|
|
759
|
+
},
|
|
611
760
|
trust: lease.quote?.trust_class ?? "open",
|
|
612
761
|
expires_at: lease.access.expires_at,
|
|
613
762
|
};
|
|
@@ -699,7 +848,7 @@ async function handleVault(name, args) {
|
|
|
699
848
|
throw new Error(`unknown tool ${name}`);
|
|
700
849
|
}
|
|
701
850
|
|
|
702
|
-
const server = new Server({ name: "prism", version: "0.
|
|
851
|
+
const server = new Server({ name: "prism", version: "0.9.0" }, { capabilities: { tools: {} } });
|
|
703
852
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
704
853
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
705
854
|
try {
|