@prismnetwork/agent-sdk
Advanced tools
+84
| export declare const robinhoodChain: unknown; | ||
| export declare const USDG: string; | ||
| export declare const DEFAULT_IMAGE: string; | ||
| export declare const TRUST_CLASSES: readonly ["open", "isolated", "attested", "confidential"]; | ||
| export interface LeaseAccess { | ||
| mode?: string; | ||
| ssh_host?: string; | ||
| ssh_port?: number; | ||
| ssh_user?: string; | ||
| expires_at?: string; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface LeaseHandle { | ||
| leaseId: number; | ||
| access: LeaseAccess; | ||
| keyPath: string; | ||
| keyDir: string; | ||
| publicKey: string; | ||
| fundingHash: string; | ||
| quote: Record<string, unknown>; | ||
| } | ||
| export interface BatchLeaseHandle { | ||
| leaseId: number; | ||
| result: { exit_code?: number; stdout?: string; stderr?: string; truncated?: boolean }; | ||
| fundingHash: string; | ||
| quote: Record<string, unknown>; | ||
| } | ||
| export interface RunResult { | ||
| code: number; | ||
| stdout: string; | ||
| stderr: string; | ||
| timedOut: boolean; | ||
| } | ||
| export declare class PrismAgent { | ||
| constructor(options: { privateKey: string; escrow: string; apiBase?: string; rpcUrl?: string }); | ||
| readonly address: string; | ||
| readonly vault: unknown; | ||
| readonly workspace: unknown; | ||
| authenticate(): Promise<{ session: string }>; | ||
| offers(options?: { minTrust?: string }): Promise<Array<Record<string, unknown>>>; | ||
| balances(): Promise<{ address: string; usdg: string; eth: string }>; | ||
| transferUsdg(to: string, amountMicros: number | string | bigint): Promise<string>; | ||
| quote(options: { | ||
| image: string; | ||
| durationSeconds: number; | ||
| minVramMib?: number; | ||
| preferredNodeId?: string | null; | ||
| minTrustClass?: string; | ||
| command?: string | null; | ||
| }): Promise<Record<string, unknown>>; | ||
| fund(quote: Record<string, unknown>): Promise<{ hash: string; clientReference: string }>; | ||
| confirm(options: { quoteId: string; transactionHash: string; sshAuthorizedKey: string }): Promise<Record<string, unknown>>; | ||
| leases(): Promise<Array<Record<string, unknown>>>; | ||
| access(leaseId: number): Promise<LeaseAccess>; | ||
| result(leaseId: number): Promise<Record<string, unknown>>; | ||
| waitForResult(leaseId: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise<Record<string, unknown>>; | ||
| waitForAccess(leaseId: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise<LeaseAccess>; | ||
| lease(options: { | ||
| image: string; | ||
| durationSeconds: number; | ||
| minVramMib?: number; | ||
| preferredNodeId?: string | null; | ||
| maxDeposit?: number | string | bigint | null; | ||
| minTrustClass?: string; | ||
| command?: string | null; | ||
| }): Promise<LeaseHandle | BatchLeaseHandle>; | ||
| run( | ||
| lease: LeaseHandle, | ||
| command: string, | ||
| options?: { timeoutMs?: number; connectRetries?: number; connectDelayMs?: number; stdin?: string | null }, | ||
| ): Promise<RunResult>; | ||
| endLease(lease: LeaseHandle): void; | ||
| } | ||
| export declare class PrismError extends Error { | ||
| readonly status: number; | ||
| readonly code: string; | ||
| readonly body: Record<string, unknown> | null | undefined; | ||
| } |
| import type { PrismAgent } from "./prism.mjs"; | ||
| export declare const DEFAULT_ESCROW: string; | ||
| export declare const PUBLIC_API: string; | ||
| export declare const NO_WALLET: string; | ||
| export declare function isRefusal(body: string): boolean; | ||
| export declare function agentFromEnv( | ||
| get?: (name: string) => string | undefined, | ||
| ): PrismAgent | null; | ||
| export interface LeaseAndRunOptions { | ||
| command: string; | ||
| durationSeconds?: number; | ||
| minVramMib?: number; | ||
| image?: string; | ||
| maxUsdg?: number; | ||
| minTrustClass?: "open" | "isolated" | "attested" | "confidential"; | ||
| } | ||
| export declare class PrismToolset { | ||
| constructor(options?: { agent?: PrismAgent | null; publicApi?: string }); | ||
| readonly agent: PrismAgent | null; | ||
| wallet(): Promise<string>; | ||
| listGpus(minTrustClass?: string): Promise<string>; | ||
| leaseAndRun(options: LeaseAndRunOptions): Promise<string>; | ||
| run(leaseId: number, command: string): Promise<string>; | ||
| endLease(leaseId: number): string; | ||
| } |
+13
-4
| { | ||
| "name": "@prismnetwork/agent-sdk", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.", | ||
@@ -8,4 +8,10 @@ "type": "module", | ||
| "exports": { | ||
| ".": "./prism.mjs", | ||
| "./toolset": "./toolset.mjs", | ||
| ".": { | ||
| "types": "./prism.d.mts", | ||
| "default": "./prism.mjs" | ||
| }, | ||
| "./toolset": { | ||
| "types": "./toolset.d.mts", | ||
| "default": "./toolset.mjs" | ||
| }, | ||
| "./vault": { | ||
@@ -22,2 +28,4 @@ "types": "./vault.d.ts", | ||
| "prism.mjs", | ||
| "toolset.d.mts", | ||
| "prism.d.mts", | ||
| "toolset.mjs", | ||
@@ -57,3 +65,4 @@ "vault.mjs", | ||
| "access": "public" | ||
| } | ||
| }, | ||
| "types": "prism.d.mts" | ||
| } |
+93
-16
@@ -61,3 +61,3 @@ // Prism Network agent SDK: headless GPU leasing for wallet-holding agents. | ||
| // that cannot run is rejected here rather than after an escrow is funded. | ||
| const MAX_COMMAND_BYTES = 8 * 1024; | ||
| export const MAX_COMMAND_BYTES = 8 * 1024; | ||
@@ -111,5 +111,17 @@ function assertCommand(value) { | ||
| if (!escrow) throw new Error("escrow address is required"); | ||
| if (typeof privateKey !== "string" || privateKey.trim() === "") { | ||
| throw new Error( | ||
| "privateKey is required: a 32-byte hex key, with or without 0x (most surfaces read it from PRISM_AGENT_KEY)", | ||
| ); | ||
| } | ||
| this.apiBase = apiBase.replace(/\/$/, ""); | ||
| this.escrow = escrow; | ||
| this.account = privateKeyToAccount(privateKey); | ||
| const trimmed = privateKey.trim(); | ||
| try { | ||
| this.account = privateKeyToAccount(trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`); | ||
| } catch (err) { | ||
| throw new Error( | ||
| `privateKey is not a valid key: ${err?.message ?? err}. Expected 32 bytes of hex, with or without the 0x prefix.`, | ||
| ); | ||
| } | ||
| const transport = http(rpcUrl ?? robinhoodChain.rpcUrls.default.http[0]); | ||
@@ -269,11 +281,41 @@ this.publicClient = createPublicClient({ chain: robinhoodChain, transport }); | ||
| const deadline = Date.now() + timeoutMs; | ||
| let polls = 0; | ||
| while (Date.now() < deadline) { | ||
| const res = await this.#proxy("GET", ["leases", String(leaseId), "result"], { raw: true }); | ||
| if (res.status === 200) return res.body; | ||
| if (res.status !== 404) throw new PrismError(res.status, res.body?.code ?? "result_failed", res.body); | ||
| await new Promise((resolve) => setTimeout(resolve, intervalMs)); | ||
| // The control plane keeps answering 404 for a batch whose node died | ||
| // without reporting, so check the lease state occasionally and stop | ||
| // waiting once it is terminal. 429 and 5xx are transient; aborting a | ||
| // paid wait on one would strand the deposit. | ||
| if (res.status === 404) { | ||
| polls += 1; | ||
| if (polls % 6 === 0) { | ||
| const state = await this.#terminalState(leaseId); | ||
| if (state) { | ||
| const again = await this.#proxy("GET", ["leases", String(leaseId), "result"], { raw: true }); | ||
| if (again.status === 200) return again.body; | ||
| throw new PrismError(502, "batch_no_result", { lease_id: leaseId, state }); | ||
| } | ||
| } | ||
| } else if (res.status !== 429 && res.status < 500) { | ||
| throw new PrismError(res.status, res.body?.code ?? "result_failed", res.body); | ||
| } | ||
| await sleep(intervalMs); | ||
| } | ||
| throw new PrismError(408, "result_timeout"); | ||
| throw new PrismError(408, "result_timeout", { lease_id: leaseId }); | ||
| } | ||
| async #terminalState(leaseId) { | ||
| let leases; | ||
| try { | ||
| leases = await this.leases(); | ||
| } catch { | ||
| return null; | ||
| } | ||
| const record = Array.isArray(leases) ? leases.find((l) => l.lease_id === leaseId) : null; | ||
| const state = record?.state ?? ""; | ||
| const terminal = ["closing", "settlement_pending", "finalized", "refunded", "failed"]; | ||
| return terminal.includes(state) ? state : null; | ||
| } | ||
| async waitForAccess(leaseId, { timeoutMs = 600_000, intervalMs = 10_000 } = {}) { | ||
@@ -287,6 +329,8 @@ const deadline = Date.now() + timeoutMs; | ||
| } | ||
| if (res.status !== 404) throw new PrismError(res.status, res.body?.error ?? "access_error"); | ||
| if (res.status !== 404 && res.status !== 429 && res.status < 500) { | ||
| throw new PrismError(res.status, res.body?.error ?? "access_error", res.body); | ||
| } | ||
| await sleep(intervalMs); | ||
| } | ||
| throw new PrismError(408, "access_timeout"); | ||
| throw new PrismError(408, "access_timeout", { lease_id: leaseId }); | ||
| } | ||
@@ -305,2 +349,14 @@ | ||
| if (!this.session) await this.authenticate(); | ||
| // A wallet with no balance at all cannot fund anything, and a doomed quote | ||
| // still holds capacity against other renters until it expires. Refuse | ||
| // before quoting. | ||
| const balances = await this.balances(); | ||
| if (balances.usdg === "0" || balances.eth === "0") { | ||
| throw new PrismError(402, "wallet_unfunded", { | ||
| address: this.address, | ||
| usdg: balances.usdg, | ||
| eth_wei: balances.eth, | ||
| hint: "the wallet needs USDG for the deposit and native ETH for gas on Robinhood Chain (id 4663) before it can lease", | ||
| }); | ||
| } | ||
| const quote = await this.quote({ | ||
@@ -318,4 +374,6 @@ image, | ||
| const key = this.#generateSshKey(); | ||
| let funded = null; | ||
| let leaseId = null; | ||
| try { | ||
| const funded = await this.fund(quote); | ||
| funded = await this.fund(quote); | ||
| const record = await this.confirm({ | ||
@@ -326,3 +384,6 @@ quoteId: quote.quote_id, | ||
| }); | ||
| if (!Number.isInteger(record?.lease_id)) throw new PrismError(502, "malformed_lease_record"); | ||
| if (!Number.isInteger(record?.lease_id)) { | ||
| throw new PrismError(502, "malformed_lease_record", { funding_hash: funded.hash }); | ||
| } | ||
| leaseId = record.lease_id; | ||
| // A batch lease never hands out access, so waiting for it would block | ||
@@ -332,9 +393,9 @@ // until the timeout and then report a failure that never happened. Wait | ||
| if (command !== null) { | ||
| const result = await this.waitForResult(record.lease_id); | ||
| const result = await this.waitForResult(leaseId, { timeoutMs: durationSeconds * 1000 + 900_000 }); | ||
| rmSync(key.dir, { recursive: true, force: true }); | ||
| return { leaseId: record.lease_id, result, fundingHash: funded.hash, quote }; | ||
| return { leaseId, result, fundingHash: funded.hash, quote }; | ||
| } | ||
| const access = await this.waitForAccess(record.lease_id); | ||
| const access = await this.waitForAccess(leaseId); | ||
| return { | ||
| leaseId: record.lease_id, | ||
| leaseId, | ||
| access, | ||
@@ -348,4 +409,15 @@ keyPath: key.keyPath, | ||
| } catch (err) { | ||
| rmSync(key.dir, { recursive: true, force: true }); | ||
| throw err; | ||
| // Before funding, the key opens nothing; discard it. After funding it is | ||
| // the only way into a machine that is being paid for, so it stays on | ||
| // disk and the error says where everything is. | ||
| if (funded === null) { | ||
| rmSync(key.dir, { recursive: true, force: true }); | ||
| throw err; | ||
| } | ||
| const detail = { funding_hash: funded.hash, lease_id: leaseId, key_path: key.keyPath }; | ||
| if (err instanceof PrismError) { | ||
| err.body = { ...(err.body ?? {}), ...detail }; | ||
| throw err; | ||
| } | ||
| throw new PrismError(502, "lease_failed_after_funding", { ...detail, cause: err?.message ?? String(err) }); | ||
| } | ||
@@ -360,3 +432,7 @@ } | ||
| if (!lease?.access?.ssh_host || !lease.access.ssh_port || !lease.keyPath) { | ||
| throw new PrismError(400, "invalid_lease_handle"); | ||
| throw new PrismError(400, "invalid_lease_handle", { | ||
| mode: lease?.access?.mode ?? null, | ||
| lease_id: lease?.leaseId ?? null, | ||
| hint: "gateway-mode access has no ssh endpoint", | ||
| }); | ||
| } | ||
@@ -488,2 +564,3 @@ if (typeof command !== "string" || command.length === 0) throw new PrismError(400, "command_required"); | ||
| super(`prism ${status}: ${code}`); | ||
| this.name = "PrismError"; | ||
| this.status = status; | ||
@@ -490,0 +567,0 @@ this.code = code; |
+11
-9
@@ -19,3 +19,3 @@ # @prismnetwork/agent-sdk | ||
| const agent = new PrismAgent({ | ||
| privateKey: process.env.AGENT_KEY, // agent's wallet | ||
| privateKey: process.env.PRISM_AGENT_KEY, // agent's wallet | ||
| escrow: "0x62C042265991bEa17B07229322A01850974626dA", | ||
@@ -36,8 +36,10 @@ }); | ||
| `@prismnetwork/agent-sdk/toolset` exports `PrismToolset`, the framework-neutral | ||
| tool surface the MCP server and the framework plugins (elizaOS, Virtuals GAME) | ||
| wrap: `wallet`, `listGpus`, `leaseAndRun`, `run`, `endLease`, each returning a | ||
| human-readable string. It holds the wallet, the open leases and the per-lease | ||
| spending cap in one place, reads `PRISM_AGENT_KEY`/`PRISM_ESCROW` from the | ||
| environment by default, and answers the read-only questions from the public API | ||
| when no wallet is configured. | ||
| tool surface the framework plugins (elizaOS, Virtuals GAME) wrap: `wallet`, | ||
| `listGpus`, `leaseAndRun`, `run`, `endLease`, each resolving to a | ||
| human-readable string, including on failure. It holds the wallet, the open | ||
| leases and the per-lease spending cap in one place, reads `PRISM_AGENT_KEY`, | ||
| `PRISM_ESCROW`, `PRISM_API_BASE` and `PRISM_RPC_URL` from the environment by | ||
| default (`agentFromEnv` accepts a getter for hosts with their own settings | ||
| store), and answers the read-only questions from the public API when no wallet | ||
| is configured. | ||
@@ -60,3 +62,3 @@ ## Vault | ||
| ECDSA is deterministic, so the same wallet reproduces the same vault on any | ||
| machine — no recovery copy is held anywhere. Pass `{ passphrase }` to require a | ||
| machine; no recovery copy is held anywhere. Pass `{ passphrase }` to require a | ||
| second factor beyond the wallet. | ||
@@ -154,2 +156,2 @@ | ||
| See `example.mjs` for a full run. | ||
| See [example.mjs](https://github.com/prismnetwork-tech/prism/blob/main/sdk/example.mjs) for a full run. |
+153
-38
| // A framework-neutral tool surface over PrismAgent. Agent frameworks disagree | ||
| // about how a tool is declared but agree about what one is: a named function | ||
| // with typed arguments that returns text. PrismToolset holds the wallet, the | ||
| // open leases, and the spending cap in one place so framework plugins stay | ||
| // thin wrappers instead of diverging copies of the same logic. | ||
| // open leases, and the per-lease spending cap in one place so framework | ||
| // plugins stay thin wrappers instead of diverging copies of the same logic. | ||
| // | ||
| // Without a wallet it still answers the read-only questions (capacity, prices) | ||
| // from the public API, the same degradation the MCP server offers. | ||
| import { DEFAULT_IMAGE, PrismAgent, TRUST_CLASSES } from "./prism.mjs"; | ||
| // Every method resolves to a string, including on failure. These tools are | ||
| // driven by language models, and a model can act on "the wallet holds 0 USDG" | ||
| // where a stack trace ends the conversation. Without a wallet the read-only | ||
| // questions still answer from the public API, the same degradation the MCP | ||
| // server offers. | ||
| import { rmSync } from "node:fs"; | ||
| import { DEFAULT_IMAGE, MAX_COMMAND_BYTES, PrismAgent, PrismError, TRUST_CLASSES } from "./prism.mjs"; | ||
@@ -14,14 +18,60 @@ export const DEFAULT_ESCROW = "0x62C042265991bEa17B07229322A01850974626dA"; | ||
| export const NO_WALLET = | ||
| "No wallet is configured, so this needs PRISM_AGENT_KEY (a funded wallet on Robinhood Chain). " + | ||
| "Looking at capacity and prices works without one."; | ||
| const MICROS = 1_000_000; | ||
| const TRUST_MESSAGE = `min_trust_class must be one of ${TRUST_CLASSES.join(", ")}.`; | ||
| const COMMAND_MESSAGE = "command is required: the shell command to run on the GPU, e.g. 'nvidia-smi'."; | ||
| const usdg = (micros) => `${(Number(micros) / MICROS).toFixed(6)} USDG`; | ||
| export function agentFromEnv() { | ||
| const privateKey = process.env.PRISM_AGENT_KEY; | ||
| // True for any string the toolset returns to describe a refusal or failure. | ||
| // Framework plugins map these to their own failed-action shape instead of | ||
| // keeping divergent copies of the wording. | ||
| export function isRefusal(body) { | ||
| return ( | ||
| body === NO_WALLET || | ||
| body === TRUST_MESSAGE || | ||
| body === COMMAND_MESSAGE || | ||
| body.startsWith("No active lease") || | ||
| body.startsWith("The lease did not go through") || | ||
| body.startsWith("The balance check failed") || | ||
| body.startsWith("The command could not run") || | ||
| body.startsWith("Prism capacity") || | ||
| body.startsWith("command exceeds the") || | ||
| body.startsWith("lease_id must be") || | ||
| /^Lease \d+ is funded .* but the command could not run/.test(body) | ||
| ); | ||
| } | ||
| // `get` lets hosts with their own settings store (elizaOS runtimes, test | ||
| // harnesses) resolve the variables without mutating process.env. | ||
| export function agentFromEnv(get = (name) => process.env[name]) { | ||
| const privateKey = (get("PRISM_AGENT_KEY") ?? "").trim(); | ||
| if (!privateKey) return null; | ||
| return new PrismAgent({ privateKey, escrow: process.env.PRISM_ESCROW ?? DEFAULT_ESCROW }); | ||
| return new PrismAgent({ | ||
| privateKey, | ||
| escrow: get("PRISM_ESCROW") || DEFAULT_ESCROW, | ||
| apiBase: get("PRISM_API_BASE") || undefined, | ||
| rpcUrl: get("PRISM_RPC_URL") || undefined, | ||
| }); | ||
| } | ||
| const NO_WALLET = | ||
| "No wallet is configured, so this needs PRISM_AGENT_KEY (a funded wallet on Robinhood Chain). " + | ||
| "Looking at capacity and prices works without one."; | ||
| function describe(err) { | ||
| if (err instanceof PrismError) { | ||
| const body = err.body ?? {}; | ||
| if (err.code === "cost_exceeds_max") { | ||
| return `the quote needs ${usdg(body.required ?? 0)} but the cap is ${usdg(body.max ?? 0)}; raise maxUsdg or shorten the lease`; | ||
| } | ||
| if (err.code === "wallet_unfunded") { | ||
| return ( | ||
| `wallet ${body.address} holds ${usdg(body.usdg ?? 0)} and ${(Number(body.eth_wei ?? 0) / 1e18).toFixed(6)} ` + | ||
| "ETH for gas; fund it on Robinhood Chain (id 4663) before leasing" | ||
| ); | ||
| } | ||
| const detail = body.cause ?? body.hint ?? body.message; | ||
| return detail ? `${err.code} (${detail})` : err.code; | ||
| } | ||
| return err?.message ?? String(err); | ||
| } | ||
@@ -33,5 +83,14 @@ export class PrismToolset { | ||
| constructor({ agent, publicApi = PUBLIC_API } = {}) { | ||
| constructor({ agent, publicApi } = {}) { | ||
| this.#agent = agent === undefined ? agentFromEnv() : agent; | ||
| this.#publicApi = publicApi; | ||
| this.#publicApi = (publicApi ?? process.env.PRISM_PUBLIC_API ?? PUBLIC_API).replace(/\/$/, ""); | ||
| process.once("exit", () => { | ||
| for (const lease of this.#leases.values()) { | ||
| try { | ||
| rmSync(lease.keyDir, { recursive: true, force: true }); | ||
| } catch { | ||
| /* best effort */ | ||
| } | ||
| } | ||
| }); | ||
| } | ||
@@ -43,27 +102,54 @@ | ||
| #sweepExpired() { | ||
| const now = Date.now(); | ||
| for (const [id, lease] of this.#leases) { | ||
| const expiry = Date.parse(lease.access?.expires_at ?? ""); | ||
| if (Number.isFinite(expiry) && expiry < now) { | ||
| this.#agent.endLease(lease); | ||
| this.#leases.delete(id); | ||
| } | ||
| } | ||
| } | ||
| async wallet() { | ||
| if (!this.#agent) return NO_WALLET; | ||
| const b = await this.#agent.balances(); | ||
| let b; | ||
| try { | ||
| b = await this.#agent.balances(); | ||
| } catch (err) { | ||
| return `The balance check failed: ${describe(err)}`; | ||
| } | ||
| return `address: ${b.address}\nusdg: ${usdg(b.usdg)}\neth: ${(Number(b.eth) / 1e18).toFixed(6)} for gas`; | ||
| } | ||
| async listGpus(minTrust = "open") { | ||
| if (!TRUST_CLASSES.includes(minTrust)) { | ||
| return `min_trust must be one of ${TRUST_CLASSES.join(", ")}`; | ||
| } | ||
| async listGpus(minTrustClass = "open") { | ||
| if (!TRUST_CLASSES.includes(minTrustClass)) return TRUST_MESSAGE; | ||
| let offers; | ||
| if (this.#agent) { | ||
| offers = await this.#agent.offers({ minTrust }); | ||
| } else { | ||
| const url = new URL("/v1/offers", this.#publicApi); | ||
| url.searchParams.set("min_trust", minTrust); | ||
| const res = await fetch(url, { headers: { accept: "application/json" } }); | ||
| if (!res.ok) return `Prism capacity is unreachable right now (${res.status}).`; | ||
| offers = await res.json(); | ||
| try { | ||
| if (this.#agent) { | ||
| offers = await this.#agent.offers({ minTrust: minTrustClass }); | ||
| } else { | ||
| const url = new URL("/v1/offers", this.#publicApi); | ||
| url.searchParams.set("min_trust", minTrustClass); | ||
| const res = await fetch(url, { | ||
| headers: { accept: "application/json" }, | ||
| signal: AbortSignal.timeout(10_000), | ||
| }); | ||
| if (!res.ok) return `Prism capacity is unreachable right now (${res.status}).`; | ||
| offers = await res.json().catch(() => null); | ||
| } | ||
| } catch (err) { | ||
| return `Prism capacity is unreachable right now: ${describe(err)}`; | ||
| } | ||
| if (!offers.length) return "No GPUs are online to rent right now."; | ||
| if (!Array.isArray(offers)) { | ||
| return "Prism capacity answered in an unexpected shape; try again shortly."; | ||
| } | ||
| if (!offers.length) { | ||
| return `No GPUs at trust class '${minTrustClass}' or above are online right now.`; | ||
| } | ||
| return offers | ||
| .map((o) => { | ||
| const perHr = ((Number(o.rate_per_second) * 3600) / MICROS).toFixed(2); | ||
| return `${o.gpu.model} · ${o.gpu.vram_mib} MiB · $${perHr}/hr · ${o.trust_class ?? "open"}`; | ||
| const row = `${o.gpu?.model ?? "GPU"} · ${o.gpu?.vram_mib ?? "?"} MiB · ${perHr} USDG/hr · ${o.trust_class ?? "open"}`; | ||
| return o.staker_only ? `${row} · stakers only` : row; | ||
| }) | ||
@@ -80,13 +166,32 @@ .join("\n"); | ||
| minTrustClass = "open", | ||
| }) { | ||
| } = {}) { | ||
| if (!this.#agent) return NO_WALLET; | ||
| const lease = await this.#agent.lease({ | ||
| image, | ||
| durationSeconds, | ||
| minVramMib, | ||
| maxDeposit: Math.round(maxUsdg * MICROS), | ||
| minTrustClass, | ||
| }); | ||
| if (typeof command !== "string" || command.trim() === "") return COMMAND_MESSAGE; | ||
| if (Buffer.byteLength(command, "utf8") > MAX_COMMAND_BYTES) { | ||
| return `command exceeds the ${MAX_COMMAND_BYTES / 1024} KiB limit; fetch the payload on the box instead of inlining it.`; | ||
| } | ||
| if (!TRUST_CLASSES.includes(minTrustClass)) return TRUST_MESSAGE; | ||
| this.#sweepExpired(); | ||
| let lease; | ||
| try { | ||
| lease = await this.#agent.lease({ | ||
| image, | ||
| durationSeconds, | ||
| minVramMib, | ||
| maxDeposit: Math.round(maxUsdg * MICROS), | ||
| minTrustClass, | ||
| }); | ||
| } catch (err) { | ||
| return `The lease did not go through: ${describe(err)}`; | ||
| } | ||
| this.#leases.set(lease.leaseId, lease); | ||
| const res = await this.#agent.run(lease, command); | ||
| let res; | ||
| try { | ||
| res = await this.#agent.run(lease, command); | ||
| } catch (err) { | ||
| return ( | ||
| `Lease ${lease.leaseId} is funded (tx ${lease.fundingHash}) but the command could not run: ` + | ||
| `${describe(err)}. The lease stays open; try run(${lease.leaseId}, ...) or release it with endLease.` | ||
| ); | ||
| } | ||
| const out = res.stdout || res.stderr || ""; | ||
@@ -98,5 +203,13 @@ return `lease ${lease.leaseId} funded onchain (tx ${lease.fundingHash}), exit ${res.code}:\n${out}`; | ||
| if (!this.#agent) return NO_WALLET; | ||
| leaseId = Number(leaseId); | ||
| if (!Number.isInteger(leaseId) || leaseId <= 0) return "lease_id must be a positive integer."; | ||
| const lease = this.#leases.get(leaseId); | ||
| if (!lease) return `No active lease ${leaseId} in this session.`; | ||
| const res = await this.#agent.run(lease, command); | ||
| if (typeof command !== "string" || command.trim() === "") return COMMAND_MESSAGE; | ||
| let res; | ||
| try { | ||
| res = await this.#agent.run(lease, command); | ||
| } catch (err) { | ||
| return `The command could not run on lease ${leaseId}: ${describe(err)}`; | ||
| } | ||
| return `exit ${res.code}:\n${res.stdout || res.stderr || ""}`; | ||
@@ -107,2 +220,4 @@ } | ||
| if (!this.#agent) return NO_WALLET; | ||
| leaseId = Number(leaseId); | ||
| if (!Number.isInteger(leaseId) || leaseId <= 0) return "lease_id must be a positive integer."; | ||
| const lease = this.#leases.get(leaseId); | ||
@@ -109,0 +224,0 @@ if (!lease) return `No active lease ${leaseId} in this session.`; |
+1
-1
@@ -68,3 +68,3 @@ // Renter-held storage. Everything in this file runs on the renter's machine: | ||
| /// The wallet address, lowercased. Casing varies by source — a checksummed | ||
| /// The wallet address, lowercased. Casing varies by source: a checksummed | ||
| /// address from one wallet and a lowercase one from another must not derive | ||
@@ -71,0 +71,0 @@ /// two different keys for the same vault. |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
78538
18.09%10
25%1615
13.17%154
1.32%14
7.69%