BoxLite Node.js/TypeScript SDK
Embeddable VM runtime for secure, isolated code execution environments.
Access the agent API through JsBox.agents
and SimpleBox.agents for agent and turn methods, grouped options and error handling.
Think "SQLite for sandboxing" - a lightweight library embedded directly in your application without requiring a daemon or root privileges.
Features
- Hardware-level VM isolation (KVM on Linux, Hypervisor.framework on macOS)
- OCI container support - Use any Docker/OCI image
- Async-first API - Built on native async Rust + napi-rs
- Multiple specialized boxes:
SimpleBox- Basic command executionCodeBox- Python code sandboxBrowserBox- Browser automation (Puppeteer/Playwright)ComputerBox- Desktop automation (14 automation functions)InteractiveBox- PTY terminal sessions
- Zero daemon - No background processes required
- Cross-platform - macOS ARM64, Linux x86_64/ARM64
Installation
npm install @boxlite-ai/boxlite
Requirements:
- Node.js 18 or later
- Platform with hardware virtualization:
- macOS: Apple Silicon (macOS 12+)
- Linux: x86_64 or ARM64 with KVM (
/dev/kvmaccessible)
Not supported: macOS Intel, Windows (use WSL2 with Linux requirements)
Quick Start
JavaScript
import { SimpleBox } from '@boxlite-ai/boxlite';
async function main() {
const box = new SimpleBox({ image: 'alpine:latest' });
try {
const result = await box.exec('echo', 'Hello from BoxLite!');
console.log(result.stdout); // Hello from BoxLite!
} finally {
await box.stop();
}
}
main();
TypeScript
import { SimpleBox } from 'boxlite';
async function main() {
const box = new SimpleBox({ image: 'alpine:latest' });
try {
const result = await box.exec('echo', 'Hello from BoxLite!');
console.log(result.stdout);
} finally {
await box.stop();
}
}
main();
TypeScript 5.2+ (Async Disposal)
import { SimpleBox } from 'boxlite';
async function main() {
await using box = new SimpleBox({ image: 'alpine:latest' });
const result = await box.exec('echo', 'Hello!');
console.log(result.stdout);
// Box automatically stopped when leaving scope
}
main();
API Reference
Remote BoxLite server (REST)
Connect to a remote BoxLite server instead of the local runtime.
Construct an ApiKeyCredential and pass it positionally:
Boxlite.rest(url, credential).
import { JsBoxlite, ApiKeyCredential } from '@boxlite-ai/boxlite';
const rt = JsBoxlite.rest(
'http://localhost:8100',
new ApiKeyCredential('your-api-key'),
);
const boxes = await rt.listInfo();
// Env discovery — returns null when BOXLITE_API_KEY is unset:
const cred = ApiKeyCredential.fromEnv();
const rt2 = JsBoxlite.rest('http://localhost:8100', cred ?? undefined);
ApiKeyCredential structurally satisfies the exported Credential
interface; functions can type their parameter as Credential and
accept any future credential kind without a signature change.
Routing prefix (vendor-agnostic)
Box-scoped requests resolve to {url}/v1/{prefix}/…. The
v1 segment is hardcoded; path_prefix is an opaque, deployment-
defined routing value that the server tells the client to use via
Principal.path_prefix from GET /v1/me. The slot's semantics
are vendor-specific: boxlite cloud uses it for the organization
id; another deployment may use a workspace name, a region+team
pair, or any other multi-segment value such as us-east/team-42.
import { JsBoxlite, BoxliteRestOptions, ApiKeyCredential } from '@boxlite-ai/boxlite';
const rt = JsBoxlite.rest(new BoxliteRestOptions({
url: 'https://api.boxlite.ai',
credential: new ApiKeyCredential('blk_live_…'),
pathPrefix: 'acme', // → requests hit /v1/acme/boxes
}));
When pathPrefix is unset, the client builds URLs without the
segment (/v1/boxes/…) — the canonical shape for single-tenant
deployments such as the local boxlite serve reference server.
The CLI captures Principal.path_prefix at login and caches it
under the active profile, so subsequent boxlite commands route
correctly without an extra flag.
Runtime Registry Options
import { JsBoxlite } from 'boxlite';
const runtime = new JsBoxlite({
imageRegistries: [
{
host: 'registry.example.com',
auth: { username: 'user', password: 'password' },
},
{
host: 'registry.local:5000',
transport: 'http',
search: true,
},
],
});
SimpleBox
Basic container for command execution.
import { SimpleBox } from 'boxlite';
const box = new SimpleBox({
image: 'python:slim',
memoryMib: 512, // Memory limit in MiB
cpus: 2, // Number of CPU cores
name: 'my-box', // Optional name
autoRemove: true, // Auto-remove on stop (default: true)
workingDir: '/app',
network: {
outbound: {
mode: 'enabled',
allowNet: ['api.openai.com'],
},
inbound: {
mode: 'disabled',
},
},
// The pre-split shape `network: { mode, allowNet }` still works and
// configures the outbound direction. Passing it together with `outbound`
// is rejected.
advanced: {
capabilities: {
add: ['NET_ADMIN'],
drop: ['NET_RAW'],
},
},
env: { FOO: 'bar' },
volumes: [
// A host bind (local runtimes only)...
{ hostPath: '/tmp/data', guestPath: '/data', readOnly: false },
// ...or a managed volume, by id or by name (REST runtimes).
{ managedVolume: 'my-data', guestPath: '/cache' }
],
ports: [
{ hostPort: 8080, guestPort: 80 }
],
secrets: [
{
name: 'openai',
value: process.env.OPENAI_API_KEY!,
hosts: ['api.openai.com'],
}
]
});
// Execute command
const result = await box.exec('ls', '-la', '/');
console.log(result.exitCode, result.stdout, result.stderr);
// Execute with options (cwd, user, timeout)
const pwdResult = await box.exec('pwd', [], undefined, {
cwd: '/tmp',
user: 'nobody',
timeoutSecs: 30,
});
console.log(pwdResult.stdout); // "/tmp\n"
// Get box info
console.log(box.id); // ULID
console.log(box.name); // Optional name
const info = await box.info(); // Metadata
// null means unknown to this handle; [] means no active publications.
console.log(info.network?.publishedPorts);
// Local info reads do not query the live network backend; REST info performs a GET.
const refreshed = await runtime.getInfo(box.id);
console.log(refreshed?.network?.publishedPorts);
// Cleanup
await box.stop();
info(), getInfo(), and listInfo() expose privileged, the guest mode saved
at creation. Local metadata reports true or false; a REST server that omits
the field produces undefined.
Guest SSH Control
JsBox and SimpleBox expose box.ssh to configure, observe, and disable
the guest SSH listener.
import { readFile } from 'node:fs/promises';
const status = await box.ssh.configure({
listenAddress: '0.0.0.0:2222',
hostPrivateKey: await readFile('host_key', 'utf8'), // OpenSSH private key
accounts: [
{
login: 'alice',
authorizedKeys: [await readFile('alice.pub', 'utf8')],
// Certificate auth instead of authorizedKeys:
// ca: { publicKey: caPublicKey, principal: 'alice' },
},
],
});
// { enabled: true, generation: 1n, listenAddress, hostPublicKey,
// hostKeyFingerprint } — generation is a bigint (full u64).
await box.ssh.status();
await box.ssh.disable();
await box.ssh.enable(); // reuse saved configuration; requires a running box
See the Node.js SSH reference for configuration types, return values, and errors.
Runtime Image Management
runtime.images holds the images a runtime can boot from: the local cache on
an embedded runtime, the server's catalog on JsBoxlite.rest(...). The same
code runs against either; pull is local only (a REST runtime pulls when a
box is created) and usage, createAlias and deleteAlias are REST only.
import { JsBoxlite } from "boxlite";
const runtime = JsBoxlite.withDefaultConfig();
const images = await runtime.images.list();
for (const image of images) {
console.log(image.repository, image.tag, image.id);
}
const detail = await runtime.images.get("docker.io/library/alpine");
console.log(detail.aliases, detail.versions.map((version) => version.digest));
await runtime.images.remove("docker.io/library/alpine");
pull fills a local cache ahead of time. A REST runtime refuses it as
unsupported, because creating a box there pulls the image:
const pulled = await runtime.images.pull("alpine:latest");
console.log(pulled.reference, pulled.configDigest, pulled.layerCount);
See the Node.js reference for each method on each runtime.
Private Registry Logins
On JsBoxlite.rest(...), runtime.images.registries manages the logins the server
pulls private images with. The password goes up once and is never returned.
A local runtime throws unsupported: pass imageRegistries to its options
instead.
const login = await runtime.images.registries.create({
registryHost: "ghcr.io",
repositoryPrefix: "acme/", // omit for the whole registry
username: "acme-bot",
password: process.env.GHCR_TOKEN!,
});
const box = await runtime.create({ image: "ghcr.io/acme/private-app:1" });
// ...
await runtime.images.registries.remove(login.id); // refused while a box pulls through it
Live Execution Inventory
await box.executions().list() is available on native JsBox and SimpleBox.
It returns ExecutionInfo[] from the guest's live inventory, including the
main process: executionId (canonical guest ID), argv, startedAt
(Unix milliseconds), tty, and isMainSession. Completed processes disappear; this
is not execution history. Listing does not attach or consume output, and never
boots a stopped box. A stopped/unstarted native box returns an empty list; a
paused box returns an invalid-state error. An uncreated SimpleBox also
returns an empty list without creating its box.
Use await box.executions().get(executionId) for control-only access while
another client owns output, or await box.executions().attach(executionId, false)
to recover streams without stdin. Both are available on native JsBox and
SimpleBox, never start the box, and return a JsExecution supporting kill(),
signal(), and wait(). Only one output reader is permitted.
for (const execution of await box.executions().list()) {
console.log(execution.executionId, execution.argv, execution.isMainSession);
}
CodeBox
Python code execution sandbox.
import { CodeBox } from 'boxlite';
const codebox = new CodeBox({
image: 'python:slim', // default
memoryMib: 512,
cpus: 1
});
try {
// Run Python code
const result = await codebox.run(`
import math
print(f"Pi is approximately {math.pi}")
`);
console.log(result); // Pi is approximately 3.141592653589793
// Install packages
await codebox.installPackage('requests');
await codebox.installPackages('numpy', 'pandas');
// Use installed packages
const result2 = await codebox.run(`
import requests
response = requests.get('https://api.github.com/zen')
print(response.text)
`);
console.log(result2);
} finally {
await codebox.stop();
}
BrowserBox
Browser automation with remote debugging.
import { BrowserBox } from 'boxlite';
const browser = new BrowserBox({
browser: 'chromium', // 'chromium', 'firefox', or 'webkit'
memoryMib: 2048,
cpus: 2
});
try {
await browser.start();
// Get CDP endpoint
const endpoint = browser.endpoint();
console.log(endpoint); // http://localhost:9222
// Connect with Puppeteer
import puppeteer from 'puppeteer-core';
const browserInstance = await puppeteer.connect({ browserURL: endpoint });
const page = await browserInstance.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(title);
await page.close();
} finally {
await browser.stop();
}
ComputerBox
Desktop automation with web access.
import { ComputerBox } from 'boxlite';
const desktop = new ComputerBox({
cpus: 2,
memoryMib: 2048,
guiHttpPort: 3000, // default
guiHttpsPort: 3001 // default (self-signed cert)
});
try {
// Wait for desktop to be ready
await desktop.waitUntilReady(60);
// Mouse automation
await desktop.mouseMove(100, 200);
await desktop.leftClick();
await desktop.doubleClick();
await desktop.rightClick();
await desktop.leftClickDrag(100, 100, 200, 200);
const [x, y] = await desktop.cursorPosition();
console.log(`Cursor at: ${x}, ${y}`);
// Keyboard automation
await desktop.type('Hello, World!');
await desktop.key('Return');
await desktop.key('ctrl+c');
// Screenshot
const screenshot = await desktop.screenshot();
console.log(`${screenshot.width}x${screenshot.height} ${screenshot.format}`);
// screenshot.data contains base64-encoded PNG
// Scroll
await desktop.scroll(500, 300, 'down', 5);
// Get screen size
const [width, height] = await desktop.getScreenSize();
console.log(`Screen: ${width}x${height}`);
// Access desktop via browser:
// HTTP: http://localhost:3000
// HTTPS: https://localhost:3001 (self-signed certificate)
} finally {
await desktop.stop();
}
InteractiveBox
Interactive terminal sessions with PTY.
import { InteractiveBox } from 'boxlite';
const box = new InteractiveBox({
image: 'alpine:latest',
shell: '/bin/sh',
tty: true, // Auto-detected if undefined
memoryMib: 512,
cpus: 1
});
try {
await box.start();
await box.wait(); // Blocks until shell exits
} finally {
await box.stop();
}
Examples
See ../../examples/node/ directory for complete examples:
# If installed via npm
npm install @boxlite-ai/boxlite
node simplebox.js
# If working from source
cd ../../sdks/node
npm install && npm run build
npm link
cd ../../examples/node
npm link @boxlite-ai/boxlite
node simplebox.js
Error Handling
import { SimpleBox, ExecError, TimeoutError, ParseError } from 'boxlite';
try {
const box = new SimpleBox({ image: 'alpine:latest' });
await box.exec('false'); // Exit code 1
} catch (err) {
if (err instanceof ExecError) {
console.error(`Command failed: ${err.command}`);
console.error(`Exit code: ${err.exitCode}`);
console.error(`Stderr: ${err.stderr}`);
}
}
A failure the runtime itself reports carries a code that errorCode(err)
returns, such as not_found or invalid_state; it is undefined for any
other error. See the Node.js reference.
Building from Source
# Clone repository
git clone https://github.com/boxlite-ai/boxlite.git
cd boxlite/sdks/node
# Initialize submodules (critical!)
git submodule update --init --recursive
# Install dependencies
npm install
# Build (Rust + TypeScript)
npm run build
# Link to examples
npm link
cd ../../examples/node
npm link @boxlite-ai/boxlite
# Run examples
node simplebox.js
TypeScript Support
Full TypeScript support included:
import {
SimpleBox, CodeBox, BrowserBox, ComputerBox, InteractiveBox,
type SimpleBoxOptions,
type CodeBoxOptions,
type BrowserBoxOptions,
type ComputerBoxOptions,
type InteractiveBoxOptions,
type Secret,
type ExecResult,
type Screenshot,
type BrowserType
} from 'boxlite';
Platform Requirements
macOS
- Apple Silicon (ARM64)
- macOS 12+ (Monterey or later)
- Hypervisor.framework (built-in)
Linux
- x86_64 or ARM64
- KVM enabled (
/dev/kvmaccessible) - User in
kvmgroup:sudo usermod -aG kvm $USER # Logout/login required
Architecture
The SDK uses a dual-layer architecture:
-
Layer 1 (Rust): napi-rs bindings to native BoxLite runtime
- Direct mapping to Rust
boxlitecrate - Async operations via
env.spawn()(non-blocking) - Stream handling via async iterators
- Direct mapping to Rust
-
Layer 2 (TypeScript): Convenience wrappers
- Specialized box classes (CodeBox, BrowserBox, etc.)
- Error class hierarchy
- Output collection helpers
- TypeScript type definitions
Performance
- Startup time: < 100ms
- Overhead vs Python SDK: < 5%
- Concurrent boxes: Limited by system resources
- Memory footprint: ~50-100MB per box (depends on image)
Troubleshooting
"BoxLite native extension not found"
- Run
npm run buildto compile Rust bindings
"Image not found"
- BoxLite auto-pulls OCI images on first use
- Ensure internet connectivity
"Permission denied" (Linux)
- Check KVM access:
ls -l /dev/kvm - Add user to kvm group:
sudo usermod -aG kvm $USER
"Unsupported engine"
- Only Apple Silicon Macs supported (not Intel)
- Windows users: Use WSL2 with Linux requirements
Contributing
See ../../CONTRIBUTING.md for development guidelines.
Agent binding tests
From the repository root, run
make test:integration:node FILTER='agent namespace'. The native Box getter test
obtains a handle from a loopback REST server and checks namespace access without
starting a VM or sending agent requests. Separate tests reject plain-object and
prototype-only method receivers before the agent operation runs.
These receiver tests exercise napi-rs callback unwrapping. They do not exercise
the generated FromNapiRef and FromNapiMutRef conversions flagged by CodeQL,
so passing them does not establish that those pointer alerts are false positives.
The conversions are generated in the locked
napi-rs backend.
License
Apache 2.0 - See ../../LICENSE
Related Projects
- Python SDK - Python bindings (Python 3.10+)
- C SDK - C FFI bindings (early stage)
- Core Runtime - Rust runtime implementation
Support
- Issues: GitHub Issues
- Documentation: docs/
- Examples: examples/
Made with ❤️ by BoxLite Labs
Service tunnels
Use the built-in address from node:net to prepare a service tunnel:
import { SocketAddress } from "node:net";
const tunnel = await box.network.tunnel(
new SocketAddress({ address: "127.0.0.1", port: 3000 }),
{ isPublic: false },
);
Options default to public; private tunnels require a cloud runtime. IPv6 targets
use new SocketAddress({ address: "::1", family: "ipv6", port: 3000 }).
The target must have a nonzero port, a loopback or guest IP, and zero flowlabel.
Bare ports and the old { type: "tcp", ... } target are rejected. Invalid argument
types can throw synchronously on the native network handle. TCP/Unix listener
shapes remain available for tunnel.forward(listen).
See private tunnels.