cVisor — Node / Bun / Deno SDK #
One npm package for all three JS runtimes. Under Node it binds via a native
N-API module (libcvisor.node); under Bun and Deno the package's "bun" and
"deno" export conditions select FFI entries (bun:ffi / Deno.dlopen) over
libcvisor.so. All entries expose the same Sandbox / sh / Output API.
Linux only (ARM & x86, glibc & musl).
Install #
npm install cvisor # Node
bun add cvisor # Bun
deno add npm:cvisor # Deno (run with --allow-ffi)
Under Deno the sandbox needs FFI permission: deno run --allow-ffi ...
(plus --allow-env if you use the CVISOR_LIB override). The runtime is
picked automatically; cvisor/bun and cvisor/deno are also exposed as
explicit subpaths.
Usage #
import { Sandbox, sh } from "cvisor";
// Explicit sandbox — filesystem writes are isolated per sandbox.
const sb = new Sandbox();
const out = sb.runCmd("echo 'Hello, world!'");
console.log(await out.stdout()); // "Hello, world!\n"
// Tagged-template runner on a sandbox:
console.log(await sb.sh`uname -n`.stdout()); // "cvisor\n"
// Or the standalone `sh`, which uses a shared, lazily-created sandbox:
const files = await sh`ls -l ${"/tmp"}`.stdout();
runCmd(cmd) / sh\…`block until the command exits and return anOutput`:
interface Output {
stdout: () => Promise<string>;
stderr: () => Promise<string>;
stdoutStream: ReadableStream<Uint8Array>;
stderrStream: ReadableStream<Uint8Array>;
exitCode: number; // shell convention: status, or 128 + signal
}
Exit codes, timeouts, and network policy #
const { exitCode } = sb.runCmd("exit 3"); // 3
sb.runCmd("false").exitCode; // 1
// SIGKILL the guest after a deadline; a timed-out run reports 137 (128 + 9):
sb.runCmd("sleep 60", { timeoutMs: 500 }).exitCode; // 137
// Egress kill switch — deny outbound INET/INET6 sockets (default allowed):
sb.setAllowNetwork(false);
// Inbound TCP servers (bind fixed port, listen) — off by default:
sb.setAllowListen(true);
// Environment variables for the guest:
sb.setEnv("TOKEN", "xyz");
Files #
Transfer files in and out of the sandbox overlay; a file written this way is visible to later runs of the same sandbox:
sb.writeFile("/app/config.json", '{"k":1}');
await sb.runCmd("cat /app/config.json").stdout();
new TextDecoder().decode(sb.readFile("/tmp/result.txt"));
// Recursive directory copy (respects .gitignore / .dockerignore):
sb.copyInto("./src", "/app");
sb.copyOut("/app/dist", "./dist");
Cache #
Back up and restore a sandbox directory, keyed — for build caches, deps, etc. The bundled native library uses the host disk with gzip/estargz/none; S3 and zstd need a library built with those features.
sb.cacheSave("/app/node_modules", "deps-v1");
// ...later, in another sandbox — exact key or newest matching prefix:
sb.cacheRestore("/app/node_modules", "deps-v1");
// options: { backend: "s3://bucket/prefix", format: "estargz" }
Filesystem operations are virtualized (a copy-on-write overlay), and unsafe commands are blocked:
await sb.sh`echo hi > /tmp/test.txt`.stdout(); // only visible in this sandbox
await sb.sh`chroot /tmp`.stderr(); // blocked
Remote daemon (GraphQL) — works on macOS #
The Sandbox above needs the native libcvisor (Linux only). To use cVisor
from any platform, including macOS, talk to a running
cvisord over its GraphQL API. This path is pure
fetch + JSON — no native library — and importing it never loads the .so:
import { RemoteSandbox, GraphQLClient } from "cvisor";
const remote = new RemoteSandbox("http://127.0.0.1:8080/graphql", token);
const out = await remote.run("echo hello"); // { stdout: "hello\n", stderr: "", exitCode: 0 }
const sb = await remote.createSandbox("my-box");
await remote.writeFile(sb.id, "/app/x", "hi"); // base64 handled for you
new TextDecoder().decode(await remote.readFile(sb.id, "/app/x")); // "hi"
const snap = await remote.snapshot(sb.id);
await remote.fork(sb.id, "clone");
await remote.freeSandbox(sb.id);
RemoteSandbox mirrors the daemon surface: run, createSandbox /
listSandboxes / freeSandbox / configure, writeFile / readFile /
copyInto / copyOut, cacheSave / cacheRestore / cacheList, snapshot /
rollback / branch / fork / snapshots / deleteSnapshot, and health.
For raw documents, drop to the client:
const gql = new GraphQLClient("http://127.0.0.1:8080/graphql", token);
const { sandboxes } = await gql.query(`{ sandboxes { id name } }`);
await gql.mutate(`mutation($c:String!){ run(command:$c){ stdout } }`, { c: "uname -a" });
The daemon prints its bearer token on startup (or set CVISOR_TOKEN). On a
non-Linux host, constructing the FFI Sandbox throws a clear "Linux-only" error
pointing you here.
Development #
The native runtime is the Rust workspace at the repo root (crate
cvisor-node), built with cargo-zigbuild. From the repo root:
cargo xtask run-node # build libcvisor.node + run test.ts in a bun container
cargo xtask run-node --script examples/hello-world.ts
cargo xtask node-artifacts # build libcvisor.node for all 4 platform packages
libcvisor.node and libcvisor.so are produced per platform into
platforms/linux-<arch>-<libc>/. The napi loader (src/native.ts) and the
FFI loader (src/libpath.ts) resolve the right one at runtime via
detect-libc; CVISOR_LIB overrides the .so path. On macOS, npm install
skips the platform packages (os/cpu filtering), so use the Docker flow
above. The Bun/Deno e2e tests are test-bun.ts / test-deno.ts (run by CI in
Alpine containers with CVISOR_LIB set).
Publishing #
Bump versions across all packages, then publish:
bun run version:patch
bun run publish:all # builds the .node artifacts (cargo xtask node-artifacts) then publishes