@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 +6 -4
- package/package.json +1 -1
- package/prism.d.mts +15 -1
- package/prism.mjs +64 -21
- package/toolset.d.mts +1 -1
- package/toolset.mjs +6 -3
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/
|
|
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/
|
|
203
|
+
See [example.mjs](https://github.com/prismnetwork-tech/prism/blob/main/sdk/example.mjs) for a full run.
|
package/package.json
CHANGED
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
|
-
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
326
|
-
|
|
327
|
-
|
|
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(
|
|
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
|
|
593
|
-
|
|
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(
|
|
681
|
-
|
|
682
|
-
|
|
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(
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
}
|