Skip to content

Releases: torkbot/sandbox

v0.8.2

12 Jun 17:16
9a24671

Choose a tag to compare

Docker networking on the built-in kernel

This release updates the bundled guest kernel used by Sandbox so Docker's default bridge networking can work with modern iptables-nft userspace.

Users running Docker inside a Sandbox guest should no longer hit startup failures like:

Warning: Extension addrtype revision 0 not supported, missing kernel module?
iptables v1.8.x (nf_tables): RULE_APPEND failed

This is especially relevant for Alpine-based root filesystems that install Docker from apk, where Docker configures bridge/NAT rules through the nftables-backed iptables frontend.

v0.8.1

12 Jun 16:05
a9a01a7

Choose a tag to compare

This release makes writable sandboxes more predictable and improves compatibility with Docker-style networking inside the VM.

Highlights

  • Writable root filesystems now have explicit behavior. Use rootfs.ephemeral() when you want disposable guest changes, and use a persistent writable rootfs when those changes should survive across runs.
  • Docker's bridge networking startup path now has the kernel support it expects for common NAT and xtables rules, including addrtype, conntrack, comment, and masquerade matches.
  • The release build now verifies embedded kernel artifacts before packaging, so locally built and published packages are less likely to carry stale guest kernels after a libkrunfw update.
  • The vendored libkrun and libkrunfw forks have been refreshed to the current sandbox fork points.

Running Docker Successfully

For short-lived Docker work, start from an ephemeral rootfs with enough dirty capacity for /var/lib/docker, pulled image layers, package installs, and logs. If the writable budget is too small, Docker may start correctly but fail later while unpacking layers or creating containers.

If you need Docker state to survive between sandbox runs, use a persistent writable rootfs instead of rootfs.ephemeral().

When starting dockerd manually inside the guest, make sure the rootfs is writable and that the guest has the tools you expect in PATH. The built-in Alpine rootfs is intended to support common interactive and automation workflows, but daemon lifecycle is still your responsibility when you do not run it through an init/service manager.

Notes

This release does not change the public package split: @torkbot/sandbox continues to install the platform-specific host artifact as an optional dependency.

v0.8.0

11 Jun 16:46
793a811

Choose a tag to compare

This release redesigns process spawning around two explicit use cases: ordinary long-lived commands with streams, and interactive terminal sessions with a PTY. This is a breaking API change because the old spawn shape blurred those two modes and made it hard to build SSH-like clients without special cases.

Separate APIs for streamed processes and PTYs

Use sandbox.spawn() for non-interactive commands that may still run for a long time. It returns a process handle immediately with Web streams for stdin, stdout, and stderr, plus lifecycle promises for readiness and exit.

const process = sandbox.spawn("node", ["server.js"], {
  cwd: "/workspace",
});

await process.ready;

const logs = process.stdout
  .pipeThrough(new TextDecoderStream())
  .getReader();

await process.stdin.getWriter().write(new TextEncoder().encode("status\n"));

const exit = await process.exit;
console.log(exit);

Use sandbox.pty() when the guest process should believe it is attached to a terminal. This is the right shape for shells, REPLs, TUIs, and SSH-like clients that need terminal behavior instead of plain pipes.

const shell = sandbox.pty("/bin/bash", [], {
  cwd: "/workspace",
  size: { rows: 30, cols: 120 },
});

await shell.ready;

shell.output
  .pipeThrough(new TextDecoderStream())
  .pipeTo(hostTerminalWritable);

hostTerminalReadable.pipeTo(shell.input);

shell.resize({ rows: 40, cols: 140 });

Undefined args default to an empty argv

Callers can still omit the argument list for commands that do not need one. undefined args are treated as [] at the public boundary for exec, spawn, and pty.

await sandbox.exec("pwd");

const process = sandbox.spawn("sleep");
const shell = sandbox.pty("/bin/sh", {
  size: { rows: 30, cols: 120 },
});

Process lifecycle is now explicit

Spawned handles expose readiness separately from process completion. This lets callers wire up streams immediately while still detecting launch failures correctly. If a process exits before it reaches the ready state, ready rejects and exit still resolves with the process termination details.

const process = sandbox.spawn("false");

try {
  await process.ready;
} catch (error) {
  // The command failed before it became ready.
}

const exit = await process.exit;

Signal exits are reported as signal exits instead of being collapsed into only numeric exit codes when the guest can report the signal.

const process = sandbox.spawn("sleep", ["60"]);
await process.kill("SIGTERM");

const exit = await process.exit;
// { exitCode: null, signal: "SIGTERM" }

Abort and stream closure behavior is stricter

Already-aborted signals now prevent spawn() and pty() from launching guest commands. Once a spawned process or PTY has closed its input stream, later writes fail instead of being accepted and ignored.

const controller = new AbortController();
controller.abort();

try {
  sandbox.spawn("touch", ["/tmp/should-not-exist"], {
    signal: controller.signal,
  });
} catch (error) {
  // No guest command was launched.
}

Choosing the right process API

Use pty() for programs that draw a terminal UI, check whether stdin is a TTY, use line editing, or expect job-control behavior. Shells, language REPLs, editors, pagers, and SSH-like clients should generally use this API.

const shell = sandbox.pty("/bin/bash", [], {
  size: { rows: 30, cols: 120 },
});

Use spawn() for services, build tools, test runners, package managers, and other commands where stdin, stdout, and stderr should remain separate streams.

const process = sandbox.spawn("npm", ["test"]);

process.stdout
  .pipeThrough(new TextDecoderStream())
  .pipeTo(logWritable);

const exit = await process.exit;

When piping host input into the guest, treat writable failures as the normal signal that the guest no longer accepts input. For long-lived clients, stop the host-side pipe when exit resolves or when the writable rejects.

v0.7.20

11 Jun 02:52
54ea087

Choose a tag to compare

Sandbox now routes host-side network egress through a shared HostEgress path. Raw TCP, UDP relays, DNS upstreams, synthesized DNS answers, and intercepted HTTP/HTTPS forwarding use the same admission and dialing boundary, which makes host VPN, enterprise DNS, and future proxy-aware behavior easier to reason about.

HTTP interception also builds upstream TLS from the host native root store so enterprise-installed certificate authorities can be honored by the host side of the connection.

Idle network-service memory has been reduced by shrinking listener and relay buffer reservations while keeping a separate TCP DNS listener pool for concurrent DNS-over-TCP accepts.

v0.7.19

09 Jun 16:52
a1fe549

Choose a tag to compare

This release ships the first notarized macOS sandbox-host artifact.

The macOS platform package now uses a Developer ID signed and Apple-notarized host binary built from the exact successful main artifact run. CI also now fails closed if release artifacts do not match the release target commit.

CI hardening in this release pins GitHub Actions to immutable SHAs and updates the workflow runner stack to current Node.js and GitHub-hosted runner images.

v0.7.18

03 Jun 18:28
0b0c146

Choose a tag to compare

Fixes DNS-derived HTTP authority matching for delayed package-manager downloads against registries served by short-TTL CDN addresses.

Sandbox now keeps accepted DNS answer attribution for a bounded window even when the DNS TTL is very short. This lets later HTTPS connections to the same resolved registry IP continue matching conn.matchHttp("registry.npmjs.org") instead of falling back to raw IP endpoint policy.

The DNS answer remains the source of authority metadata. Sandbox still does not infer HTTP authority from HTTP Host headers or TLS SNI.

v0.7.17

03 Jun 17:02
c1fa20a

Choose a tag to compare

Fixes DNS-derived HTTP authority matching for registry and CDN responses that return usable IPv4 addresses outside the primary answer list.

conn.matchHttp(...) can now match authorities resolved through accepted DNS responses where a queried hostname follows a CNAME chain and the corresponding A record is supplied in the DNS additional section. The authority remains trusted DNS metadata; Sandbox still does not infer match authority from HTTP Host headers.

This release also adds e2e coverage for registry.npmjs.org-style multi-answer DNS responses and CNAME/additional-section address responses.

v0.7.16

03 Jun 15:56
b5508ab

Choose a tag to compare

Standard file descriptor links

Sandbox now creates the standard Linux file descriptor links during guest init:

/dev/fd -> /proc/self/fd
/dev/stdin -> /proc/self/fd/0
/dev/stdout -> /proc/self/fd/1
/dev/stderr -> /proc/self/fd/2

This fixes common shell and tool workflows that expect /dev/fd to exist, including Bash process substitution such as cat < <(printf hi).

v0.7.15

01 Jun 15:51
8540b39

Choose a tag to compare

Adds rootfs image flattening APIs for exporting COW-backed filesystems.

New APIs:

const source = rootfs.compose({
  base: rootfs.builtIn("alpine:3.23"),
  overlay: blockStore,
});

const image = await rootfs.flatten({
  format: "qcow2",
  source,
  dest: imageStore,
});

for await (const chunk of rootfs.bytes(image)) {
  await upload(chunk);
}

rootfs.cow() now uses the same composed source shape as flattening, so VM boot and offline image export share one base-plus-overlay model. Flattened images are written to caller-provided block storage and can be streamed as raw image container bytes for persistence to disk, object storage, or other sinks.

v0.7.14

31 May 21:02
ba97f0c

Choose a tag to compare

What's Changed

  • Added AbortSignal-backed cancellation for sandbox exec(...) calls.
  • Made the guest init control plane handle overlapping exec and spawn work concurrently.
  • Kept aborted exec calls from poisoning later control-plane requests, including descendants that keep output pipes open.
  • Documented abortable exec calls and streaming spawn(...) output.