@prismnetwork/agent-sdk
Advanced tools
+158
| // Checking which machine answered. | ||
| // | ||
| // A lease hands the renter an address and a private key. Until the host key on | ||
| // the other end is checked, anything that can reach that address can take the | ||
| // session, read the work and answer as if it were the GPU. What the network can | ||
| // say about that key differs by where the capacity came from, so the decision is | ||
| // made here rather than defaulted: | ||
| // | ||
| // - the grant names a fingerprint, so the key is checked before the session | ||
| // opens and a mismatch ends the attempt; | ||
| // - the grant names none, so the key is recorded the first time it is seen and | ||
| // held for the rest of the lease. That catches a substitution partway | ||
| // through and cannot catch one that was there from the start. | ||
| // | ||
| // The record lives beside the lease's private key and goes when the lease does. | ||
| // Nothing here touches the caller's own ~/.ssh/known_hosts. | ||
| import { execFile } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { readFileSync, writeFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| const SCAN_TIMEOUT_SECONDS = 10; | ||
| export class HostKeyError extends Error { | ||
| constructor(code, detail) { | ||
| super(code); | ||
| this.name = "HostKeyError"; | ||
| this.code = code; | ||
| this.detail = detail ?? null; | ||
| } | ||
| } | ||
| /// What the network is willing to say about the machine behind a grant, in the | ||
| /// terms a renter would use to decide whether to send it anything. | ||
| /// | ||
| /// `attested` is the only one that survives a hostile operator: the fingerprint | ||
| /// comes out of a report the processor signed. `reported` is the operator's word | ||
| /// under their bonded device key, which rules out everyone between them and the | ||
| /// renter. `unverified` means nobody published a key and the first connection | ||
| /// decides. | ||
| export function hostKeyPolicy(access) { | ||
| const fingerprint = access?.channel_key_fingerprint ?? null; | ||
| if (!fingerprint) return { mode: "unverified", fingerprint: null, source: null }; | ||
| return { | ||
| mode: access.channel_key_source === "snp_report" ? "attested" : "reported", | ||
| fingerprint, | ||
| source: access.channel_key_source ?? null, | ||
| }; | ||
| } | ||
| export function knownHostsPath(keyPath) { | ||
| return join(dirname(keyPath), "known_hosts"); | ||
| } | ||
| /// The `ssh-keygen -lf` form of the key in a `known_hosts` line, or null if the | ||
| /// line does not hold one. The control plane publishes fingerprints in exactly | ||
| /// this form, so the two are compared as strings. | ||
| export function knownHostsFingerprint(line) { | ||
| const blob = line.trim().split(/\s+/)[2]; | ||
| if (!blob) return null; | ||
| const raw = Buffer.from(blob, "base64"); | ||
| if (raw.length === 0) return null; | ||
| return `SHA256:${createHash("sha256").update(raw).digest("base64").replace(/=+$/, "")}`; | ||
| } | ||
| /// How `ssh` and `ssh-keyscan` both name a host in `known_hosts`. Anything off | ||
| /// the default port is bracketed, and an entry under the wrong name is an entry | ||
| /// `ssh` will not find. | ||
| function hostField(host, port) { | ||
| return Number(port) === 22 ? String(host) : `[${host}]:${port}`; | ||
| } | ||
| /// A relayed session has no address of its own: the tunnel opens on whatever | ||
| /// local port is free and closes with the command, so the name `ssh` would file | ||
| /// the key under is gone before the next one runs and a record made on first | ||
| /// sight would never be read again. `HostKeyAlias` files it under the lease | ||
| /// instead, which is what makes first-use pinning worth anything there. Capacity | ||
| /// with a real endpoint keeps its own host and port, which is stable and says | ||
| /// something true about where the session went. | ||
| function hostKeyAlias(access) { | ||
| if (access?.mode !== "gateway") return null; | ||
| return access.lease_id ? `prism-lease-${access.lease_id}` : "prism-lease"; | ||
| } | ||
| function scan(host, port) { | ||
| return new Promise((resolve) => { | ||
| execFile( | ||
| "ssh-keyscan", | ||
| ["-T", String(SCAN_TIMEOUT_SECONDS), "-p", String(port), host], | ||
| { timeout: (SCAN_TIMEOUT_SECONDS + 5) * 1_000 }, | ||
| (err, stdout) => resolve({ err, lines: (stdout ?? "").split("\n").filter((l) => l && !l.startsWith("#")) }), | ||
| ); | ||
| }); | ||
| } | ||
| // A record that names the right key under a name `ssh` will not look up is a | ||
| // record `ssh` will refuse to use. Both halves have to match for the scan to be | ||
| // worth skipping. | ||
| function alreadyPinned(path, name, fingerprint) { | ||
| try { | ||
| return readFileSync(path, "utf8") | ||
| .split("\n") | ||
| .some((line) => line.trim().split(/\s+/)[0] === name && knownHostsFingerprint(line) === fingerprint); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| /// Reads the host key off the wire and records it only if it is the one the | ||
| /// grant named. | ||
| /// | ||
| /// Done as a separate exchange before ssh runs, because a fingerprint cannot be | ||
| /// turned into a `known_hosts` entry without the key itself, and letting ssh | ||
| /// learn the key first would mean trusting it to find out whether it should | ||
| /// have. Nothing is sent here that the machine could use: `ssh-keyscan` reads | ||
| /// the key the server offers and hangs up. | ||
| async function pin(host, port, fingerprint, path, name) { | ||
| if (alreadyPinned(path, name, fingerprint)) return; | ||
| const { err, lines } = await scan(host, port); | ||
| if (lines.length === 0) { | ||
| throw new HostKeyError("host_key_unavailable", err?.message ?? `${host}:${port} offered no host key`); | ||
| } | ||
| const match = lines.find((line) => knownHostsFingerprint(line) === fingerprint); | ||
| if (!match) { | ||
| throw new HostKeyError("host_key_mismatch", { | ||
| expected: fingerprint, | ||
| offered: lines.map(knownHostsFingerprint).filter(Boolean), | ||
| hint: "the machine answering is not the one the lease names; nothing was sent to it", | ||
| }); | ||
| } | ||
| // Only the key that matched. Writing everything the machine offered would pin | ||
| // keys nobody vouched for alongside the one that was checked, and the name is | ||
| // rewritten to the one `ssh` will look the key up under. | ||
| writeFileSync(path, `${name} ${match.trim().split(/\s+/).slice(1).join(" ")}\n`, { mode: 0o600 }); | ||
| } | ||
| /// The `ssh` arguments that make the connection check the machine it reaches. | ||
| /// | ||
| /// `requireHostKey` turns the unverified case into a refusal instead of a first | ||
| /// sighting, for callers who would rather not run at all than run somewhere they | ||
| /// cannot name. | ||
| export async function hostKeyArgs(target, access, { requireHostKey = false } = {}) { | ||
| const path = knownHostsPath(target.keyPath); | ||
| const alias = hostKeyAlias(access); | ||
| const where = ["-o", `UserKnownHostsFile=${path}`, ...(alias ? ["-o", `HostKeyAlias=${alias}`] : [])]; | ||
| const policy = hostKeyPolicy(access); | ||
| if (policy.fingerprint === null) { | ||
| if (requireHostKey) { | ||
| throw new HostKeyError("host_key_unpublished", { | ||
| mode: access?.mode ?? null, | ||
| hint: "this lease publishes no host key, so which machine answers cannot be checked", | ||
| }); | ||
| } | ||
| return [...where, "-o", "StrictHostKeyChecking=accept-new"]; | ||
| } | ||
| await pin(target.host, target.port, policy.fingerprint, path, alias ?? hostField(target.host, target.port)); | ||
| return [...where, "-o", "StrictHostKeyChecking=yes"]; | ||
| } |
| /// The digest both sides compare: the command for a job, the request bytes for | ||
| /// a generation. | ||
| export declare function hashRequest(payload: string | Uint8Array): string; | ||
| /// The message a payer signs on the legacy rail, binding a transaction to the | ||
| /// one request it buys. | ||
| export declare function boundMessage(txHash: string, requestHash: string): string; |
+23
| // What a payer signs on the legacy rail, where the transfer is already on-chain | ||
| // and the header only says who made it. | ||
| // | ||
| // Signing the transaction hash alone proves who paid, not what they paid for. | ||
| // Anyone who saw the header in flight could put their own command or prompt in | ||
| // front of it and spend someone else's transfer, so the request travels inside | ||
| // the signed message and the server checks it against the request that arrived. | ||
| // | ||
| // The definition lives here because @prismnetwork/x402 depends on this package | ||
| // and not the other way round. Its codec re-exports both, so a server and its | ||
| // clients read the same two lines. | ||
| import { createHash } from "node:crypto"; | ||
| /// The digest both sides compare: the command for a job, the request bytes for | ||
| /// a generation. Text is hashed as UTF-8, which is how it goes on the wire. | ||
| export function hashRequest(payload) { | ||
| const bytes = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload; | ||
| return createHash("sha256").update(bytes).digest("hex"); | ||
| } | ||
| export function boundMessage(txHash, requestHash) { | ||
| return `prism-x402:v2\n${String(txHash).toLowerCase()}\n${requestHash}`; | ||
| } |
+8
-9
| { | ||
| "name": "@prismnetwork/agent-sdk", | ||
| "version": "0.7.2", | ||
| "version": "0.7.4", | ||
| "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.", | ||
@@ -31,2 +31,6 @@ "type": "module", | ||
| "default": "./workspace.mjs" | ||
| }, | ||
| "./x402": { | ||
| "types": "./x402.d.mts", | ||
| "default": "./x402.mjs" | ||
| } | ||
@@ -41,2 +45,3 @@ }, | ||
| "e2ee.d.mts", | ||
| "hostkey.mjs", | ||
| "relay.mjs", | ||
@@ -49,2 +54,4 @@ "toolset.mjs", | ||
| "workspace.d.ts", | ||
| "x402.mjs", | ||
| "x402.d.mts", | ||
| "vendor/aci-verifier/*.mjs", | ||
@@ -73,10 +80,2 @@ "README.md", | ||
| "homepage": "https://prismnetwork.tech", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/winter0x/prism.git", | ||
| "directory": "sdk" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/winter0x/prism/issues" | ||
| }, | ||
| "license": "Apache-2.0", | ||
@@ -83,0 +82,0 @@ "publishConfig": { |
+41
-1
@@ -10,2 +10,3 @@ import type { AttestationResult, VerifyConfidentialOptions, WorkloadPin } from "./attest.d.mts"; | ||
| export type { AttestationCheck, AttestationResult, WorkloadPin } from "./attest.d.mts"; | ||
| export { boundMessage, hashRequest } from "./x402.d.mts"; | ||
@@ -29,5 +30,36 @@ /// `mode` says which of the two shapes arrived. Brokered capacity fills in | ||
| expires_at?: string; | ||
| /// The `ssh-keygen -lf` fingerprint of the SSH host key the workspace answers | ||
| /// on. Absent on capacity brokered from a public cloud, where the instance's | ||
| /// host key is generated by the cloud and never shown to the network. | ||
| channel_key_fingerprint?: string; | ||
| /// Where that fingerprint came from. `snp_report` is a verified guest report | ||
| /// and holds against the operator too; `node_report` is the operator's word | ||
| /// under their bonded device key. | ||
| channel_key_source?: "snp_report" | "node_report"; | ||
| [key: string]: unknown; | ||
| } | ||
| /// What the network can say about the machine behind a grant. | ||
| export interface HostKeyPolicy { | ||
| mode: "attested" | "reported" | "unverified"; | ||
| fingerprint: string | null; | ||
| source: "snp_report" | "node_report" | null; | ||
| } | ||
| export declare function hostKeyPolicy(access: LeaseAccess | null | undefined): HostKeyPolicy; | ||
| /// The `ssh` arguments that make a connection check the machine it reaches: | ||
| /// the host key is verified against the grant when one is published, and | ||
| /// recorded on first sight and held for the rest of the lease when none is. | ||
| export declare function hostKeyArgs( | ||
| target: { host: string; port: number; keyPath: string }, | ||
| access: LeaseAccess | null | undefined, | ||
| options?: { requireHostKey?: boolean }, | ||
| ): Promise<string[]>; | ||
| export declare class HostKeyError extends Error { | ||
| readonly code: string; | ||
| readonly detail: unknown; | ||
| } | ||
| /// A local address that forwards to the workspace until it is closed. | ||
@@ -94,3 +126,11 @@ export interface RelayForwarder { | ||
| export declare class PrismAgent { | ||
| constructor(options: { privateKey: string; escrow: string; apiBase?: string; rpcUrl?: string }); | ||
| constructor(options: { | ||
| privateKey: string; | ||
| escrow: string; | ||
| apiBase?: string; | ||
| rpcUrl?: string; | ||
| /// Refuse to open a session on a lease that publishes no host key, rather | ||
| /// than trusting whichever machine answers first. | ||
| requireHostKey?: boolean; | ||
| }); | ||
| readonly address: string; | ||
@@ -97,0 +137,0 @@ readonly vault: unknown; |
+77
-29
| // Prism Network agent SDK: headless GPU leasing for wallet-holding agents. | ||
| // No browser, no Privy. Authenticate with a wallet signature, pay on-chain, run. | ||
| import { execFileSync, spawn } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { mkdtempSync, readFileSync, rmSync } from "node:fs"; | ||
@@ -20,8 +19,12 @@ import { tmpdir } from "node:os"; | ||
| import { decryptResponse, encryptChatRequest } from "./e2ee.mjs"; | ||
| import { hostKeyArgs, HostKeyError } from "./hostkey.mjs"; | ||
| import { openRelayForwarder } from "./relay.mjs"; | ||
| import { PrismVault } from "./vault.mjs"; | ||
| import { toHex, verifyComposeMeasurement, verifyQuote, verifyReportBinding } from "./vendor/aci-verifier/index.mjs"; | ||
| import { boundMessage, hashRequest } from "./x402.mjs"; | ||
| import { PrismWorkspace } from "./workspace.mjs"; | ||
| export { DEFAULT_CONFIDENTIAL_BASE, EXPECTED_WORKLOAD, renderChecks, verifyConfidential } from "./attest.mjs"; | ||
| export { hostKeyArgs, hostKeyPolicy, HostKeyError } from "./hostkey.mjs"; | ||
| export { boundMessage, hashRequest } from "./x402.mjs"; | ||
| export { PrismVault, VaultError, DEFAULT_TRUST_FLOOR, VAULT_KEY_STATEMENT } from "./vault.mjs"; | ||
@@ -147,3 +150,3 @@ export { | ||
| export class PrismAgent { | ||
| constructor({ privateKey, apiBase = "https://prismnetwork.tech", escrow, rpcUrl }) { | ||
| constructor({ privateKey, apiBase = "https://prismnetwork.tech", escrow, rpcUrl, requireHostKey = false }) { | ||
| if (!escrow) throw new Error("escrow address is required"); | ||
@@ -157,2 +160,7 @@ if (typeof privateKey !== "string" || privateKey.trim() === "") { | ||
| this.escrow = escrow; | ||
| // Off by default because most capacity publishes no host key, and refusing | ||
| // those leases would take the network's own supply away from callers who | ||
| // never asked for the guarantee. On, nothing runs anywhere the grant cannot | ||
| // name. | ||
| this.requireHostKey = requireHostKey; | ||
| const trimmed = privateKey.trim(); | ||
@@ -217,2 +225,3 @@ try { | ||
| async transferUsdg(to, amountMicros) { | ||
| let broadcast = null; | ||
| try { | ||
@@ -227,2 +236,3 @@ const hash = await this.#submit(() => | ||
| ); | ||
| broadcast = hash; | ||
| const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); | ||
@@ -233,3 +243,10 @@ if (receipt.status !== "success") throw new PrismError(502, "transfer_reverted", { hash }); | ||
| if (err instanceof PrismError) throw err; | ||
| throw new PrismError(502, "chain_error", { cause: err?.shortMessage ?? err?.message ?? String(err) }); | ||
| // A receipt that could not be read is not a transfer that never happened. | ||
| // The hash travels with the failure because it is the only thing that | ||
| // says the money left this wallet, and whatever is counting the day's | ||
| // spend has to be able to tell the two apart. | ||
| throw new PrismError(502, "chain_error", { | ||
| cause: err?.shortMessage ?? err?.message ?? String(err), | ||
| ...(broadcast ? { payment_tx: broadcast } : {}), | ||
| }); | ||
| } | ||
@@ -271,2 +288,3 @@ } | ||
| const clientReference = keccak256(stringToBytes(quote.quote_id)); | ||
| let broadcast = null; | ||
| try { | ||
@@ -299,2 +317,3 @@ // Approving and spending are one indivisible step. The approval covers | ||
| }); | ||
| broadcast = funding; | ||
| // One confirmation here, not for the control-plane's benefit but so the | ||
@@ -311,3 +330,9 @@ // allowance and the nonce are settled before the next lease reads them. | ||
| if (err instanceof PrismError) throw err; | ||
| throw new PrismError(502, "chain_error", { cause: err?.shortMessage ?? err?.message ?? String(err) }); | ||
| // The deposit is in the escrow the moment the chain accepts this, and | ||
| // waiting for confirmations is where a flaky rpc gives up. Losing the | ||
| // hash here would leave a funded lease nobody can name. | ||
| throw new PrismError(502, "chain_error", { | ||
| cause: err?.shortMessage ?? err?.message ?? String(err), | ||
| ...(broadcast ? { funding_hash: broadcast } : {}), | ||
| }); | ||
| } | ||
@@ -518,2 +543,3 @@ } | ||
| keyPath: lease.keyPath, | ||
| access: lease.access, | ||
| } | ||
@@ -525,2 +551,3 @@ : { | ||
| keyPath: lease.keyPath, | ||
| access: lease.access, | ||
| }; | ||
@@ -536,3 +563,19 @@ if (!target.host || !target.port) { | ||
| for (let attempt = 0; attempt <= connectRetries; attempt++) { | ||
| const res = await this.#ssh(target, command, timeoutMs, stdin); | ||
| let res; | ||
| try { | ||
| res = await this.#ssh(target, command, timeoutMs, stdin); | ||
| } catch (err) { | ||
| // A box that is still coming up has nothing listening to read a key | ||
| // from, which is the same wait the retry loop already exists for. | ||
| // Being answered by the wrong machine is not a wait. | ||
| if (!(err instanceof HostKeyError) || err.code !== "host_key_unavailable") { | ||
| // `host_key_unpublished` is this client refusing on the caller's | ||
| // own policy. Anything else is the far end failing the check. | ||
| throw new PrismError(err?.code === "host_key_unpublished" ? 400 : 502, err?.code ?? "ssh_failed", { | ||
| lease_id: lease.leaseId ?? null, | ||
| detail: err?.detail ?? err?.message ?? String(err), | ||
| }); | ||
| } | ||
| res = { code: 255, stdout: "", stderr: `ssh: ${err.detail ?? err.code}`, timedOut: false }; | ||
| } | ||
| if (!isSshWarmup(res)) return res; | ||
@@ -603,23 +646,29 @@ last = res; | ||
| let sent = seal ? seal() : { bytes: asBytes(body), headers }; | ||
| const identity = createHash("sha256").update(fingerprint ?? sent.bytes).digest("hex"); | ||
| const identity = hashRequest(fingerprint ?? sent.bytes); | ||
| const key = `${base}${path}:${price}:${identity}`; | ||
| let pending = this.#pendingPayments.get(key); | ||
| if (!pending) { | ||
| const tx = await this.transferUsdg(payTo, price); | ||
| const signature = await this.account.signMessage({ message: tx }); | ||
| pending = { tx, header: Buffer.from(JSON.stringify({ txHash: tx, signature })).toString("base64") }; | ||
| this.#pendingPayments.set(key, pending); | ||
| let tx = this.#pendingPayments.get(key); | ||
| if (!tx) { | ||
| tx = await this.transferUsdg(payTo, price); | ||
| this.#pendingPayments.set(key, tx); | ||
| } | ||
| // The transfer is on-chain and irreversible from here. The signed header is | ||
| // the only thing that redeems it, and it lives in this process. | ||
| const kept = { | ||
| payment_tx: pending.tx, | ||
| payment_header: pending.header, | ||
| hint: | ||
| `the payment (tx ${pending.tx}) settled on-chain and the endpoint did not serve. While this process lives, ` + | ||
| `the next ${caller} for this same request redeems it without paying again. payment_header is what redeems ` + | ||
| "it, so keep it to do that from anywhere else.", | ||
| }; | ||
| const deadline = Date.now() + PAID_CALL_DEADLINE_MS; | ||
| for (;;) { | ||
| // The signature covers the transaction and the bytes it buys, so a header | ||
| // read off the wire cannot be spent on a different request. A resealed | ||
| // attempt carries different bytes and is signed again; the transfer, which | ||
| // is the half that costs money, is made once. | ||
| const header = Buffer.from(JSON.stringify({ | ||
| txHash: tx, | ||
| signature: await this.account.signMessage({ message: boundMessage(tx, hashRequest(sent.bytes)) }), | ||
| })).toString("base64"); | ||
| // The transfer is on-chain and irreversible from here. The signed header | ||
| // is the only thing that redeems it, and it lives in this process. | ||
| const kept = { | ||
| payment_tx: tx, | ||
| payment_header: header, | ||
| hint: | ||
| `the payment (tx ${tx}) settled on-chain and the endpoint did not serve. While this process lives, ` + | ||
| `the next ${caller} for this same request redeems it without paying again. payment_header redeems it ` + | ||
| "from anywhere else, and only for this request: the signature covers these exact bytes.", | ||
| }; | ||
| let res; | ||
@@ -630,3 +679,3 @@ let bytes; | ||
| method: "POST", | ||
| headers: { "content-type": "application/json", "x-payment": pending.header, ...sent.headers }, | ||
| headers: { "content-type": "application/json", "x-payment": header, ...sent.headers }, | ||
| body: sent.bytes, | ||
@@ -646,7 +695,7 @@ signal: AbortSignal.timeout(PAID_CALL_TIMEOUT_MS), | ||
| throw new PrismError(409, "payment_replayed", { | ||
| cause: `the endpoint replayed an earlier answer for tx ${pending.tx}`, | ||
| cause: `the endpoint replayed an earlier answer for tx ${tx}`, | ||
| hint: "this payment was already consumed by another call; pay again to have this request served", | ||
| }); | ||
| } | ||
| return { status: 200, headers: res.headers, bytes, tx: pending.tx, sent }; | ||
| return { status: 200, headers: res.headers, bytes, tx, sent }; | ||
| } | ||
@@ -671,3 +720,3 @@ const answered = (() => { | ||
| cause: said || answered?.error || `status ${res.status}`, | ||
| ...(this.#pendingPayments.has(key) ? kept : { payment_tx: pending.tx }), | ||
| ...(this.#pendingPayments.has(key) ? kept : { payment_tx: tx }), | ||
| }); | ||
@@ -943,8 +992,7 @@ } | ||
| #ssh(target, command, timeoutMs, stdin = null) { | ||
| async #ssh(target, command, timeoutMs, stdin = null) { | ||
| const args = [ | ||
| "-i", target.keyPath, | ||
| "-p", String(target.port), | ||
| "-o", "StrictHostKeyChecking=no", | ||
| "-o", "UserKnownHostsFile=/dev/null", | ||
| ...(await hostKeyArgs(target, target.access, { requireHostKey: this.requireHostKey })), | ||
| "-o", "BatchMode=yes", | ||
@@ -951,0 +999,0 @@ "-o", "ConnectTimeout=15", |
+50
-2
@@ -136,2 +136,50 @@ # @prismnetwork/agent-sdk | ||
| ## Which machine answered | ||
| Every session `run()` opens checks the SSH host key on the far end. What that is | ||
| worth depends on what the lease publishes about it, and the SDK reports which of | ||
| the three it got: | ||
| ```js | ||
| import { hostKeyPolicy } from "@prismnetwork/agent-sdk"; | ||
| hostKeyPolicy(lease.access); | ||
| // { mode: "attested" | "reported" | "unverified", fingerprint, source } | ||
| ``` | ||
| `attested` means the fingerprint comes out of a hardware report whose signed | ||
| data commits to the key the guest generated at boot. Prism walks that report to | ||
| AMD's root and puts the fingerprint on the access grant; the SDK refuses any | ||
| session whose host key does not match it. The operator cannot put a different | ||
| machine on the other end, and neither can anything on the path between you and | ||
| it. | ||
| The chain walk runs in our control plane, so `attested` says we checked the | ||
| report. The SDK holds the session to the fingerprint we published and never sees | ||
| the report itself, which leaves us in the set you are trusting. | ||
| [ATTESTATION.md](https://github.com/winter0x/prism/blob/main/docs/ATTESTATION.md) | ||
| says what the report covers and what it does not. | ||
| `reported` means the node named the key on the signed report that opened access. | ||
| That rules out the relay, the network path and anyone in between; it does not | ||
| rule out the operator, whose bond is what a dispute reaches instead. | ||
| `unverified` means nobody published a key. Capacity brokered from a public cloud | ||
| is the case: the instance's host key is generated by the cloud and never shown to | ||
| the network, so there is nothing to publish. The key is recorded the first time | ||
| it is seen and held for the rest of the lease, which catches a machine swapped in | ||
| partway through and cannot catch one that was wrong from the start. | ||
| Nothing is written to your own `~/.ssh/known_hosts`. The record sits beside the | ||
| lease's private key and is removed with it by `endLease()`. | ||
| To refuse the third case outright: | ||
| ```js | ||
| const agent = new PrismAgent({ privateKey, escrow, requireHostKey: true }); | ||
| ``` | ||
| The lease still funds and provisions; `run()` refuses to open a session on it | ||
| with `host_key_unpublished`. | ||
| ## Auth | ||
@@ -151,5 +199,5 @@ | ||
| Node >= 20, `viem` ^2 (peer), and `ssh` + `ssh-keygen` on PATH for `run()` and | ||
| for workspace save and restore. | ||
| Node >= 20, `viem` ^2 (peer), and `ssh`, `ssh-keygen` and `ssh-keyscan` on PATH | ||
| for `run()` and for workspace save and restore. | ||
| See [example.mjs](https://github.com/winter0x/prism/blob/main/sdk/example.mjs) for a full run. |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
213929
7.4%27
12.5%3870
5.91%202
31.17%15
7.14%15
7.14%