/** * NodeRuntime — ergonomic façade for running guest JavaScript end-to-end. * * Boots a fully virtualized VM (via the native sidecar) and runs guest Node * programs with minimal boilerplate. All of the sidecar spawn, session * handshake, VM creation, root filesystem bootstrap, runtime-driver mounting, * and lifecycle waiting are hidden behind `NodeRuntime.create()`. * * ```ts * const rt = await NodeRuntime.create(); * const { stdout, exitCode } = await rt.exec("console.log('hi', 1 + 1)"); * await rt.dispose(); * ``` * * Guest code is written to an ESM module inside the VM and executed as * `node <file>` through the kernel, so all execution stays inside the kernel * isolation boundary — no host escapes, no real Node.js builtins for guest * work. */ import { existsSync } from "node:fs";import path from "node:path";import { fileURLToPath } from "node:url";import type { ExecResult, BindingDefinition, Kernel, KernelBootTiming, Permissions, VirtualDirEntry, VirtualFileSystem,} from "./test-runtime.js";import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js";import type { VmUserConfig } from "./generated/VmUserConfig.js";import type { SidecarProcess } from "./sidecar-process.js";import { createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem,} from "./test-runtime.js";import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js"; export type { BindingDefinition, BindingExample, VirtualDirEntry,} from "./test-runtime.js";export { resolveNodeRuntimeSidecarBinary } from "./test-runtime.js"; export type NodeRuntimeBootTimingPhase = | KernelBootTiming["phase"] | "runtime_mount_wasm" | "runtime_mount_node" | "bindings"; export interface NodeRuntimeBootTiming { phase: NodeRuntimeBootTimingPhase; durationMs: number;} /** Repository root, used to locate the in-repo WASM command build output. */const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); /** * In-repo build output for the WASM coreutils/shell command binaries, produced * by the Rust command build (`make -C toolchain wasm`). Only present in a * developer checkout; preferred when it exists so local edits are picked up * without re-vendoring. */const REPO_COMMANDS_DIR = path.join( REPO_ROOT, "toolchain/target/wasm32-wasip1/release/commands",); /** * Commands vendored into the published `@rivet-dev/agentos-runtime-core` package by * `scripts/copy-wasm-commands.mjs` (listed in `files` as `commands`). This is * the directory a real `npm install secure-exec` resolves: from the compiled * `dist/node-runtime.js` it sits at `<package>/commands`. This is the analogue * of how the sidecar binary ships inside `@rivet-dev/agentos-runtime-sidecar`. */const BUNDLED_COMMANDS_DIR = fileURLToPath( new URL("../commands", import.meta.url),); /** * Resolve the directory holding the WASM command binaries (the source of the * guest `sh` the kernel needs to spawn any process). Precedence: * * 1. explicit `commandsDir` option, * 2. `AGENTOS_WASM_COMMANDS_DIR` env var, * 3. the in-repo build output (developer checkout), when present, * 4. the commands vendored into the installed package (published installs). * * The in-repo path wins over the bundled copy so local development picks up * freshly built commands without re-vendoring. A fresh `npm install` has no * in-repo path, so it falls through to the bundled copy. */export function resolveNodeRuntimeCommandsDir(explicit?: string): string { if (explicit !== undefined) { return explicit; } const fromEnv = process.env.AGENTOS_WASM_COMMANDS_DIR; if (fromEnv) { return fromEnv; } if (existsSync(REPO_COMMANDS_DIR)) { return REPO_COMMANDS_DIR; } return BUNDLED_COMMANDS_DIR;} /** * Secure-by-default permission policy applied when the caller passes no * `permissions`. Outward-facing capabilities are denied: there is **no network * access** (and no host callbacks) by default — guest code cannot reach the * network until you opt in. The filesystem, child-process, process, and env * scopes are allowed because they are fully virtualized (the guest only ever * sees the VM's in-memory filesystem and kernel-managed processes, never the * real host) and are required for the runtime to execute a guest program at * all. Tighten or widen any scope by passing your own `permissions`. */const DEFAULT_PERMISSIONS: Permissions = { fs: "allow", childProcess: "allow", process: "allow", env: "allow", network: "deny",}; /** * Options for {@link NodeRuntime.create}. * * Keep this public interface in sync with * `packages/core/src/node-runtime-options-schema.ts::nodeRuntimeCreateOptionsSchema`. * Options that translate into sidecar VM JSON must also stay aligned with * `crates/vm-config/src/lib.rs::CreateVmConfig`. */export interface NodeRuntimeCreateOptions { /** * Caller-owned filesystem used only by this low-level compatibility runtime. * AgentOS clients do not create a TypeScript filesystem implicitly; normal * product code should use the sidecar-owned VFS through `AgentOs`. */ filesystem: VirtualFileSystem; /** Environment variables visible to guest processes. */ env?: Record<string, string>; /** Initial working directory for guest processes. Defaults to `/workspace`. */ cwd?: string; /** Initial virtual Linux credentials and account record. Defaults to `1000:1000` (`agentos`). */ user?: VmUserConfig; /** * Permission policy for the VM. Merged over a secure default that **denies * network access** (guest code cannot reach the network until you opt in); * the virtualized filesystem and processes stay enabled so programs run. * Because it merges, a partial policy works: `{ network: "allow" }` grants * the network while keeping the execution essentials. Pass a fuller policy * (rule sets) to further sandbox individual scopes. */ permissions?: Permissions; /** * Override the directory containing the WASM command binaries (the source of * the guest `sh`). When unset, resolution falls back through the * `AGENTOS_WASM_COMMANDS_DIR` environment variable, the in-repo build * output (developer checkouts), then the commands vendored into the installed * `@rivet-dev/agentos-runtime-core` package (published installs). */ commandsDir?: string; /** * Additional directories of wasm32-wasip1 commands to register in the VM. * Intended for low-level tooling and benchmark harnesses; normal callers use * the bundled shell/coreutils command directory resolved by `commandsDir`. */ wasmCommandDirs?: string[]; /** * Existing native sidecar process to use for this runtime. Omit this to use * the default shared sidecar behavior. When provided, the runtime owns only * its VM and leaves sidecar process disposal to the caller. */ sidecar?: SidecarProcess; /** Receives coarse boot phase timings for benchmarks and diagnostics. */ onBootTiming?: (timing: NodeRuntimeBootTiming) => void; /** * Files to seed into the VM's virtual filesystem before the guest runs, * keyed by absolute guest path. Parent directories are created as needed. * Use this to project host assets, npm packages, or fixtures into the * sandbox so guest code can `import`/`require`/read them. The bytes are * copied into the VM's in-memory filesystem; the host filesystem is never * exposed to the guest. * * ```ts * const rt = await NodeRuntime.create({ * files: { "/root/data.json": '{"ok":true}' }, * }); * ``` */ files?: Record<string, string | Uint8Array>; /** * Host directories to project into the VM's virtual filesystem, Docker-style. * Each mount makes a host directory readable at a guest path. Files are read * lazily from the host as the guest accesses them, so large trees (for * example a `node_modules` package such as the TypeScript compiler) are * projected without copying their bytes up front. The guest sees only the * mounted subtree, never the wider host filesystem. * * ```ts * const rt = await NodeRuntime.create({ * mounts: [ * { * guestPath: "/root/node_modules/typescript", * hostPath: "/abs/path/to/node_modules/typescript", * readOnly: true, * }, * ], * }); * ``` */ mounts?: HostDirectoryMount[]; /** * Mount a host `node_modules` directory into the VM in one call so guest * `import`/`require` resolve real, host-installed npm packages. * * Pass the absolute host path to a `node_modules` directory (or an object * with that path and an explicit guest location). The whole directory is * projected lazily, Docker-style, at a guest `node_modules` on the resolution * path, so any package inside it resolves the way Node would over a real * filesystem (ancestor `node_modules` walk, `exports`/conditions, symlinks). * This is the ergonomic alternative to wiring up individual `mounts` entries * per package. * * By default the directory is mounted at `/tmp/node_modules`, which is where * the resolution walk for a program run by {@link NodeRuntime.exec} / * {@link NodeRuntime.run} begins (each program is written under `/tmp`). Pass * the object form with `guestPath` to mount it elsewhere on a different * module's resolution path. * * ```ts * const rt = await NodeRuntime.create({ * nodeModules: "/abs/path/to/project/node_modules", * }); * await rt.exec(` * import isNumber from "is-number"; * console.log(isNumber(42)); * `); * ``` * * The host filesystem is never exposed beyond the mounted `node_modules` * subtree. The mount is read-only. */ nodeModules?: string | NodeModulesMount; /** * Host-side bindings the guest can invoke as shell commands. Each entry is * registered as a named guest command; when the guest runs it, the * invocation round-trips back to the host and runs the binding's `handler`, * whose return value is delivered back to the guest. This is how you give * sandboxed guest code controlled, named host capabilities (the kind an AI * agent calls as tools) without granting it the underlying access directly. * * The guest invokes a binding by name with JSON input: * * ```ts * const rt = await NodeRuntime.create({ * bindings: { * add: { * description: "Add two numbers", * inputSchema: { * type: "object", * properties: { a: { type: "number" }, b: { type: "number" } }, * required: ["a", "b"], * }, * handler: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }), * }, * }, * }); * await rt.exec(` * import { execFileSync } from "node:child_process"; * const out = execFileSync("add", ["add", "--json", JSON.stringify({ a: 2, b: 3 })]); * console.log(out.toString()); * `); * ``` * * When `bindings` is provided and no `binding` permission scope is set, the * `binding` scope is granted so the registered bindings are invocable; pass * your own `permissions.binding` policy to gate individual bindings. */ bindings?: Record<string, BindingDefinition>; /** * Guest-bound ports that may accept non-loopback connections. By default a * guest server is reachable only over loopback inside the VM; listing a port * here lifts that restriction for that port, letting connections from outside * the loopback interface reach it. Use this for guests that run servers which * must accept external connections (for example a dev server you expose * beyond loopback). * * ```ts * const rt = await NodeRuntime.create({ * permissions: { network: "allow" }, * loopbackExemptPorts: [3000], * }); * ``` */ loopbackExemptPorts?: number[]; /** * Low-level guest JavaScript runtime configuration. Most callers should leave * this unset. Benchmarks may opt in to `highResolutionTime`, which disables * the default 1ms timer quantization and should not be enabled for untrusted * workloads. */ jsRuntime?: Partial<JsRuntimeConfig>;} /** A host directory projected into the VM's virtual filesystem. */export interface HostDirectoryMount { /** Absolute guest path the directory appears at inside the VM. */ guestPath: string; /** Absolute host directory to project (read through the VFS, lazily). */ hostPath: string; /** Mount read-only (the default). Pass `false` to allow guest writes. */ readOnly?: boolean;} /** Guest path a `nodeModules` mount is projected at by default. */const DEFAULT_NODE_MODULES_GUEST_PATH = "/tmp/node_modules"; /** * Object form of the `nodeModules` create option: a host `node_modules` * directory to project, optionally at an explicit guest path. The string form * (`nodeModules: "/abs/node_modules"`) is shorthand for `{ hostPath }`. */export interface NodeModulesMount { /** Absolute host `node_modules` directory to project (read lazily). */ hostPath: string; /** * Absolute guest path to mount it at. Defaults to `/tmp/node_modules`, where * the resolution walk for {@link NodeRuntime.exec} / {@link NodeRuntime.run} * programs begins. Override to put it on a different module's resolution path. */ guestPath?: string;} /** Result of {@link NodeRuntime.exec}. */export interface NodeRuntimeExecResult { stdout: string; stderr: string; exitCode: number;} /** Options for a single {@link NodeRuntime.exec} call. */export interface NodeRuntimeExecOptions { /** Extra environment variables for this run, merged over the VM env. */ env?: Record<string, string>; /** Working directory for this run. */ cwd?: string; /** Data piped to the guest program's stdin. */ stdin?: string | Uint8Array; /** Abort the run after this many milliseconds. */ timeout?: number; /** * Cancel the run when this signal aborts. On abort the guest process is * killed inside the VM (the kernel delivers `SIGTERM`) and the call rejects * with the signal's abort reason. Use this to cancel an in-flight run from * the outside, for example to enforce your own deadline or stop work when a * request is canceled. * * ```ts * const controller = new AbortController(); * const pending = rt.exec("while (true) {}", { signal: controller.signal }); * controller.abort(); * await pending.catch((err) => console.log(err.name)); // "AbortError" * ``` */ signal?: AbortSignal; /** * Called with each chunk the guest writes to stdout as it is produced, * letting you observe output incrementally instead of waiting for the run to * finish. Chunks arrive as raw bytes; decode with a `TextDecoder` for text. * The complete output is still returned as `result.stdout` when the run ends. */ onStdout?: (chunk: Uint8Array) => void; /** * Called with each chunk the guest writes to stderr as it is produced, * letting you observe output incrementally instead of waiting for the run to * finish. Chunks arrive as raw bytes; decode with a `TextDecoder` for text. * The complete output is still returned as `result.stderr` when the run ends. */ onStderr?: (chunk: Uint8Array) => void;} /** The HTTP request {@link NodeRuntime.fetch} drives into the VM. */export interface NodeRuntimeFetchInput { /** HTTP method. Defaults to `GET`. */ method?: string; /** Request path (and query), e.g. `/api/users?limit=10`. */ path: string; /** Request headers. */ headers?: Record<string, string>; /** Request body. */ body?: string | Uint8Array;} /** The HTTP response {@link NodeRuntime.fetch} returns from the VM. */export interface NodeRuntimeFetchResponse { /** HTTP status code, e.g. `200`. */ status: number; /** HTTP status text, e.g. `OK`. */ statusText: string; /** Response headers, lower-cased by name. */ headers: Record<string, string>; /** Response body decoded as UTF-8 text. */ body: string;} /** * Options for {@link NodeRuntime.spawn}. Inherits the streaming `onStdout` / * `onStderr` hooks from {@link NodeRuntimeExecOptions}. */export interface NodeRuntimeSpawnOptions extends NodeRuntimeExecOptions {} /** * Describes the guest TCP listener to wait for with * {@link NodeRuntime.waitForListener} or look up with * {@link NodeRuntime.findListener}. A listener matches when a guest process is * accepting connections on the given `port` (and `host`/`path` when supplied). */export interface NodeRuntimeListenerQuery { /** TCP port the guest listener is bound to, e.g. `3000`. */ port: number; /** Bind host to match, e.g. `127.0.0.1`. Omit to match any host. */ host?: string; /** Unix socket path to match, for path-bound listeners. */ path?: string;} /** * A matched guest listener returned by {@link NodeRuntime.waitForListener} and * {@link NodeRuntime.findListener}. `processId` identifies the guest process * that owns the listening socket; the `host`/`port`/`path` it is bound to are * reported when known. */export interface NodeRuntimeListener { /** The guest process id that owns the listening socket. */ processId: string; /** The host the listener is bound to, when reported. */ host?: string; /** The port the listener is bound to, when reported. */ port?: number; /** The unix socket path the listener is bound to, when reported. */ path?: string;} /** Options for {@link NodeRuntime.waitForListener}. */export interface NodeRuntimeWaitForListenerOptions { /** * Give up after this many milliseconds and reject. Defaults to 10000. The * wait also rejects if the bound `signal` aborts first. */ timeoutMs?: number; /** Abort the wait early; the returned promise rejects when it fires. */ signal?: AbortSignal; /** * How long to wait between listener lookups while polling, in milliseconds. * Defaults to 50. */ pollIntervalMs?: number;} /** * A live handle to a guest process started with {@link NodeRuntime.spawn}. * * Unlike {@link NodeRuntime.exec}, which runs a program to completion and * returns its captured output, a handle is returned immediately while the * process keeps running. Use it to stream stdout/stderr, feed stdin, signal or * kill the process, and await its exit. This is the building block for * long-running guests such as dev servers: start one here, then drive requests * into it with {@link NodeRuntime.fetch}. */export interface NodeRuntimeProcess { /** The guest process id. */ readonly pid: number; /** Write data to the guest process's stdin. */ writeStdin(data: string | Uint8Array): void; /** Close the guest process's stdin, signalling end-of-input. */ closeStdin(): void; /** * Send a signal to the guest process. Defaults to `SIGTERM`. Accepts a * signal name (e.g. `"SIGKILL"`) or a raw signal number. */ kill(signal?: NodeJS.Signals | number): void; /** Resolve with the guest process's exit code once it terminates. */ wait(): Promise<number>; /** The exit code once the process has exited, or `null` while it runs. */ readonly exitCode: number | null;} export interface NodeRuntimeResourceSnapshot { runningProcesses: number; exitedProcesses: number; fdTables: number; openFds: number;Lines 1–500 of 1513. Load more
/** * NodeRuntime — ergonomic façade for running guest JavaScript end-to-end. * * Boots a fully virtualized VM (via the native sidecar) and runs guest Node * programs with minimal boilerplate. All of the sidecar spawn, session * handshake, VM creation, root filesystem bootstrap, runtime-driver mounting, * and lifecycle waiting are hidden behind `NodeRuntime.create()`. * * ```ts * const rt = await NodeRuntime.create(); * const { stdout, exitCode } = await rt.exec("console.log('hi', 1 + 1)"); * await rt.dispose(); * ``` * * Guest code is written to an ESM module inside the VM and executed as * `node <file>` through the kernel, so all execution stays inside the kernel * isolation boundary — no host escapes, no real Node.js builtins for guest * work. */ import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { ExecResult, BindingDefinition, Kernel, KernelBootTiming, Permissions, VirtualDirEntry, VirtualFileSystem, } from "./test-runtime.js"; import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; import type { VmUserConfig } from "./generated/VmUserConfig.js"; import type { SidecarProcess } from "./sidecar-process.js"; import { createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, } from "./test-runtime.js"; import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js"; export type { BindingDefinition, BindingExample, VirtualDirEntry, } from "./test-runtime.js"; export { resolveNodeRuntimeSidecarBinary } from "./test-runtime.js"; export type NodeRuntimeBootTimingPhase = | KernelBootTiming["phase"] | "runtime_mount_wasm" | "runtime_mount_node" | "bindings"; export interface NodeRuntimeBootTiming { phase: NodeRuntimeBootTimingPhase; durationMs: number; } /** Repository root, used to locate the in-repo WASM command build output. */ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); /** * In-repo build output for the WASM coreutils/shell command binaries, produced * by the Rust command build (`make -C toolchain wasm`). Only present in a * developer checkout; preferred when it exists so local edits are picked up * without re-vendoring. */ const REPO_COMMANDS_DIR = path.join( REPO_ROOT, "toolchain/target/wasm32-wasip1/release/commands", ); /** * Commands vendored into the published `@rivet-dev/agentos-runtime-core` package by * `scripts/copy-wasm-commands.mjs` (listed in `files` as `commands`). This is * the directory a real `npm install secure-exec` resolves: from the compiled * `dist/node-runtime.js` it sits at `<package>/commands`. This is the analogue * of how the sidecar binary ships inside `@rivet-dev/agentos-runtime-sidecar`. */ const BUNDLED_COMMANDS_DIR = fileURLToPath( new URL("../commands", import.meta.url), ); /** * Resolve the directory holding the WASM command binaries (the source of the * guest `sh` the kernel needs to spawn any process). Precedence: * * 1. explicit `commandsDir` option, * 2. `AGENTOS_WASM_COMMANDS_DIR` env var, * 3. the in-repo build output (developer checkout), when present, * 4. the commands vendored into the installed package (published installs). * * The in-repo path wins over the bundled copy so local development picks up * freshly built commands without re-vendoring. A fresh `npm install` has no * in-repo path, so it falls through to the bundled copy. */ export function resolveNodeRuntimeCommandsDir(explicit?: string): string { if (explicit !== undefined) { return explicit; } const fromEnv = process.env.AGENTOS_WASM_COMMANDS_DIR; if (fromEnv) { return fromEnv; } if (existsSync(REPO_COMMANDS_DIR)) { return REPO_COMMANDS_DIR; } return BUNDLED_COMMANDS_DIR; } /** * Secure-by-default permission policy applied when the caller passes no * `permissions`. Outward-facing capabilities are denied: there is **no network * access** (and no host callbacks) by default — guest code cannot reach the * network until you opt in. The filesystem, child-process, process, and env * scopes are allowed because they are fully virtualized (the guest only ever * sees the VM's in-memory filesystem and kernel-managed processes, never the * real host) and are required for the runtime to execute a guest program at * all. Tighten or widen any scope by passing your own `permissions`. */ const DEFAULT_PERMISSIONS: Permissions = { fs: "allow", childProcess: "allow", process: "allow", env: "allow", network: "deny", }; /** * Options for {@link NodeRuntime.create}. * * Keep this public interface in sync with * `packages/core/src/node-runtime-options-schema.ts::nodeRuntimeCreateOptionsSchema`. * Options that translate into sidecar VM JSON must also stay aligned with * `crates/vm-config/src/lib.rs::CreateVmConfig`. */ export interface NodeRuntimeCreateOptions { /** * Caller-owned filesystem used only by this low-level compatibility runtime. * AgentOS clients do not create a TypeScript filesystem implicitly; normal * product code should use the sidecar-owned VFS through `AgentOs`. */ filesystem: VirtualFileSystem; /** Environment variables visible to guest processes. */ env?: Record<string, string>; /** Initial working directory for guest processes. Defaults to `/workspace`. */ cwd?: string; /** Initial virtual Linux credentials and account record. Defaults to `1000:1000` (`agentos`). */ user?: VmUserConfig; /** * Permission policy for the VM. Merged over a secure default that **denies * network access** (guest code cannot reach the network until you opt in); * the virtualized filesystem and processes stay enabled so programs run. * Because it merges, a partial policy works: `{ network: "allow" }` grants * the network while keeping the execution essentials. Pass a fuller policy * (rule sets) to further sandbox individual scopes. */ permissions?: Permissions; /** * Override the directory containing the WASM command binaries (the source of * the guest `sh`). When unset, resolution falls back through the * `AGENTOS_WASM_COMMANDS_DIR` environment variable, the in-repo build * output (developer checkouts), then the commands vendored into the installed * `@rivet-dev/agentos-runtime-core` package (published installs). */ commandsDir?: string; /** * Additional directories of wasm32-wasip1 commands to register in the VM. * Intended for low-level tooling and benchmark harnesses; normal callers use * the bundled shell/coreutils command directory resolved by `commandsDir`. */ wasmCommandDirs?: string[]; /** * Existing native sidecar process to use for this runtime. Omit this to use * the default shared sidecar behavior. When provided, the runtime owns only * its VM and leaves sidecar process disposal to the caller. */ sidecar?: SidecarProcess; /** Receives coarse boot phase timings for benchmarks and diagnostics. */ onBootTiming?: (timing: NodeRuntimeBootTiming) => void; /** * Files to seed into the VM's virtual filesystem before the guest runs, * keyed by absolute guest path. Parent directories are created as needed. * Use this to project host assets, npm packages, or fixtures into the * sandbox so guest code can `import`/`require`/read them. The bytes are * copied into the VM's in-memory filesystem; the host filesystem is never * exposed to the guest. * * ```ts * const rt = await NodeRuntime.create({ * files: { "/root/data.json": '{"ok":true}' }, * }); * ``` */ files?: Record<string, string | Uint8Array>; /** * Host directories to project into the VM's virtual filesystem, Docker-style. * Each mount makes a host directory readable at a guest path. Files are read * lazily from the host as the guest accesses them, so large trees (for * example a `node_modules` package such as the TypeScript compiler) are * projected without copying their bytes up front. The guest sees only the * mounted subtree, never the wider host filesystem. * * ```ts * const rt = await NodeRuntime.create({ * mounts: [ * { * guestPath: "/root/node_modules/typescript", * hostPath: "/abs/path/to/node_modules/typescript", * readOnly: true, * }, * ], * }); * ``` */ mounts?: HostDirectoryMount[]; /** * Mount a host `node_modules` directory into the VM in one call so guest * `import`/`require` resolve real, host-installed npm packages. * * Pass the absolute host path to a `node_modules` directory (or an object * with that path and an explicit guest location). The whole directory is * projected lazily, Docker-style, at a guest `node_modules` on the resolution * path, so any package inside it resolves the way Node would over a real * filesystem (ancestor `node_modules` walk, `exports`/conditions, symlinks). * This is the ergonomic alternative to wiring up individual `mounts` entries * per package. * * By default the directory is mounted at `/tmp/node_modules`, which is where * the resolution walk for a program run by {@link NodeRuntime.exec} / * {@link NodeRuntime.run} begins (each program is written under `/tmp`). Pass * the object form with `guestPath` to mount it elsewhere on a different * module's resolution path. * * ```ts * const rt = await NodeRuntime.create({ * nodeModules: "/abs/path/to/project/node_modules", * }); * await rt.exec(` * import isNumber from "is-number"; * console.log(isNumber(42)); * `); * ``` * * The host filesystem is never exposed beyond the mounted `node_modules` * subtree. The mount is read-only. */ nodeModules?: string | NodeModulesMount; /** * Host-side bindings the guest can invoke as shell commands. Each entry is * registered as a named guest command; when the guest runs it, the * invocation round-trips back to the host and runs the binding's `handler`, * whose return value is delivered back to the guest. This is how you give * sandboxed guest code controlled, named host capabilities (the kind an AI * agent calls as tools) without granting it the underlying access directly. * * The guest invokes a binding by name with JSON input: * * ```ts * const rt = await NodeRuntime.create({ * bindings: { * add: { * description: "Add two numbers", * inputSchema: { * type: "object", * properties: { a: { type: "number" }, b: { type: "number" } }, * required: ["a", "b"], * }, * handler: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }), * }, * }, * }); * await rt.exec(` * import { execFileSync } from "node:child_process"; * const out = execFileSync("add", ["add", "--json", JSON.stringify({ a: 2, b: 3 })]); * console.log(out.toString()); * `); * ``` * * When `bindings` is provided and no `binding` permission scope is set, the * `binding` scope is granted so the registered bindings are invocable; pass * your own `permissions.binding` policy to gate individual bindings. */ bindings?: Record<string, BindingDefinition>; /** * Guest-bound ports that may accept non-loopback connections. By default a * guest server is reachable only over loopback inside the VM; listing a port * here lifts that restriction for that port, letting connections from outside * the loopback interface reach it. Use this for guests that run servers which * must accept external connections (for example a dev server you expose * beyond loopback). * * ```ts * const rt = await NodeRuntime.create({ * permissions: { network: "allow" }, * loopbackExemptPorts: [3000], * }); * ``` */ loopbackExemptPorts?: number[]; /** * Low-level guest JavaScript runtime configuration. Most callers should leave * this unset. Benchmarks may opt in to `highResolutionTime`, which disables * the default 1ms timer quantization and should not be enabled for untrusted * workloads. */ jsRuntime?: Partial<JsRuntimeConfig>; } /** A host directory projected into the VM's virtual filesystem. */ export interface HostDirectoryMount { /** Absolute guest path the directory appears at inside the VM. */ guestPath: string; /** Absolute host directory to project (read through the VFS, lazily). */ hostPath: string; /** Mount read-only (the default). Pass `false` to allow guest writes. */ readOnly?: boolean; } /** Guest path a `nodeModules` mount is projected at by default. */ const DEFAULT_NODE_MODULES_GUEST_PATH = "/tmp/node_modules"; /** * Object form of the `nodeModules` create option: a host `node_modules` * directory to project, optionally at an explicit guest path. The string form * (`nodeModules: "/abs/node_modules"`) is shorthand for `{ hostPath }`. */ export interface NodeModulesMount { /** Absolute host `node_modules` directory to project (read lazily). */ hostPath: string; /** * Absolute guest path to mount it at. Defaults to `/tmp/node_modules`, where * the resolution walk for {@link NodeRuntime.exec} / {@link NodeRuntime.run} * programs begins. Override to put it on a different module's resolution path. */ guestPath?: string; } /** Result of {@link NodeRuntime.exec}. */ export interface NodeRuntimeExecResult { stdout: string; stderr: string; exitCode: number; } /** Options for a single {@link NodeRuntime.exec} call. */ export interface NodeRuntimeExecOptions { /** Extra environment variables for this run, merged over the VM env. */ env?: Record<string, string>; /** Working directory for this run. */ cwd?: string; /** Data piped to the guest program's stdin. */ stdin?: string | Uint8Array; /** Abort the run after this many milliseconds. */ timeout?: number; /** * Cancel the run when this signal aborts. On abort the guest process is * killed inside the VM (the kernel delivers `SIGTERM`) and the call rejects * with the signal's abort reason. Use this to cancel an in-flight run from * the outside, for example to enforce your own deadline or stop work when a * request is canceled. * * ```ts * const controller = new AbortController(); * const pending = rt.exec("while (true) {}", { signal: controller.signal }); * controller.abort(); * await pending.catch((err) => console.log(err.name)); // "AbortError" * ``` */ signal?: AbortSignal; /** * Called with each chunk the guest writes to stdout as it is produced, * letting you observe output incrementally instead of waiting for the run to * finish. Chunks arrive as raw bytes; decode with a `TextDecoder` for text. * The complete output is still returned as `result.stdout` when the run ends. */ onStdout?: (chunk: Uint8Array) => void; /** * Called with each chunk the guest writes to stderr as it is produced, * letting you observe output incrementally instead of waiting for the run to * finish. Chunks arrive as raw bytes; decode with a `TextDecoder` for text. * The complete output is still returned as `result.stderr` when the run ends. */ onStderr?: (chunk: Uint8Array) => void; } /** The HTTP request {@link NodeRuntime.fetch} drives into the VM. */ export interface NodeRuntimeFetchInput { /** HTTP method. Defaults to `GET`. */ method?: string; /** Request path (and query), e.g. `/api/users?limit=10`. */ path: string; /** Request headers. */ headers?: Record<string, string>; /** Request body. */ body?: string | Uint8Array; } /** The HTTP response {@link NodeRuntime.fetch} returns from the VM. */ export interface NodeRuntimeFetchResponse { /** HTTP status code, e.g. `200`. */ status: number; /** HTTP status text, e.g. `OK`. */ statusText: string; /** Response headers, lower-cased by name. */ headers: Record<string, string>; /** Response body decoded as UTF-8 text. */ body: string; } /** * Options for {@link NodeRuntime.spawn}. Inherits the streaming `onStdout` / * `onStderr` hooks from {@link NodeRuntimeExecOptions}. */ export interface NodeRuntimeSpawnOptions extends NodeRuntimeExecOptions {} /** * Describes the guest TCP listener to wait for with * {@link NodeRuntime.waitForListener} or look up with * {@link NodeRuntime.findListener}. A listener matches when a guest process is * accepting connections on the given `port` (and `host`/`path` when supplied). */ export interface NodeRuntimeListenerQuery { /** TCP port the guest listener is bound to, e.g. `3000`. */ port: number; /** Bind host to match, e.g. `127.0.0.1`. Omit to match any host. */ host?: string; /** Unix socket path to match, for path-bound listeners. */ path?: string; } /** * A matched guest listener returned by {@link NodeRuntime.waitForListener} and * {@link NodeRuntime.findListener}. `processId` identifies the guest process * that owns the listening socket; the `host`/`port`/`path` it is bound to are * reported when known. */ export interface NodeRuntimeListener { /** The guest process id that owns the listening socket. */ processId: string; /** The host the listener is bound to, when reported. */ host?: string; /** The port the listener is bound to, when reported. */ port?: number; /** The unix socket path the listener is bound to, when reported. */ path?: string; } /** Options for {@link NodeRuntime.waitForListener}. */ export interface NodeRuntimeWaitForListenerOptions { /** * Give up after this many milliseconds and reject. Defaults to 10000. The * wait also rejects if the bound `signal` aborts first. */ timeoutMs?: number; /** Abort the wait early; the returned promise rejects when it fires. */ signal?: AbortSignal; /** * How long to wait between listener lookups while polling, in milliseconds. * Defaults to 50. */ pollIntervalMs?: number; } /** * A live handle to a guest process started with {@link NodeRuntime.spawn}. * * Unlike {@link NodeRuntime.exec}, which runs a program to completion and * returns its captured output, a handle is returned immediately while the * process keeps running. Use it to stream stdout/stderr, feed stdin, signal or * kill the process, and await its exit. This is the building block for * long-running guests such as dev servers: start one here, then drive requests * into it with {@link NodeRuntime.fetch}. */ export interface NodeRuntimeProcess { /** The guest process id. */ readonly pid: number; /** Write data to the guest process's stdin. */ writeStdin(data: string | Uint8Array): void; /** Close the guest process's stdin, signalling end-of-input. */ closeStdin(): void; /** * Send a signal to the guest process. Defaults to `SIGTERM`. Accepts a * signal name (e.g. `"SIGKILL"`) or a raw signal number. */ kill(signal?: NodeJS.Signals | number): void; /** Resolve with the guest process's exit code once it terminates. */ wait(): Promise<number>; /** The exit code once the process has exited, or `null` while it runs. */ readonly exitCode: number | null; } export interface NodeRuntimeResourceSnapshot { runningProcesses: number; exitedProcesses: number; fdTables: number; openFds: number;