@local-sandbox/lsb-nodejs
v0.7.0
Published
Native Node.js bindings for lsb microVM sandboxes
Readme
@local-sandbox/lsb-nodejs
Native Node.js bindings for lsb, built with
napi-rs.
This package is the canonical JavaScript and TypeScript entrypoint for lsb. It wraps the
Rust lsb-sdk directly and exposes a Node-facing Sandbox API.
Install
npm install @local-sandbox/lsb-nodejsThe published npm package is split into a root package plus a platform package. On supported hosts,
npm resolves and installs either @local-sandbox/lsb-nodejs-darwin-arm64 or
@local-sandbox/lsb-nodejs-darwin-x64 on macOS, or
@local-sandbox/lsb-nodejs-win32-x64-msvc on Windows x64, automatically.
For local development, use Corepack to run the Yarn version pinned in
package.json:
corepack yarn installRequirements
- Node.js 18+
- macOS 14+ on Apple Silicon or Intel x86_64, or Windows 11 on x86_64
- Runtime assets initialized with
initSandbox()orlsb init. On Windows, that initialization also installs LocalSandbox-managed QEMU host tools.Sandbox.start()still expects the lsb runtime data directory to already containImage,rootfs.ext4, andinitramfs.cpio.gz; it does not download assets or host tools implicitly. - On macOS, the
nodeexecutable loading this SDK must be code signed with thecom.apple.security.virtualizationentitlement. For a project-local workflow, sign a copied Node binary with../../lsb.entitlements, or usetest:signed-nodeas a reference. - On Windows, npm packages do not contain QEMU. Enable Windows Hypervisor
Platform, then initialize host tools through
initSandbox()orlsb init. The Windows backend requires WHPX and does not fall back to TCG for production Node users.LSB_QEMUandLSB_QEMU_IMGremain supported override/debug paths. - Windows support covers sandbox start/stop, non-interactive
exec()/execShell(), streamingspawn(), guest file APIs,watch(), overlay mounts, SMB/CIFS direct mounts, loopback port forwarding, policy-mediated proxy networking, and checkpoint restore/save. Interactive PTY shells remain macOS-only.
Usage
Experimental Windows service client
SeaWork integrations on Windows use the machine service instead of the direct Sandbox API. The
remote API contains no caller-selected runtime directory, QEMU path, instance ID, checkpoint, or
identity fields:
import { connectSeaWorkService } from '@local-sandbox/lsb-nodejs'
const service = await connectSeaWorkService({ connectTimeoutMs: 10_000 })
const info = await service.getServiceInfo()
const health = await service.healthCheck()
if (!health.ready) throw new Error(health.stableCode)
const sandbox = await service.startSandbox({
cpus: 2,
memoryMb: 2048,
diskSizeMb: 4096,
mounts: [{
type: 'direct',
hostPath: 'C:\\work\\project',
guestPath: '/workspace',
flags: 0,
pruneSubtrees: ['node_modules', '.seawork'],
}],
network: {
allow: ['api.example.com'],
secrets: { API_TOKEN: { value: 'secret', hosts: ['api.example.com'] } },
},
})
const result = await sandbox.exec(['sh', '-lc', 'echo service-ready'])
await sandbox.mkdir('/workspace/out', { recursive: true })
await sandbox.stop()
await service.close()This surface includes lifecycle, bounded unary/cancellable exec, credited process streams, guest
filesystem metadata and byte transfer, direct SMB mounts, and managed watches. Host ports remain
intentionally unavailable in the initial service scope. SeaWorkService.connect(), health(), and
start() remain compatibility aliases. The existing Sandbox API remains the direct SDK path.
SeaWork direct mounts prune node_modules and .seawork directory basenames from the recursive
SMB ACL startup traversal by default, at any depth and case-insensitively. Set pruneSubtrees to a
custom list to replace those defaults, or to [] to disable pruning. Pruning only affects startup
inspection and ACL work; it does not hide those paths from the SMB share.
The service start type preserves exact ports and network.exposeHost requests for the NET-02
contract, but does not claim those capabilities yet. Host ports return
PORT_ISOLATION_UNAVAILABLE, and expose-host relay requests fail feature negotiation until the
owner-token relay and Windows WFP isolation gates are implemented and enabled.
Start a sandbox and run commands
import { Sandbox, initSandbox } from '@local-sandbox/lsb-nodejs'
const init = await initSandbox()
const sandbox = await Sandbox.start({
dataDir: init.dataDir,
cpus: 2,
memoryMb: 2048,
mounts: [{ type: 'overlay', hostPath: './src', guestPath: '/workspace' }],
network: { allow: ['registry.npmjs.org'] },
})
const result = await sandbox.exec('echo hello from lsb')
console.log(result.stdout)
await sandbox.writeFile('/tmp/demo.txt', 'hello')
const content = await sandbox.readFile('/tmp/demo.txt')
console.log(content.toString())
await sandbox.stop()Initialize runtime assets
import { initSandbox } from '@local-sandbox/lsb-nodejs'
const init = await initSandbox()
console.log(init.dataDir, init.version, init.downloaded)initSandbox() defaults to this package version and pins that base rootfs.
Sandbox.start() defaults to the initialized VERSION in the runtime data directory. You only need
to pass a version when preparing or booting from an older pinned base.
To show first-start download progress, pass onProgress while keeping the returned promise as the
completion and error channel:
const init = await initSandbox({
onProgress(progress) {
if (progress.downloadedBytes !== undefined && progress.totalBytes !== undefined) {
const percent = Math.min(
100,
Math.floor((progress.downloadedBytes / progress.totalBytes) * 100),
)
process.stdout.write(`\r${progress.phase}: ${percent}%`)
} else {
console.log(progress.phase)
}
},
})
console.log(`\nready: ${init.dataDir}`)Download counters represent compressed response bytes consumed. totalBytes is omitted when the
server does not provide a valid positive Content-Length; downloadedBytes still advances in that
case. Runtime download and extraction are pipelined, so they appear as the single
downloading-and-extracting-runtime-assets phase.
On a first Windows startup, managed host tools and runtime assets are separate downloads. The byte
counter resets to zero when the runtime-assets download begins. Ready assets do not emit download
phases, while force: true downloads them again. Notifications are queued non-blockingly on the
JavaScript thread, so a slow handler does not control installation speed and the promise may settle
before the final queued callback runs. The callback return value is ignored and the callback must
not throw.
On Windows, pass fix: true from an elevated process to apply every available automatic host
configuration repair. The result reports each attempted fix and whether it changed the host.
const init = await initSandbox({ fix: true })
console.log(init.fixes) // [{ name: 'windows-smb-policy', changed: true }]await initSandbox({ version: '0.3.8' })
const sandbox = await Sandbox.start({ baseVersion: '0.3.8' })Pass argv directly or run through a shell
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const sandbox = await Sandbox.start()
const argvResult = await sandbox.exec(['sh', '-lc', 'printf "%s" "$HOME"'])
console.log(argvResult.stdout)
const shellResult = await sandbox.execShell('uname -a')
console.log(shellResult.stdout)
await sandbox.stop()Inspect the guest filesystem
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const sandbox = await Sandbox.start()
await sandbox.writeFile('/tmp/demo.txt', 'hello from lsb')
const entries = await sandbox.readDir('/tmp')
const stat = await sandbox.stat('/tmp/demo.txt')
const exists = await sandbox.exists('/tmp/demo.txt')
console.log(entries.map((entry) => `${entry.type}: ${entry.name}`))
console.log({ size: stat.size, mode: stat.mode, exists })
await sandbox.stop()Save and resume from a checkpoint
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const base = await Sandbox.start()
await base.exec('mkdir -p /workspace && echo ready > /workspace/state.txt')
await base.checkpoint('my-env')
const resumed = await Sandbox.start({ from: 'my-env' })
const state = await resumed.readFile('/workspace/state.txt')
console.log(state.toString())
await resumed.stop()Configure mounts, ports, secrets, and network policy
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const sandbox = await Sandbox.start({
cpus: 4,
memoryMb: 4096,
diskSizeMb: 8192,
ports: [{ host: 8080, guest: 80 }],
mounts: [{ type: 'overlay', hostPath: './src', guestPath: '/workspace' }],
network: {
allow: ['api.openai.com', 'registry.npmjs.org'],
exposeHost: [{ host: 3000, guest: 3000 }],
secrets: {
API_KEY: { value: 'sk-test', hosts: ['api.openai.com'] },
},
httpsInterception: {
enabled: true,
requestHeaders: [
{
name: 'User-Agent',
value: 'my-sandbox-agent/1.0',
hosts: { allow: ['api.openai.com'], deny: ['private.api.openai.com'] },
},
],
},
},
})
console.log(sandbox.instanceDir)
await sandbox.stop()httpsInterception is off by default. Rules without hosts are global; an
allow match is required when allow is present, and deny always wins. The
scope follows TLS SNI rather than the HTTP Host header. Interception supports
HTTP/1.1 over TCP port 443 with visible SNI and installs an ephemeral proxy CA
in the guest. Certificate pinning, mutual TLS, private trust stores, HTTP/2,
HTTP/3, QUIC, and TLS without usable SNI are unsupported. Connections without
an applicable header or secret remain blind tunnels.
Each rule has set semantics: on every request, existing instances of the named
header are removed case-insensitively and one configured value is inserted.
Header values may be sensitive, so prefer an allow scope instead of a global
rule for credentials or private identifiers. Explicitly empty allow or
deny arrays, duplicate names (ignoring case), and an enabled configuration
with no rules are rejected before the sandbox starts.
Routing, framing, proxy, and hop-by-hop headers such as Host,
Content-Length, Transfer-Encoding, Connection, Upgrade, and Expect
cannot be configured. A configuration may contain at most 64 rules, with a
128-byte name, an 8 KiB value, and 64 KiB total across names and values.
Secret substitution uses the same framing-aware HTTP/1.1 path. When a
fixed-length request body must be scanned for a secret placeholder, the proxy
streams it upstream with Transfer-Encoding: chunked so replacements cannot
leave a stale content length. Origins that reject chunked HTTP/1.1 request
bodies may be incompatible with body-based secret substitution.
Start options
| Option | Type | Description |
| ------------ | ----------------------------------- | ------------------------------ |
| instanceId | string | Stable instance directory name |
| from | string | Checkpoint name to start from |
| cpus | number | Number of vCPUs |
| memoryMb | number | Memory in MB |
| diskSizeMb | number | Disk size in MB |
| dataDir | string | lsb runtime data directory |
| ports | { host: number; guest: number }[] | Host-to-guest port forwards |
| mounts | MountConfig[] | Directory mounts |
| network | NetworkConfig | Network access policy |
mounts accepts discriminated entries:
| Type | Shape | Behavior |
| --------- | ------------------------------------------------------------------------ | --------------------------------------------- |
| overlay | { type: 'overlay'; hostPath: string; guestPath: string } | Host is read-only; guest writes go to overlay |
| direct | { type: 'direct'; hostPath: string; guestPath: string; flags: number } | Direct mount with libc flags |
For direct mounts, flags: 0 is read-write and flags: 1 is MS_RDONLY. On Windows, direct mounts
use SMB/CIFS, require an elevated Administrator shell, and use LocalSandbox-controlled proxy
networking without enabling arbitrary outbound network access. If Windows local security policy
denies network logon to NT AUTHORITY\Local account, direct mounts fail preflight; use
lsb doctor windows-smb-policy to diagnose or lsb doctor windows-smb-policy --fix to apply the
recommended local policy repair. lsb init --fix and initSandbox({ fix: true }) apply this repair
as part of initialization.
On Windows, watch() on a direct SMB mount path uses a host-side Windows directory watcher and maps
events back to guest paths. It reports host-created, modified, renamed, and deleted files, and it
also reports guest writes that go through the CIFS mount. Read-only direct mounts can still be
watched for host-originated changes while guest writes remain denied.
network enables proxy networking when present. It accepts:
| Option | Type | Description |
| ------------------- | ------------------------------------ | ---------------------------------------- |
| allow | string[] | Allowed outbound host patterns |
| exposeHost | { host: number; guest?: number }[] | Host ports exposed to the guest |
| secrets | Record<string, SecretConfig> | Secrets injected via the lsb proxy |
| httpsInterception | HttpsInterceptionConfig | Opt-in HTTPS request-header interception |
Stream process output
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const sandbox = await Sandbox.start()
const proc = await sandbox.spawn('echo out; echo err >&2')
for await (const chunk of proc.stdout) {
process.stdout.write(chunk)
}
console.log(await proc.exited)
await sandbox.stop()spawn() streams stdout and stderr on macOS and Windows x64. On Windows it runs over the
virtio-serial session mux and supports cwd, stdin writes, kill, non-zero exits, and concurrent
processes. Interactive PTY shells are still outside the Node API.
Watch files
import { Sandbox } from '@local-sandbox/lsb-nodejs'
const sandbox = await Sandbox.start()
const events = await sandbox.watch('/tmp')
for await (const event of events) {
console.log(event.path, event.event)
}watch() works on macOS and Windows x64. On Windows, normal guest paths and overlay/import mounts
use guest-side inotify over the session mux. Direct SMB mount paths use the host-side watcher
described above. A recursive Windows watch whose root is an ancestor of a direct SMB mount is
rejected; watch the direct mount target directly or start separate watches.
Scripts
corepack yarn build
corepack yarn test
corepack yarn test:signed-nodecorepack yarn test always builds the native binding first, then runs AVA against the
generated root entrypoint. The positive VM smoke tests only run when runtime assets already exist in
the platform default lsb data directory (~/.local/share/lsb on macOS/Linux,
%LOCALAPPDATA%\lsb on Windows) or in LSB_NODEJS_TEST_DATA_DIR (Image is expected there and
usually needs to be provisioned manually).
On macOS, positive VM tests also require the current node executable to have the
com.apple.security.virtualization entitlement. To avoid modifying your global Node installation,
use test:signed-node, which copies the current node binary into
.signed-node/node, signs that local copy with ../../lsb.entitlements,
prepends it to PATH, and then runs the local napi build --platform plus ava commands through
that signed Node:
corepack yarn test:signed-nodeIf runtime assets are missing, provision them in the lsb data directory first. On Windows, positive
VM tests require Windows 11 x64 with WHPX enabled and initialized managed QEMU host tools. The
generated build outputs (index.js, index.d.ts, lsb-nodejs.*.node) are local artifacts and are
ignored by git.
Platform Notes
- Supported targets: macOS on Apple Silicon (
aarch64-apple-darwin), macOS Intel (x86_64-apple-darwin), and Windows 11 x64 (x86_64-pc-windows-msvc/win32-x64-msvc). - Installation is limited to supported operating systems and CPU families where npm can express them. Unsupported platform packages should fail clearly instead of masking native-module load failures.
- npm cannot express supported OS/CPU pairs in the root package metadata, so Windows ARM64 may be
accepted by the root package metadata even though no Windows ARM64 native package is published.
The loader reports this as unsupported Windows architecture; only
win32-x64-msvcis supported for Windows. - The published native binaries live in the platform packages
@local-sandbox/lsb-nodejs-darwin-arm64,@local-sandbox/lsb-nodejs-darwin-x64, and@local-sandbox/lsb-nodejs-win32-x64-msvc. - If the Windows native package is missing, the load error should name
@local-sandbox/lsb-nodejs-win32-x64-msvcorlsb-nodejs.win32-x64-msvc.node. If the native module loads but QEMU, WHPX, or runtime assets are not ready,Sandbox.start()surfaces the Rust backend preflight error with the relevant remediation.
