@torkbot/sandbox
v0.1.1
Published
A TypeScript-first Node.js library for spawning libkrun-backed microVMs.
Readme
Sandbox
Sandbox is a TypeScript-first Node.js library for spawning libkrun-backed microVMs.
The target shape is:
- boot a guest from a prebuilt read-only rootfs artifact, likely EROFS,
- mount host-implemented virtual filesystems,
- intercept guest HTTP request headers through host TypeScript hooks,
- communicate with guest init over a bidirectional transport,
- ship as a statically linked host artifact.
import {
acceptPublicInternet,
acceptTcp,
binding,
linuxOverlayFs,
mount,
prebuiltRootfs,
projectInit,
projectKernel,
scratchFs,
createSandbox,
type SandboxWritableFileSystem,
} from "@torkbot/sandbox";
declare const workspaceFs: SandboxWritableFileSystem;
await using sandbox = createSandbox({
kernel: projectKernel(),
init: projectInit(),
rootfs: linuxOverlayFs({
lower: prebuiltRootfs("dist/rootfs/sandbox.erofs", { format: "erofs" }),
upper: scratchFs(),
}),
mounts: [
mount("/sandbox", {
async stat(path) {
if (path === "/") {
return {
type: "directory",
sizeBytes: null,
mediaType: null,
modifiedAtMs: null,
};
}
if (path === "/status.json") {
const body = JSON.stringify({ ready: true });
return {
type: "file",
sizeBytes: Buffer.byteLength(body),
mediaType: "application/json",
modifiedAtMs: null,
};
}
throw new Error(`missing path ${path}`);
},
async list(path) {
if (path !== "/") throw new Error(`missing directory ${path}`);
return [{ name: "status.json", type: "file" }];
},
async read(input) {
if (input.path !== "/status.json") {
throw new Error(`unknown virtual file: ${input.path}`);
}
return Buffer.from(JSON.stringify({ ready: true }));
},
}),
],
bindings: [
binding("/workspace", workspaceFs),
],
network: {
outbound: {
policy: "deny",
rules: [
acceptTcp({ cidr: "127.0.0.1/32", ports: [8080] }),
acceptPublicInternet({ ports: [443] }),
],
},
},
});
sandbox.http.onRequest({ origin: "https://api.github.com" }, (request) => {
request.headers.set("authorization", `Bearer ${process.env.GITHUB_TOKEN}`);
});
await using vm = await sandbox.run();Incremental guest operations are explicit:
const result = await vm.control.exec({
id: "tests",
argv: ["node", "--test", "test/**/*.test.ts"],
});
if (result.exitCode !== 0) {
throw new Error(result.stderr);
}Mounted filesystems expose both the raw callback shape and a host-side tool surface for agent workflows:
const sandboxProc = vm.mounts.virtualFs("/sandbox");
const statusBytes = await sandboxProc.read({
path: "/status.json",
signal: AbortSignal.timeout(1_000),
});
console.log(JSON.parse(Buffer.from(statusBytes).toString("utf8")));
const workspace = vm.mounts.host("/workspace");
const notes = await workspace.read({
path: "notes.md",
offset: 1,
limit: 80,
});
await workspace.write({
path: "plan.md",
content: "# Plan\n\nStart here.\n",
});
await workspace.patch({
path: "plan.md",
edits: [{ oldText: "Start here.", newText: "Ship the narrow slice." }],
});
const grep = await workspace.bash({
command: "grep \"Ship\" plan.md",
timeoutMs: 1_000,
});Root filesystems are immutable by default. A writable root is expressed as an explicit Linux overlayfs composition:
await using sandbox = createSandbox({
kernel: projectKernel(),
init: projectInit(),
rootfs: linuxOverlayFs({
lower: prebuiltRootfs("dist/rootfs/base.erofs", { format: "erofs" }),
upper: scratchFs(),
}),
});
await using vm = await sandbox.run();
await vm.control.exec({
id: "install-toolchain",
argv: ["/bin/sh", "-lc", "apk add --no-cache git nodejs"],
});mount(...) means a guest-visible mount boundary. binding(...) means a host-side attachment point into the same filesystem abstraction and does not create a guest mount.
The guest contract is intentionally narrow:
/is read-only unless the rootfs is alinuxOverlayFs(...)composition./sandboxis implemented by the host.- HTTP request-header hooks are registered in TypeScript and enforced by the Rust host data plane.
- Network egress starts from deny; outbound rules opt in the exact protocols, ranges, and ports the guest can reach.
- The HTTP interception CA is generated and injected by Sandbox. Callers provide request-header hooks, not certificate plumbing.
Design Targets
- no dynamic
libkrunorlibkrunfwdependency in the final host artifact, - a signed
sandbox-hostprocess for the Node/Rust host boundary, - custom guest init owned by this repo,
- implicit fd-backed host control sockets owned by Sandbox,
- avoid host filesystem coordination unless it is intrinsic to the artifact; prefer file descriptors, database handles, bytes, and async iterables over paths,
- build-time rootfs shaping, with prebuilt rootfs artifacts supplied at VM instantiation,
- root filesystem composition through small explicit primitives such as
linuxOverlayFs(...)andscratchFs(), with lower and upper expressed as filesystem values, mount(...)only for guest-visible mounts;binding(...)only for host-side attachment points,- programmable virtual filesystems backed by TypeScript callbacks,
- transparent HTTP interception with TypeScript request-header hooks,
- default-deny outbound networking with explicit accept rules for protocols, CIDR ranges, public internet reachability, and ports,
- Rust-native or statically linkable networking components; sidecar network daemons are references, not default runtime dependencies,
- macOS HVF entitlement signing verified as part of the integration test flow.
Repository Layout
src/: TypeScript API consumed by Node.js callers.crates/sandbox-host: signed VM-host helper used for macOS HVF launch.crates/sandbox: Rust host implementation that owns the libkrun boundary and host services.crates/sandbox-init: custom guest init used to configure the guest before supervising untrusted code.tests/e2e: TypeScript e2e scenarios run directly by Node.js 24+ type stripping.
See docs/architecture.md for the initial design.
Kernel artifacts are built separately from runtime VM creation. See docs/kernel-build.md for the Docker-based deps/libkrunfw build entrypoint.
See docs/testing-strategy.md for the integration and e2e verification plan.
Publishing
The npm package is published as @torkbot/sandbox. It does not use post-install scripts. The root package contains the TypeScript API and declares platform artifacts as optional dependencies:
@torkbot/sandbox-darwin-arm64@torkbot/sandbox-linux-x64-gnu
Each platform package contains the N-API binding and the sandbox-host helper for that target. Runtime artifact resolution only loads the installed optional dependency for the current platform. Local development uses the same layout by materializing the current platform package under node_modules.
macOS signing setup
For now, the macOS sandbox-host artifact is not Developer ID signed or notarized. This is an explicit, possibly temporary workaround for publishing before this project has an Apple Developer account.
macOS users must sign the installed helper locally before launching a VM:
npx @torkbot/sandbox setup-macosThis performs an ad-hoc local codesign with the com.apple.security.hypervisor entitlement required by Hypervisor.framework. It does not contact Apple and does not require an Apple Developer account. If a macOS user tries to launch a VM before running setup, Sandbox throws a runtime error that points back to this command.
The release workflow verifies the tag, builds platform packages on their native runners, publishes the platform packages first, and then publishes the root package. That keeps the installable root package from pointing at missing optional artifacts while staying as close as npm allows to a single coordinated release operation.
Local release packaging sanity check:
npm run release:packAfter rebuilding local native artifacts, refresh the local optional package layout with:
npm run artifacts:link-current