@prismnetwork/agent-sdk 0.3.1 → 0.5.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 CHANGED
@@ -16,7 +16,7 @@ npm install @prismnetwork/agent-sdk viem
16
16
  import { PrismAgent, DEFAULT_IMAGE } from "@prismnetwork/agent-sdk";
17
17
 
18
18
  const agent = new PrismAgent({
19
- privateKey: process.env.AGENT_KEY, // agent's wallet
19
+ privateKey: process.env.PRISM_AGENT_KEY, // agent's wallet
20
20
  escrow: "0x62C042265991bEa17B07229322A01850974626dA",
21
21
  });
22
22
 
@@ -29,6 +29,18 @@ agent.endLease(lease);
29
29
 
30
30
  `image` must be an immutable digest-pinned reference (`repo@sha256:...`). `DEFAULT_IMAGE` is one; a plain tag is rejected.
31
31
 
32
+ ## Toolset
33
+
34
+ `@prismnetwork/agent-sdk/toolset` exports `PrismToolset`, the framework-neutral
35
+ tool surface the framework plugins (elizaOS, Virtuals GAME) wrap: `wallet`,
36
+ `listGpus`, `leaseAndRun`, `run`, `endLease`, each resolving to a
37
+ human-readable string, including on failure. It holds the wallet, the open
38
+ leases and the per-lease spending cap in one place, reads `PRISM_AGENT_KEY`,
39
+ `PRISM_ESCROW`, `PRISM_API_BASE` and `PRISM_RPC_URL` from the environment by
40
+ default (`agentFromEnv` accepts a getter for hosts with their own settings
41
+ store), and answers the read-only questions from the public API when no wallet
42
+ is configured.
43
+
32
44
  ## Vault
33
45
 
34
46
  Cards, identity documents, API credentials and recovery codes go in the vault
@@ -45,7 +57,7 @@ const value = await agent.vault.get(card.item_id, { json: true });
45
57
 
46
58
  `unlock()` derives the key from a signature over a fixed statement. Ethereum's
47
59
  ECDSA is deterministic, so the same wallet reproduces the same vault on any
48
- machine no recovery copy is held anywhere. Pass `{ passphrase }` to require a
60
+ machine; no recovery copy is held anywhere. Pass `{ passphrase }` to require a
49
61
  second factor beyond the wallet.
50
62
 
51
63
  Every item carries the weakest workspace trust class it may ever be released
@@ -65,6 +77,62 @@ ciphertext, so a service that moved an item between accounts, replayed an older
65
77
  version, or lowered its floor would produce a failed decrypt rather than a
66
78
  plausible wrong answer. See [docs/VAULT.md](../docs/VAULT.md).
67
79
 
80
+ ## Workspaces
81
+
82
+ A lease destroys its machine, so training output, checkpoints and a working
83
+ directory need somewhere that outlives it. A workspace is that place: the SDK
84
+ archives a directory off the leased box, seals it here under a key derived from
85
+ your wallet, and uploads the ciphertext straight to object storage. Prism
86
+ records the version, the size and the hash, and holds nothing that opens it.
87
+
88
+ ```js
89
+ await agent.workspace.unlock();
90
+
91
+ const ws = await agent.workspace.create("finetune-run");
92
+ const saved = await agent.workspace.save(lease, ws, "/root/out");
93
+
94
+ // On a later lease, onto a fresh machine.
95
+ await agent.workspace.restore(next, ws, "/root/out", { expectVersion: saved.version });
96
+ ```
97
+
98
+ The workspace key comes from a different statement and a different salt than
99
+ the vault's, so opening one does not open the other. Pass `{ passphrase }` to
100
+ require a second factor beyond the wallet.
101
+
102
+ A restore hashes the downloaded ciphertext and compares it to the digest
103
+ recorded at save time before it decrypts anything, so bytes altered in storage
104
+ are reported as tampering rather than as a wrong key. The account, workspace,
105
+ version and trust floor are authenticated into the ciphertext, so a snapshot
106
+ served for the wrong workspace, or under a floor that has been rewritten, fails
107
+ to open rather than returning a plausible wrong answer.
108
+
109
+ An older snapshot is a different case worth being precise about: its own
110
+ associated data is genuine for its own version, so it decrypts cleanly and
111
+ nothing in the ciphertext gives it away. A restore therefore compares the
112
+ version it was granted against the version the record says is current, and
113
+ refuses a rollback on that basis. Pass `expectVersion` to pin a specific one,
114
+ and `expectTrustClass` to refuse a floor that has moved.
115
+
116
+ A restore names the lease it is landing on, and Prism refuses to issue the
117
+ download at all when that lease's trust class is below the workspace's floor.
118
+ The check is server-side deliberately: a client-side one would be a courtesy
119
+ that a modified client could skip.
120
+
121
+ Bulk data never passes through Prism. Uploads and downloads use presigned URLs
122
+ that live fifteen minutes, and they are used from your process, never handed to
123
+ the leased machine. The machine only ever sees `tar` and `base64`, which is all
124
+ this needs from it.
125
+
126
+ Snapshots travel over the lease's SSH channel, which caps a single save at 64
127
+ MiB of archive; a larger directory is refused on the machine before anything is
128
+ transferred. New workspaces default to the `open` trust floor, unlike vault
129
+ items: their contents are the files you are already handing to a rented box.
130
+ Raise it at creation when they deserve more:
131
+
132
+ ```js
133
+ await agent.workspace.create("model-weights", { minTrustClass: "isolated" });
134
+ ```
135
+
68
136
  ## Auth
69
137
 
70
138
  `authenticate()` fetches a challenge (`GET /api/agent/challenge`), signs the message with the wallet, and exchanges it for a session (`POST /api/agent/session`). The session is a bearer token used on every `/api/agent/proxy/*` call. No shared secret, no cookie. The wallet is the identity (`subject = wallet:0x...`).
@@ -79,6 +147,7 @@ The wallet needs two balances on Robinhood Chain (id 4663): USDG (`0x5fc5360D040
79
147
 
80
148
  ## Requirements
81
149
 
82
- Node >= 20, `viem` ^2 (peer), and `ssh` + `ssh-keygen` on PATH for `run()`.
150
+ Node >= 20, `viem` ^2 (peer), and `ssh` + `ssh-keygen` on PATH for `run()` and
151
+ for workspace save and restore.
83
152
 
84
- See `example.mjs` for a full run.
153
+ See [example.mjs](https://github.com/prismnetwork-tech/prism/blob/main/sdk/example.mjs) for a full run.
package/package.json CHANGED
@@ -1,20 +1,36 @@
1
1
  {
2
2
  "name": "@prismnetwork/agent-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.",
5
5
  "type": "module",
6
6
  "main": "prism.mjs",
7
7
  "exports": {
8
- ".": "./prism.mjs",
8
+ ".": {
9
+ "types": "./prism.d.mts",
10
+ "default": "./prism.mjs"
11
+ },
12
+ "./toolset": {
13
+ "types": "./toolset.d.mts",
14
+ "default": "./toolset.mjs"
15
+ },
9
16
  "./vault": {
10
17
  "types": "./vault.d.ts",
11
18
  "default": "./vault.mjs"
19
+ },
20
+ "./workspace": {
21
+ "types": "./workspace.d.ts",
22
+ "default": "./workspace.mjs"
12
23
  }
13
24
  },
14
25
  "files": [
15
26
  "prism.mjs",
27
+ "toolset.d.mts",
28
+ "prism.d.mts",
29
+ "toolset.mjs",
16
30
  "vault.mjs",
17
31
  "vault.d.ts",
32
+ "workspace.mjs",
33
+ "workspace.d.ts",
18
34
  "README.md"
19
35
  ],
20
36
  "engines": {
@@ -44,5 +60,6 @@
44
60
  "license": "Apache-2.0",
45
61
  "publishConfig": {
46
62
  "access": "public"
47
- }
63
+ },
64
+ "types": "prism.d.mts"
48
65
  }
package/prism.d.mts ADDED
@@ -0,0 +1,84 @@
1
+ export declare const robinhoodChain: unknown;
2
+ export declare const USDG: string;
3
+ export declare const DEFAULT_IMAGE: string;
4
+ export declare const TRUST_CLASSES: readonly ["open", "isolated", "attested", "confidential"];
5
+
6
+ export interface LeaseAccess {
7
+ mode?: string;
8
+ ssh_host?: string;
9
+ ssh_port?: number;
10
+ ssh_user?: string;
11
+ expires_at?: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export interface LeaseHandle {
16
+ leaseId: number;
17
+ access: LeaseAccess;
18
+ keyPath: string;
19
+ keyDir: string;
20
+ publicKey: string;
21
+ fundingHash: string;
22
+ quote: Record<string, unknown>;
23
+ }
24
+
25
+ export interface BatchLeaseHandle {
26
+ leaseId: number;
27
+ result: { exit_code?: number; stdout?: string; stderr?: string; truncated?: boolean };
28
+ fundingHash: string;
29
+ quote: Record<string, unknown>;
30
+ }
31
+
32
+ export interface RunResult {
33
+ code: number;
34
+ stdout: string;
35
+ stderr: string;
36
+ timedOut: boolean;
37
+ }
38
+
39
+ export declare class PrismAgent {
40
+ constructor(options: { privateKey: string; escrow: string; apiBase?: string; rpcUrl?: string });
41
+ readonly address: string;
42
+ readonly vault: unknown;
43
+ readonly workspace: unknown;
44
+ authenticate(): Promise<{ session: string }>;
45
+ offers(options?: { minTrust?: string }): Promise<Array<Record<string, unknown>>>;
46
+ balances(): Promise<{ address: string; usdg: string; eth: string }>;
47
+ transferUsdg(to: string, amountMicros: number | string | bigint): Promise<string>;
48
+ quote(options: {
49
+ image: string;
50
+ durationSeconds: number;
51
+ minVramMib?: number;
52
+ preferredNodeId?: string | null;
53
+ minTrustClass?: string;
54
+ command?: string | null;
55
+ }): Promise<Record<string, unknown>>;
56
+ fund(quote: Record<string, unknown>): Promise<{ hash: string; clientReference: string }>;
57
+ confirm(options: { quoteId: string; transactionHash: string; sshAuthorizedKey: string }): Promise<Record<string, unknown>>;
58
+ leases(): Promise<Array<Record<string, unknown>>>;
59
+ access(leaseId: number): Promise<LeaseAccess>;
60
+ result(leaseId: number): Promise<Record<string, unknown>>;
61
+ waitForResult(leaseId: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise<Record<string, unknown>>;
62
+ waitForAccess(leaseId: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise<LeaseAccess>;
63
+ lease(options: {
64
+ image: string;
65
+ durationSeconds: number;
66
+ minVramMib?: number;
67
+ preferredNodeId?: string | null;
68
+ maxDeposit?: number | string | bigint | null;
69
+ minTrustClass?: string;
70
+ command?: string | null;
71
+ }): Promise<LeaseHandle | BatchLeaseHandle>;
72
+ run(
73
+ lease: LeaseHandle,
74
+ command: string,
75
+ options?: { timeoutMs?: number; connectRetries?: number; connectDelayMs?: number; stdin?: string | null },
76
+ ): Promise<RunResult>;
77
+ endLease(lease: LeaseHandle): void;
78
+ }
79
+
80
+ export declare class PrismError extends Error {
81
+ readonly status: number;
82
+ readonly code: string;
83
+ readonly body: Record<string, unknown> | null | undefined;
84
+ }
package/prism.mjs CHANGED
@@ -15,8 +15,15 @@ import {
15
15
  } from "viem";
16
16
  import { privateKeyToAccount } from "viem/accounts";
17
17
  import { PrismVault } from "./vault.mjs";
18
+ import { PrismWorkspace } from "./workspace.mjs";
18
19
 
19
20
  export { PrismVault, VaultError, DEFAULT_TRUST_FLOOR, VAULT_KEY_STATEMENT } from "./vault.mjs";
21
+ export {
22
+ PrismWorkspace,
23
+ WorkspaceError,
24
+ DEFAULT_WORKSPACE_TRUST_FLOOR,
25
+ WORKSPACE_KEY_STATEMENT,
26
+ } from "./workspace.mjs";
20
27
 
21
28
  export const robinhoodChain = defineChain({
22
29
  id: 4663,
@@ -51,7 +58,7 @@ const escrowAbi = parseAbi([
51
58
 
52
59
  // Matches the limit the control plane and the node both enforce, so a command
53
60
  // that cannot run is rejected here rather than after an escrow is funded.
54
- const MAX_COMMAND_BYTES = 8 * 1024;
61
+ export const MAX_COMMAND_BYTES = 8 * 1024;
55
62
 
56
63
  function assertCommand(value) {
57
64
  if (typeof value !== "string" || value.trim() === "") {
@@ -100,22 +107,36 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
100
107
  export class PrismAgent {
101
108
  constructor({ privateKey, apiBase = "https://prismnetwork.tech", escrow, rpcUrl }) {
102
109
  if (!escrow) throw new Error("escrow address is required");
110
+ if (typeof privateKey !== "string" || privateKey.trim() === "") {
111
+ throw new Error(
112
+ "privateKey is required: a 32-byte hex key, with or without 0x (most surfaces read it from PRISM_AGENT_KEY)",
113
+ );
114
+ }
103
115
  this.apiBase = apiBase.replace(/\/$/, "");
104
116
  this.escrow = escrow;
105
- this.account = privateKeyToAccount(privateKey);
117
+ const trimmed = privateKey.trim();
118
+ try {
119
+ this.account = privateKeyToAccount(trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`);
120
+ } catch (err) {
121
+ throw new Error(
122
+ `privateKey is not a valid key: ${err?.message ?? err}. Expected 32 bytes of hex, with or without the 0x prefix.`,
123
+ );
124
+ }
106
125
  const transport = http(rpcUrl ?? robinhoodChain.rpcUrls.default.http[0]);
107
126
  this.publicClient = createPublicClient({ chain: robinhoodChain, transport });
108
127
  this.walletClient = createWalletClient({ account: this.account, chain: robinhoodChain, transport });
109
128
  this.session = null;
110
129
  this.vault = new PrismVault(this);
130
+ this.workspace = new PrismWorkspace(this);
111
131
  }
112
132
 
113
133
  get address() {
114
134
  return this.account.address;
115
135
  }
116
136
 
117
- // The vault key is derived from this signature on the caller's machine. It is
118
- // returned to the vault client and never sent anywhere.
137
+ // The vault and workspace keys are derived from this signature on the
138
+ // caller's machine. It is returned to the client that asked and never sent
139
+ // anywhere.
119
140
  async signVaultStatement(statement) {
120
141
  return this.account.signMessage({ message: statement });
121
142
  }
@@ -124,6 +145,10 @@ export class PrismAgent {
124
145
  return this.#proxy(method, ["vault", ...segments], { body });
125
146
  }
126
147
 
148
+ async workspaceRequest(method, segments, { body = null } = {}) {
149
+ return this.#proxy(method, ["workspaces", ...segments], { body });
150
+ }
151
+
127
152
  async authenticate() {
128
153
  const challenge = await this.#json(`/api/agent/challenge?address=${this.address}`);
129
154
  const signature = await this.account.signMessage({ message: challenge.message });
@@ -251,13 +276,43 @@ export class PrismAgent {
251
276
 
252
277
  async waitForResult(leaseId, { timeoutMs = 900_000, intervalMs = 10_000 } = {}) {
253
278
  const deadline = Date.now() + timeoutMs;
279
+ let polls = 0;
254
280
  while (Date.now() < deadline) {
255
281
  const res = await this.#proxy("GET", ["leases", String(leaseId), "result"], { raw: true });
256
282
  if (res.status === 200) return res.body;
257
- if (res.status !== 404) throw new PrismError(res.status, res.body?.code ?? "result_failed", res.body);
258
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
283
+ // The control plane keeps answering 404 for a batch whose node died
284
+ // without reporting, so check the lease state occasionally and stop
285
+ // waiting once it is terminal. 429 and 5xx are transient; aborting a
286
+ // paid wait on one would strand the deposit.
287
+ if (res.status === 404) {
288
+ polls += 1;
289
+ if (polls % 6 === 0) {
290
+ const state = await this.#terminalState(leaseId);
291
+ if (state) {
292
+ const again = await this.#proxy("GET", ["leases", String(leaseId), "result"], { raw: true });
293
+ if (again.status === 200) return again.body;
294
+ throw new PrismError(502, "batch_no_result", { lease_id: leaseId, state });
295
+ }
296
+ }
297
+ } else if (res.status !== 429 && res.status < 500) {
298
+ throw new PrismError(res.status, res.body?.code ?? "result_failed", res.body);
299
+ }
300
+ await sleep(intervalMs);
259
301
  }
260
- throw new PrismError(408, "result_timeout");
302
+ throw new PrismError(408, "result_timeout", { lease_id: leaseId });
303
+ }
304
+
305
+ async #terminalState(leaseId) {
306
+ let leases;
307
+ try {
308
+ leases = await this.leases();
309
+ } catch {
310
+ return null;
311
+ }
312
+ const record = Array.isArray(leases) ? leases.find((l) => l.lease_id === leaseId) : null;
313
+ const state = record?.state ?? "";
314
+ const terminal = ["closing", "settlement_pending", "finalized", "refunded", "failed"];
315
+ return terminal.includes(state) ? state : null;
261
316
  }
262
317
 
263
318
  async waitForAccess(leaseId, { timeoutMs = 600_000, intervalMs = 10_000 } = {}) {
@@ -268,10 +323,12 @@ export class PrismAgent {
268
323
  if (!res.body?.ssh_host && res.body?.mode !== "gateway") throw new PrismError(502, "malformed_access");
269
324
  return res.body;
270
325
  }
271
- if (res.status !== 404) throw new PrismError(res.status, res.body?.error ?? "access_error");
326
+ if (res.status !== 404 && res.status !== 429 && res.status < 500) {
327
+ throw new PrismError(res.status, res.body?.error ?? "access_error", res.body);
328
+ }
272
329
  await sleep(intervalMs);
273
330
  }
274
- throw new PrismError(408, "access_timeout");
331
+ throw new PrismError(408, "access_timeout", { lease_id: leaseId });
275
332
  }
276
333
 
277
334
  // quote -> ssh keygen -> fund on-chain -> confirm -> wait for access.
@@ -285,6 +342,18 @@ export class PrismAgent {
285
342
  command = null,
286
343
  } = {}) {
287
344
  if (!this.session) await this.authenticate();
345
+ // A wallet with no balance at all cannot fund anything, and a doomed quote
346
+ // still holds capacity against other renters until it expires. Refuse
347
+ // before quoting.
348
+ const balances = await this.balances();
349
+ if (balances.usdg === "0" || balances.eth === "0") {
350
+ throw new PrismError(402, "wallet_unfunded", {
351
+ address: this.address,
352
+ usdg: balances.usdg,
353
+ eth_wei: balances.eth,
354
+ hint: "the wallet needs USDG for the deposit and native ETH for gas on Robinhood Chain (id 4663) before it can lease",
355
+ });
356
+ }
288
357
  const quote = await this.quote({
289
358
  image,
290
359
  durationSeconds,
@@ -297,25 +366,30 @@ export class PrismAgent {
297
366
  throw new PrismError(402, "cost_exceeds_max", { required: quote.maximum_escrow, max: String(maxDeposit) });
298
367
  }
299
368
  const key = this.#generateSshKey();
369
+ let funded = null;
370
+ let leaseId = null;
300
371
  try {
301
- const funded = await this.fund(quote);
372
+ funded = await this.fund(quote);
302
373
  const record = await this.confirm({
303
374
  quoteId: quote.quote_id,
304
375
  transactionHash: funded.hash,
305
376
  sshAuthorizedKey: key.publicKey,
306
377
  });
307
- if (!Number.isInteger(record?.lease_id)) throw new PrismError(502, "malformed_lease_record");
378
+ if (!Number.isInteger(record?.lease_id)) {
379
+ throw new PrismError(502, "malformed_lease_record", { funding_hash: funded.hash });
380
+ }
381
+ leaseId = record.lease_id;
308
382
  // A batch lease never hands out access, so waiting for it would block
309
383
  // until the timeout and then report a failure that never happened. Wait
310
384
  // for what the command printed instead.
311
385
  if (command !== null) {
312
- const result = await this.waitForResult(record.lease_id);
386
+ const result = await this.waitForResult(leaseId, { timeoutMs: durationSeconds * 1000 + 900_000 });
313
387
  rmSync(key.dir, { recursive: true, force: true });
314
- return { leaseId: record.lease_id, result, fundingHash: funded.hash, quote };
388
+ return { leaseId, result, fundingHash: funded.hash, quote };
315
389
  }
316
- const access = await this.waitForAccess(record.lease_id);
390
+ const access = await this.waitForAccess(leaseId);
317
391
  return {
318
- leaseId: record.lease_id,
392
+ leaseId,
319
393
  access,
320
394
  keyPath: key.keyPath,
321
395
  keyDir: key.dir,
@@ -324,17 +398,33 @@ export class PrismAgent {
324
398
  quote,
325
399
  };
326
400
  } catch (err) {
327
- rmSync(key.dir, { recursive: true, force: true });
328
- throw err;
401
+ // Before funding, the key opens nothing; discard it. After funding it is
402
+ // the only way into a machine that is being paid for, so it stays on
403
+ // disk and the error says where everything is.
404
+ if (funded === null) {
405
+ rmSync(key.dir, { recursive: true, force: true });
406
+ throw err;
407
+ }
408
+ const detail = { funding_hash: funded.hash, lease_id: leaseId, key_path: key.keyPath };
409
+ if (err instanceof PrismError) {
410
+ err.body = { ...(err.body ?? {}), ...detail };
411
+ throw err;
412
+ }
413
+ throw new PrismError(502, "lease_failed_after_funding", { ...detail, cause: err?.message ?? String(err) });
329
414
  }
330
415
  }
331
416
 
332
417
  // Run a command in the remote login shell over SSH (so pipes, redirects, and
333
418
  // $(...) all evaluate on the GPU). Retries through the host's sshd warmup, which
334
- // can lag a few minutes after the box reports ready.
335
- async run(lease, command, { timeoutMs = 120_000, connectRetries = 24, connectDelayMs = 10_000 } = {}) {
419
+ // can lag a few minutes after the box reports ready. `stdin` feeds the command
420
+ // its input, which keeps anything sensitive out of the remote process table.
421
+ async run(lease, command, { timeoutMs = 120_000, connectRetries = 24, connectDelayMs = 10_000, stdin = null } = {}) {
336
422
  if (!lease?.access?.ssh_host || !lease.access.ssh_port || !lease.keyPath) {
337
- throw new PrismError(400, "invalid_lease_handle");
423
+ throw new PrismError(400, "invalid_lease_handle", {
424
+ mode: lease?.access?.mode ?? null,
425
+ lease_id: lease?.leaseId ?? null,
426
+ hint: "gateway-mode access has no ssh endpoint",
427
+ });
338
428
  }
339
429
  if (typeof command !== "string" || command.length === 0) throw new PrismError(400, "command_required");
340
430
  const target = {
@@ -345,7 +435,7 @@ export class PrismAgent {
345
435
  };
346
436
  let last;
347
437
  for (let attempt = 0; attempt <= connectRetries; attempt++) {
348
- const res = await this.#ssh(target, command, timeoutMs);
438
+ const res = await this.#ssh(target, command, timeoutMs, stdin);
349
439
  if (!isSshWarmup(res)) return res;
350
440
  last = res;
351
441
  if (attempt < connectRetries) await sleep(connectDelayMs);
@@ -376,7 +466,7 @@ export class PrismAgent {
376
466
  }
377
467
  }
378
468
 
379
- #ssh(target, command, timeoutMs) {
469
+ #ssh(target, command, timeoutMs, stdin = null) {
380
470
  const args = [
381
471
  "-i", target.keyPath,
382
472
  "-p", String(target.port),
@@ -398,6 +488,12 @@ export class PrismAgent {
398
488
  }, timeoutMs);
399
489
  child.stdout.on("data", (d) => (stdout += d));
400
490
  child.stderr.on("data", (d) => (stderr += d));
491
+ if (stdin !== null) {
492
+ // A command that exits before reading its input closes the pipe, which
493
+ // is a normal end to the transfer and not a failure to report.
494
+ child.stdin.on("error", () => {});
495
+ child.stdin.end(stdin);
496
+ }
401
497
  child.on("close", (code) => {
402
498
  clearTimeout(timer);
403
499
  resolve({ code: code ?? -1, stdout: stdout.trim(), stderr: stderr.trim(), timedOut });
@@ -455,6 +551,7 @@ export class PrismAgent {
455
551
  export class PrismError extends Error {
456
552
  constructor(status, code, body) {
457
553
  super(`prism ${status}: ${code}`);
554
+ this.name = "PrismError";
458
555
  this.status = status;
459
556
  this.code = code;
460
557
  this.body = body;
package/toolset.d.mts ADDED
@@ -0,0 +1,30 @@
1
+ import type { PrismAgent } from "./prism.mjs";
2
+
3
+ export declare const DEFAULT_ESCROW: string;
4
+ export declare const PUBLIC_API: string;
5
+ export declare const NO_WALLET: string;
6
+
7
+ export declare function isRefusal(body: string): boolean;
8
+
9
+ export declare function agentFromEnv(
10
+ get?: (name: string) => string | undefined,
11
+ ): PrismAgent | null;
12
+
13
+ export interface LeaseAndRunOptions {
14
+ command: string;
15
+ durationSeconds?: number;
16
+ minVramMib?: number;
17
+ image?: string;
18
+ maxUsdg?: number;
19
+ minTrustClass?: "open" | "isolated" | "attested" | "confidential";
20
+ }
21
+
22
+ export declare class PrismToolset {
23
+ constructor(options?: { agent?: PrismAgent | null; publicApi?: string });
24
+ readonly agent: PrismAgent | null;
25
+ wallet(): Promise<string>;
26
+ listGpus(minTrustClass?: string): Promise<string>;
27
+ leaseAndRun(options: LeaseAndRunOptions): Promise<string>;
28
+ run(leaseId: number, command: string): Promise<string>;
29
+ endLease(leaseId: number): string;
30
+ }