Sign In

@prismnetwork/agent-sdk

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@prismnetwork/agent-sdk - npm Package Compare versions

Comparing version
0.4.0
to
0.5.0
+84
prism.d.mts
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;

@@ -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.
// 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.`;

@@ -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.