@prismnetwork/agent-sdk 0.7.6 → 0.7.8

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
@@ -24,7 +24,7 @@ await agent.authenticate();
24
24
  const lease = await agent.lease({ image: DEFAULT_IMAGE, durationSeconds: 900, minVramMib: 16000 });
25
25
  const out = await agent.run(lease, "nvidia-smi");
26
26
  console.log(out.stdout);
27
- agent.endLease(lease);
27
+ await agent.endLease(lease);
28
28
  ```
29
29
 
30
30
  `image` must be an immutable digest-pinned reference (`repo@sha256:...`). `DEFAULT_IMAGE` is one; a plain tag is rejected.
@@ -156,7 +156,7 @@ it.
156
156
  The chain walk runs in our control plane, so `attested` says we checked the
157
157
  report. The SDK holds the session to the fingerprint we published and never sees
158
158
  the report itself, which leaves us in the set you are trusting.
159
- [ATTESTATION.md](https://github.com/winter0x/prism/blob/main/docs/ATTESTATION.md)
159
+ [ATTESTATION.md](https://github.com/prismnetwork-tech/prism/blob/main/docs/ATTESTATION.md)
160
160
  says what the report covers and what it does not.
161
161
 
162
162
  `reported` means the node named the key on the signed report that opened access.
@@ -170,7 +170,9 @@ it is seen and held for the rest of the lease, which catches a machine swapped i
170
170
  partway through and cannot catch one that was wrong from the start.
171
171
 
172
172
  Nothing is written to your own `~/.ssh/known_hosts`. The record sits beside the
173
- lease's private key and is removed with it by `endLease()`.
173
+ lease's private key and is removed with it by `endLease()`, which also releases
174
+ the lease on the network so billing stops there instead of at the end of the
175
+ window.
174
176
 
175
177
  To refuse the third case outright:
176
178
 
@@ -198,4 +200,4 @@ The wallet needs two balances on Robinhood Chain (id 4663): USDG (`0x5fc5360D040
198
200
  Node >= 20, `viem` ^2 (peer), and `ssh`, `ssh-keygen` and `ssh-keyscan` on PATH
199
201
  for `run()` and for workspace save and restore.
200
202
 
201
- See [example.mjs](https://github.com/winter0x/prism/blob/main/sdk/example.mjs) for a full run.
203
+ See [example.mjs](https://github.com/prismnetwork-tech/prism/blob/main/sdk/example.mjs) for a full run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismnetwork/agent-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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",
package/prism.d.mts CHANGED
@@ -67,6 +67,13 @@ export interface RelayForwarder {
67
67
  close(): Promise<void>;
68
68
  }
69
69
 
70
+ export interface LeaseRelease {
71
+ lease_id: number | null;
72
+ state: string | null;
73
+ release: "queued" | "already_closed" | "failed";
74
+ error?: string;
75
+ }
76
+
70
77
  export interface LeaseHandle {
71
78
  leaseId: number;
72
79
  access: LeaseAccess;
@@ -169,7 +176,14 @@ export declare class PrismAgent {
169
176
  /// Only for a lease reached through the gateway. Use it for anything that is
170
177
  /// not a one-shot command: scp, a notebook client, an interactive shell.
171
178
  forward(lease: LeaseHandle, options?: { service?: "ssh" | "jupyter" }): Promise<RelayForwarder>;
172
- endLease(lease: LeaseHandle): void;
179
+ /// Releases the lease on the network and removes its key material. This is
180
+ /// what stops the meter: settlement charges the seconds the lease was open and
181
+ /// returns the rest of the deposit. Never rejects; `release` is "queued",
182
+ /// "already_closed" or "failed" (with `error`), and a failed release means the
183
+ /// machine is still billing until its window ends.
184
+ endLease(lease: LeaseHandle): Promise<LeaseRelease>;
185
+ /// Releases a lease by id. Rejects when the network refuses.
186
+ release(leaseId: number): Promise<{ lease_id: number; state: string; release: "queued" | "already_closed" }>;
173
187
  /// Pay for one call to a metered endpoint, keeping the payment until the
174
188
  /// endpoint serves. Bytes are sent verbatim. Pass `seal` instead of `body` for
175
189
  /// a request that has to be rebuilt per attempt, with `fingerprint` so the
package/prism.mjs CHANGED
@@ -239,10 +239,12 @@ export class PrismAgent {
239
239
  // The hash travels with the failure because it is the only thing that
240
240
  // says the money left this wallet, and whatever is counting the day's
241
241
  // spend has to be able to tell the two apart.
242
- throw new PrismError(502, "chain_error", {
243
- cause: err?.shortMessage ?? err?.message ?? String(err),
244
- ...(broadcast ? { payment_tx: broadcast } : {}),
245
- });
242
+ throw new PrismError(
243
+ 502,
244
+ "chain_error",
245
+ { cause: err?.shortMessage ?? err?.message ?? String(err), ...(broadcast ? { payment_tx: broadcast } : {}) },
246
+ broadcast,
247
+ );
246
248
  }
247
249
  }
248
250
 
@@ -322,10 +324,12 @@ export class PrismAgent {
322
324
  // The deposit is in the escrow the moment the chain accepts this, and
323
325
  // waiting for confirmations is where a flaky rpc gives up. Losing the
324
326
  // hash here would leave a funded lease nobody can name.
325
- throw new PrismError(502, "chain_error", {
326
- cause: err?.shortMessage ?? err?.message ?? String(err),
327
- ...(broadcast ? { funding_hash: broadcast } : {}),
328
- });
327
+ throw new PrismError(
328
+ 502,
329
+ "chain_error",
330
+ { cause: err?.shortMessage ?? err?.message ?? String(err), ...(broadcast ? { funding_hash: broadcast } : {}) },
331
+ broadcast,
332
+ );
329
333
  }
330
334
  }
331
335
 
@@ -498,9 +502,15 @@ export class PrismAgent {
498
502
  const detail = { funding_hash: funded.hash, lease_id: leaseId, key_path: key.keyPath };
499
503
  if (err instanceof PrismError) {
500
504
  err.body = { ...(err.body ?? {}), ...detail };
505
+ err.broadcast = funded.hash;
501
506
  throw err;
502
507
  }
503
- throw new PrismError(502, "lease_failed_after_funding", { ...detail, cause: err?.message ?? String(err) });
508
+ throw new PrismError(
509
+ 502,
510
+ "lease_failed_after_funding",
511
+ { ...detail, cause: err?.message ?? String(err) },
512
+ funded.hash,
513
+ );
504
514
  }
505
515
  }
506
516
 
@@ -589,8 +599,20 @@ export class PrismAgent {
589
599
  return openRelayForwarder(lease.access, { service });
590
600
  }
591
601
 
592
- // Releases local key material. The on-chain lease settles at the end of its duration.
593
- endLease(lease) {
602
+ // Releases the lease on the network, which is what stops the meter: settlement
603
+ // charges the seconds between access opening and this call and returns the
604
+ // rest of the deposit. Key material goes either way. Never rejects, because
605
+ // most callers fire it from cleanup paths; a refused release comes back in
606
+ // the result so a caller that cares can tell the operator the meter is still
607
+ // running.
608
+ // Releases a lease by id, for one this process holds no handle to (the id a
609
+ // failed fund names, a lease listed by leases()). Rejects when the network
610
+ // refuses, because the wallet is still paying for the machine.
611
+ async release(leaseId) {
612
+ return this.#proxy("POST", ["leases", String(leaseId), "release"]);
613
+ }
614
+
615
+ async endLease(lease) {
594
616
  if (lease?.keyDir) {
595
617
  try {
596
618
  rmSync(lease.keyDir, { recursive: true, force: true });
@@ -598,6 +620,13 @@ export class PrismAgent {
598
620
  /* best effort */
599
621
  }
600
622
  }
623
+ if (lease?.leaseId === undefined || lease?.leaseId === null) return { lease_id: null, release: "failed", error: "no lease id" };
624
+ try {
625
+ const out = await this.release(lease.leaseId);
626
+ return { lease_id: Number(lease.leaseId), state: out?.state ?? null, release: out?.release ?? "queued" };
627
+ } catch (error) {
628
+ return { lease_id: Number(lease.leaseId), state: null, release: "failed", error: error?.message ?? String(error) };
629
+ }
601
630
  }
602
631
 
603
632
  // Unconsumed inference payments, keyed by the endpoint, the price and the
@@ -669,7 +698,7 @@ export class PrismAgent {
669
698
  });
670
699
  bytes = Buffer.from(await res.arrayBuffer());
671
700
  } catch (err) {
672
- throw new PrismError(504, "endpoint_unreachable", { cause: err?.message ?? String(err), ...kept });
701
+ throw new PrismError(504, "endpoint_unreachable", { cause: err?.message ?? String(err), ...kept }, tx);
673
702
  }
674
703
  if (res.status === 200) {
675
704
  this.#pendingPayments.delete(key);
@@ -677,10 +706,15 @@ export class PrismAgent {
677
706
  // already consumed. That is an answer to an earlier call, so it is not
678
707
  // this one's, whatever the status line says.
679
708
  if (String(res.headers.get("x-prism-replayed") ?? "").toLowerCase() === "true") {
680
- throw new PrismError(409, "payment_replayed", {
681
- cause: `the endpoint replayed an earlier answer for tx ${tx}`,
682
- hint: "this payment was already consumed by another call; pay again to have this request served",
683
- });
709
+ throw new PrismError(
710
+ 409,
711
+ "payment_replayed",
712
+ {
713
+ cause: `the endpoint replayed an earlier answer for tx ${tx}`,
714
+ hint: "this payment was already consumed by another call; pay again to have this request served",
715
+ },
716
+ tx,
717
+ );
684
718
  }
685
719
  return { status: 200, headers: res.headers, bytes, tx, sent };
686
720
  }
@@ -700,10 +734,15 @@ export class PrismAgent {
700
734
  (res.status === 402 && ["insufficient_confirmations", "tx_not_found"].includes(answered?.error));
701
735
  if (!retryable || Date.now() > deadline) {
702
736
  const said = [answered?.detail, answered?.retry].filter(Boolean).join("; ");
703
- throw new PrismError(res.status, answered?.error ?? "generation_failed", {
704
- cause: said || answered?.error || `status ${res.status}`,
705
- ...(this.#pendingPayments.has(key) ? kept : { payment_tx: tx }),
706
- });
737
+ throw new PrismError(
738
+ res.status,
739
+ answered?.error ?? "generation_failed",
740
+ {
741
+ cause: said || answered?.error || `status ${res.status}`,
742
+ ...(this.#pendingPayments.has(key) ? kept : { payment_tx: tx }),
743
+ },
744
+ tx,
745
+ );
707
746
  }
708
747
  await sleep(retryDelayMs);
709
748
  if (seal) sent = seal();
@@ -1055,11 +1094,15 @@ export class PrismAgent {
1055
1094
  }
1056
1095
 
1057
1096
  export class PrismError extends Error {
1058
- constructor(status, code, body) {
1097
+ constructor(status, code, body, broadcast = null) {
1059
1098
  super(`prism ${status}: ${code}`);
1060
1099
  this.name = "PrismError";
1061
1100
  this.status = status;
1062
1101
  this.code = code;
1063
1102
  this.body = body;
1103
+ // The transaction this process put on the wire before the failure, when
1104
+ // there was one. `body` is whatever the far side sent back and can say
1105
+ // anything; this is the only field that says the money left this wallet.
1106
+ this.broadcast = broadcast;
1064
1107
  }
1065
1108
  }
package/toolset.d.mts CHANGED
@@ -26,5 +26,5 @@ export declare class PrismToolset {
26
26
  listGpus(minTrustClass?: string): Promise<string>;
27
27
  leaseAndRun(options: LeaseAndRunOptions): Promise<string>;
28
28
  run(leaseId: number, command: string): Promise<string>;
29
- endLease(leaseId: number): string;
29
+ endLease(leaseId: number): Promise<string>;
30
30
  }
package/toolset.mjs CHANGED
@@ -210,14 +210,17 @@ export class PrismToolset {
210
210
  return `exit ${res.code}:\n${res.stdout || res.stderr || ""}`;
211
211
  }
212
212
 
213
- endLease(leaseId) {
213
+ async endLease(leaseId) {
214
214
  if (!this.#agent) return NO_WALLET;
215
215
  leaseId = Number(leaseId);
216
216
  if (!Number.isInteger(leaseId) || leaseId <= 0) return "lease_id must be a positive integer.";
217
217
  const lease = this.#leases.get(leaseId);
218
218
  if (!lease) return `No active lease ${leaseId} in this session.`;
219
- this.#agent.endLease(lease);
220
219
  this.#leases.delete(leaseId);
221
- return `released lease ${leaseId}`;
220
+ const out = await this.#agent.endLease(lease);
221
+ if (out.release === "failed") {
222
+ return `Lease ${leaseId} could not be released: ${out.error}. Its access key is gone but the meter may still be running; check receipts for the settled charge.`;
223
+ }
224
+ return `released lease ${leaseId}; billing stopped here and the unused deposit returns after settlement`;
222
225
  }
223
226
  }