Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/deeplethe/forkd/llms.txt

Use this file to discover all available pages before exploring further.

The @deeplethe/forkd TypeScript SDK is the Node.js 18+ client for the forkd-controller daemon. It exposes two classes: Controller for host-side VM lifecycle management (spawning, branching, killing sandboxes and managing snapshots) and Sandbox as a higher-level single-VM wrapper with an E2B/Daytona-compatible shape. Both classes are fully typed — type definitions are sourced from the wire-level api.rs shapes and kept in sync with the Python SDK. The fetch global introduced in Node 18 is used by default; older Node versions can pass a custom fetch implementation via ControllerOptions.

Installation

npm install @deeplethe/forkd
# or
pnpm add @deeplethe/forkd
Requires Node.js 18 or later (uses the global fetch).

ControllerOptions

Options accepted by the Controller constructor.
baseUrl
string
default:"$FORKD_URL → http://127.0.0.1:8889"
Daemon base URL. Resolved in order: options.baseUrl, the FORKD_URL environment variable, then the hardcoded default. Trailing slashes are stripped.
token
string
default:"$FORKD_TOKEN"
Bearer token sent as Authorization: Bearer <token>. Defaults to the FORKD_TOKEN environment variable. Required only when the daemon was started with --token-file.
timeoutMs
number
default:"60000"
Per-request timeout in milliseconds. Branching a large source VM can take several seconds; the default is generous. An AbortController enforces this on every fetch call.
fetch
typeof fetch
Custom fetch implementation. Defaults to globalThis.fetch. Provide undici’s fetch for test scenarios that need request recording, or a polyfill for Node versions prior to 18.

Controller

Controller is the primary client for the forkd-controller REST API.

Constructor

import { Controller } from '@deeplethe/forkd';

const ctrl = new Controller({
  baseUrl: 'http://127.0.0.1:8889',  // or env FORKD_URL
  token: process.env.FORKD_TOKEN,    // or env FORKD_TOKEN
  timeoutMs: 60_000,
});
Throws Error if no fetch implementation is available (Node 17 or earlier without a polyfill).

Snapshot methods

listSnapshots

listSnapshots(): Promise<SnapshotInfo[]>
GET /v1/snapshots — returns every snapshot registered with the daemon.
returns
Promise<SnapshotInfo[]>
Array of SnapshotInfo objects. See type definitions for the full shape.

deleteSnapshot

deleteSnapshot(tag: string): Promise<void>
DELETE /v1/snapshots/:tag — removes a snapshot from the registry and deletes its on-disk files.
tag
string
required
The snapshot tag to delete.

Sandbox methods

spawnSandboxes

spawnSandboxes(options: {
  snapshotTag: string;
  n?: number;
  perChildNetns?: boolean;
  memoryLimitMib?: number;
  prewarm?: boolean;
  liveFork?: boolean;
  hugepages?: boolean;
}): Promise<SandboxInfo[]>
POST /v1/sandboxes — fork n children from a registered snapshot tag.
options.snapshotTag
string
required
Name of a registered snapshot to fork from. Must exist in listSnapshots().
options.n
number
default:"1"
Number of child sandboxes to spawn, 1–1000.
options.perChildNetns
boolean
default:"false"
When true, each child is placed in a dedicated network namespace forkd-child-<i>. The host must have run scripts/netns-setup.sh N beforehand.
options.memoryLimitMib
number
Sets the cgroup memory.max for each child in MiB.
options.prewarm
boolean
default:"false"
v0.2.5+. When true, each child performs a throwaway snapshot immediately after restore to fault in all guest pages. Trades ~170 ms / 512 MiB of extra spawn time for predictable BRANCH latency. Useful when fanning out 3+ sandboxes from the same source with a BRANCH SLO.
options.liveFork
boolean
default:"false"
v0.4+. Boots the sandbox with a memfd-backed RAM region so later BRANCHes can use mode: "live" (UFFD_WP). Requires Linux kernel 5.7+ and the vendored Firecracker fork.
options.hugepages
boolean
default:"false"
v0.4+. Backs the memfd with 2 MiB hugepages. Only meaningful with liveFork: true. Reduces TLB pressure during spawn-many and live BRANCH bulk-copy. Requires free hugepages in /proc/meminfo.
returns
Promise<SandboxInfo[]>
Array of SandboxInfo objects, one per spawned child.

listSandboxes

listSandboxes(): Promise<SandboxInfo[]>
GET /v1/sandboxes — returns every live sandbox the daemon tracks.
returns
Promise<SandboxInfo[]>
Array of SandboxInfo objects.

getSandbox

getSandbox(id: string): Promise<SandboxInfo>
GET /v1/sandboxes/:id — fetch metadata for one sandbox.
id
string
required
The sandbox id (e.g. "sb-67a1b3-0000").
returns
Promise<SandboxInfo>
A single SandboxInfo object.

killSandbox

killSandbox(id: string): Promise<void>
DELETE /v1/sandboxes/:id — terminate one sandbox.
id
string
required
The sandbox id to terminate.

branchSandbox

branchSandbox(sandboxId: string, options?: BranchOptions): Promise<SnapshotInfo>
POST /v1/sandboxes/:id/branch — pause the source, write a snapshot, resume. Returns the new snapshot. Pass its tag to spawnSandboxes to fan out grandchildren that inherit the source’s exact state.
sandboxId
string
required
Id of the sandbox to branch from.
options.tag
string
Optional tag for the new snapshot. When unset, the daemon generates branch-<sandbox-id>-<unix-ts>.
options.mode
BranchMode
v0.4+ canonical mode selector ("full", "diff", or "live"). Prefer this over the legacy diff boolean. Mutually exclusive with diff — passing both yields HTTP 400.
  • "full" — copy entire guest RAM (0.5–8 s pause). Default for v0.x.
  • "diff" — Firecracker Diff snapshot (v0.3+). ~200 ms idle source; 6–15× speedup on typical agent workloads.
  • "live" — UFFD_WP (v0.4+). Sub-50 ms source pause. Source must have been spawned with liveFork: true.
options.diff
boolean
default:"false"
Legacy. Equivalent to mode: "diff". Kept for v0.3.x daemon compatibility. Mutually exclusive with mode.
options.measure_diff
boolean
default:"false"
v0.3+. Measurement-only hook. Takes a Diff snapshot inside the Full pause to report what diff would have cost, without changing semantics.
options.wait
boolean
default:"true"
v0.4+, only meaningful with mode: "live". Default true blocks until the background copy finishes (status: "ready"). Set to false to return after the source resumes (~10 ms); poll listSnapshots() for status: "ready".
returns
Promise<SnapshotInfo>
A SnapshotInfo object. When mode: "live" with wait: false, the initial status is "writing".

execCommand

execCommand(
  sandboxId: string,
  args: string[],
  options?: { timeoutSecs?: number },
): Promise<ExecResult>
POST /v1/sandboxes/:id/exec — run a subprocess inside a sandbox.
sandboxId
string
required
The target sandbox id.
args
string[]
required
Argv list (e.g. ["python3", "-c", "print(2+2)"]).
options.timeoutSecs
number
default:"30"
Maximum seconds to wait for the command.
returns
Promise<ExecResult>
{ stdout: string, stderr: string, exit_code: number }

evalCode

evalCode(sandboxId: string, code: string): Promise<EvalResult>
POST /v1/sandboxes/:id/eval — evaluate code against the sandbox’s warmed PID-1 process.
sandboxId
string
required
The target sandbox id.
code
string
required
Python expression to evaluate. Pre-imported packages like numpy are in scope if the parent snapshot included them.
returns
Promise<EvalResult>
{ result: unknown, error: string | null, exit_code: number }

pingSandbox

pingSandbox(sandboxId: string): Promise<PingResult>
POST /v1/sandboxes/:id/ping — round-trip health check to the in-guest agent.
sandboxId
string
required
The target sandbox id.
returns
Promise<PingResult>
An object whose shape depends on the recipe. Guaranteed to contain at least pong.

Sandbox

Sandbox is a higher-level wrapper around a single live sandbox. Its shape mirrors E2B and Daytona SDKs so existing agent code can swap in forkd with minimal changes.

Sandbox.create

static async create(options: {
  snapshotTag: string;
  perChildNetns?: boolean;
  memoryLimitMib?: number;
  prewarm?: boolean;
} & ControllerOptions): Promise<Sandbox>
Spawns one sandbox and returns a Sandbox instance that owns it. The most common entry point.
const sb = await Sandbox.create({
  snapshotTag: 'python-3-12-slim',
  prewarm: true,
  token: process.env.FORKD_TOKEN,
});
try {
  const r = await sb.exec(['python3', '-c', 'print(2+2)']);
  console.log(r.stdout); // "4\n"
} finally {
  await sb.kill();
}
static async with<T>(
  options: Parameters<typeof Sandbox.create>[0],
  fn: (sb: Sandbox) => Promise<T>,
): Promise<T>
Convenience pattern: spawn + run callback + kill, with guaranteed cleanup even on exception.
const result = await Sandbox.with(
  { snapshotTag: 'python-3-12-slim' },
  async (sb) => sb.exec(['python3', '-c', 'print(2+2)']),
);
console.log(result.stdout); // "4\n"

Constructor (low-level attach)

new Sandbox(controller: Controller, info: SandboxInfo)
Attach to a sandbox that was already spawned via Controller.spawnSandboxes. Useful when you fan out many children and want to wrap each one individually.
const [info] = await ctrl.spawnSandboxes({ snapshotTag: 'pyagent', n: 1 });
const sb = new Sandbox(ctrl, info);

Instance methods

exec

exec(args: string[], options?: { timeoutSecs?: number }): Promise<ExecResult>
Run a subprocess in the sandbox. Delegates to Controller.execCommand.

eval

eval(code: string): Promise<unknown>
Evaluate code against the warmed PID-1. Delegates to Controller.evalCode. Throws Error when the eval returns an error.

ping

ping(): Promise<Record<string, unknown>>
Round-trip to the in-guest agent for health and version info.

branch

branch(options?: {
  tag?: string;
  diff?: boolean;
  measure_diff?: boolean;
}): Promise<SnapshotInfo>
Branch this sandbox into a new snapshot tag. Pass the returned tag to Controller.spawnSandboxes to fan out grandchildren.

kill

kill(): Promise<void>
Terminate the sandbox. Idempotent — calling it multiple times is safe.

Type definitions

All types are re-exported from @deeplethe/forkd. Wire-level field names use snake_case (matching the daemon’s REST API); TypeScript argument names use camelCase where noted.

SandboxInfo

interface SandboxInfo {
  id: string;
  snapshot_tag: string;
  netns: string | null;
  guest_addr: string;
  created_at_unix: number;
  pid: number | null;
  memory_limit_mib: number | null;
  /** v0.3+: any BRANCH has been taken from this sandbox. */
  has_branched?: boolean;
  /** v0.3.1+: chain head for the next diff BRANCH. */
  last_branch_memory_path?: string | null;
}

SnapshotInfo

interface SnapshotInfo {
  tag: string;
  dir: string;
  created_at_unix: number;
  /** Set when produced by BRANCH; the source sandbox id. */
  branched_from?: string;
  /** v0.2.5+: source-VM pause window in ms during BRANCH. */
  pause_ms?: number;
  /** v0.3+: time spent in the Diff snapshot call (subset of pause_ms). */
  diff_ms?: number;
  /** v0.3+: on-disk bytes of the diff = dirty page count. */
  diff_physical_bytes?: number;
  /** v0.3+: full guest-RAM size (what a Full snapshot would have written). */
  diff_logical_bytes?: number;
  /**
   * v0.4+: live BRANCH lifecycle marker.
   * "writing" while the background memory copy is in flight (wait: false),
   * "ready" once the snapshot is consumable,
   * "failed" if the background copy hit an error.
   */
  status?: "writing" | "ready" | "failed";
}

BranchMode

type BranchMode = "full" | "diff" | "live";
ValuePause windowNotes
"full"0.5–8 sCopies entire guest RAM. Default for v0.x.
"diff"~200 msFirecracker Diff snapshot (v0.3+). 6–15× speedup on typical workloads.
"live"sub-50 msUFFD_WP (v0.4+). Source must be spawned with liveFork: true.

SpawnOptions

Wire-level body for POST /v1/sandboxes. Used internally by Controller.spawnSandboxes.
interface SpawnOptions {
  snapshot_tag: string;
  n?: number;
  per_child_netns?: boolean;
  memory_limit_mib?: number;
  prewarm?: boolean;
  live_fork?: boolean;
  hugepages?: boolean;
}

BranchOptions

interface BranchOptions {
  tag?: string;
  mode?: BranchMode;
  /** Legacy: equivalent to mode: "diff". Kept for v0.3.x daemon compat. */
  diff?: boolean;
  measure_diff?: boolean;
  /** v0.4+: default true. Set false for fire-and-forget live BRANCH. */
  wait?: boolean;
}

ExecResult

interface ExecResult {
  stdout: string;
  stderr: string;
  exit_code: number;
}

EvalResult

interface EvalResult {
  result: unknown;
  error: string | null;
  exit_code: number;
}

PingResult

interface PingResult {
  [key: string]: unknown;
}
Shape is recipe-specific; stable fields per recipe are documented in recipes/.

ControllerError

Thrown on any non-2xx response from the daemon.
class ControllerError extends Error {
  readonly status: number;   // HTTP status code
  readonly body: unknown;    // Parsed JSON or raw string
  readonly url: string;      // Full URL of the failed request
}
import { ControllerError } from '@deeplethe/forkd';

try {
  await ctrl.getSandbox('sb-missing');
} catch (e) {
  if (e instanceof ControllerError && e.status === 404) {
    console.log('sandbox not found');
  } else {
    throw e;
  }
}

Python vs TypeScript comparison

from forkd import Controller

c = Controller()
children = c.spawn_sandboxes(
    "pyagent",
    n=1,
    per_child_netns=True,
    live_fork=True,
)
sb_id = children[0]["id"]

branch = c.branch_sandbox(sb_id, mode="live", wait=False)
grandchildren = c.spawn_sandboxes(branch["tag"], n=5)

Full example

This example spawns a live-fork-capable sandbox, does work inside it, branches live with fire-and-forget, then fans out five grandchildren.
import { Controller, Sandbox, ControllerError } from '@deeplethe/forkd';

const ctrl = new Controller({ token: process.env.FORKD_TOKEN });

// ── Quick path: spawn + use + auto-cleanup ────────────────────────────
const echoResult = await Sandbox.with(
  { snapshotTag: 'python-3-12-slim', token: process.env.FORKD_TOKEN },
  async (sb) => sb.exec(['python3', '-c', 'import numpy; print(numpy.zeros(3))']),
);
console.log(echoResult.stdout); // "[0. 0. 0.]\n"

// ── Long-lived path: BRANCH + fan-out ────────────────────────────────
// Spawn a parent with memfd-backed RAM (prerequisite for mode: "live").
const [parent] = await ctrl.spawnSandboxes({
  snapshotTag: 'pyagent',
  n: 1,
  perChildNetns: true,
  liveFork: true,
});
console.log(`spawned ${parent.id} at ${parent.guest_addr}`);

// Drive the parent via exec.
const warmup = await ctrl.execCommand(parent.id, [
  'python3', '-c', 'import numpy, torch; print("warmed")'
]);
console.log(warmup.stdout.trim()); // "warmed"

// BRANCH live: source pause drops to sub-50 ms.
// wait: false returns after the source resumes (~10 ms).
const branch = await ctrl.branchSandbox(parent.id, {
  tag: 'checkpoint-1',
  mode: 'live',
  wait: false,
});
console.log(`branch ${branch.tag} status=${branch.status}`); // "writing"

// Poll until the background copy completes.
let ready = false;
while (!ready) {
  await new Promise((r) => setTimeout(r, 200));
  const snapshots = await ctrl.listSnapshots();
  const snap = snapshots.find((s) => s.tag === branch.tag);
  if (snap?.status === 'ready') ready = true;
  if (snap?.status === 'failed') throw new Error('live branch failed');
}

// Fan out 5 grandchildren from the checkpoint.
const kids = await ctrl.spawnSandboxes({ snapshotTag: branch.tag, n: 5 });
console.log(`spawned ${kids.length} grandchildren`);

// Cleanup
await ctrl.killSandbox(parent.id);
await Promise.all(kids.map((k) => ctrl.killSandbox(k.id)));

Build docs developers (and LLMs) love