smolmachines
v1.24.0
Published
Embed isolated microVM sandboxes directly in your code — no server to run.
Maintainers
Readme
smol (Node SDK)
Embed isolated microVM sandboxes directly in your Node.js code — no server to
run. The SDK is linked into your process via a native addon; the VMM itself is
a separate smol-vmm helper (seccomp- and Landlock-confined on Linux), so a
guest escape lands there and not in your application.
Supported platforms (native local transport): macOS Apple Silicon, and Linux x64/arm64 with glibc ≥ 2.34 (RHEL 9, Ubuntu 22.04+, Debian 12, Amazon Linux 2023). The cloud transport works anywhere the package installs. Not yet prebuilt: macOS Intel, and Linux with glibc < 2.34.
The local engine for your platform (native addon, boot helper, hypervisor libraries and guest rootfs) installs automatically as an optional dependency, e.g.
smolmachines-linux-x64-gnu, so an install downloads only your platform's runtime. Using only the cloud transport? Install withnpm install --omit=optional smolmachinesto skip it entirely.
Run the same code against the local embedded engine or smol cloud —
the backend is chosen by ConnectOptions:
// Local (embedded, default) — no server, no config:
const local = await Machine.create({ resources: { cpus: 2, memoryMb: 1024 } });
// A trusted local interceptor can inspect outbound TCP. Keep its token out of logs.
const intercepted = await Machine.create({
network: true,
egressInterceptor: { address: '127.0.0.1:9000', token: process.env.SMOLVM_INTERCEPTOR_TOKEN! },
});
// Supply the binding again when connecting from a new process:
// await Machine.connect(intercepted.name, { target: 'local', egressInterceptor: { address: '127.0.0.1:9000', token: process.env.SMOLVM_INTERCEPTOR_TOKEN! } });
// Branch a prepared machine: a CoW clone of its RAM and disks, typically
// under 200ms, so a warm environment is reused instead of rebuilt. Pass
// `network: true` whenever an image has to be pulled.
const source = await Machine.create({ image: 'alpine', network: true, branchable: true });
const branch = await source.branch('b1');
// Periodic local rollback points reuse unchanged RAM and disk chunks.
const first = await source.checkpoint('./points/1.smolcheckpoint', { store: './points/store' });
const second = await source.checkpoint('./points/2.smolcheckpoint', { store: './points/store' });
await Machine.restoreCheckpoint('./points/2.smolcheckpoint', 'restored');
Machine.exportCheckpoint('./points/2.smolcheckpoint', './point-2.smolcheckpoint');
Machine.pruneCheckpointStore('./points/store');
// smol cloud — pass an API key, or set SMOL_CLOUD_TOKEN.
const cloud = await Machine.create(
{ image: 'python:3.12' },
{ target: 'cloud' }, // uses SMOL_CLOUD_TOKEN
);
try {
// create() returned only after the guest agent became reachable.
const res = await cloud.exec(['python', '-c', 'print(40 + 2)']);
console.log(res.stdout);
} finally {
await cloud.delete();
}Cloud-only gaps (run, execStream, pullImage, listImages) throw NotSupportedError;
the common surface (create/exec/files/state/stop/delete) is identical on both.
Disposable workers: wait for ready, then connect (cloud)
Launching a machine as a disposable agent runtime has two easy-to-miss steps; both are first-class here.
Machine.create() already waits for the machine to be ready — not merely
started. state === "started" means the VM process launched; the guest is
still booting and is not usable yet. Acting on started is the classic
teardown race (works on a slow cold start, times out on a warm one). Gate on the
unambiguous signal:
const m = await Machine.create(
{ image, ports: [{ host: 8080, guest: 8080 }] },
{ target: 'cloud' },
);
try {
// create() has already waited: the guest agent is reachable and the
// published port is accepting connections.
const res = await m.fetch(8080, '/healthz');
console.log(await res.text());
} finally {
await m.delete();
}To reach a service inside the VM, use the authenticated connect bridge — no Cloudflare/localhost.run tunnel, no public exposure, no egress allow-list. Have the worker LISTEN on a published port and connect inbound:
// Machine.connect() does not wait. Explicitly gate a pre-existing machine:
const existing = await Machine.connect(machineId, { target: 'cloud' });
await existing.waitUntilReady();
// Or a WebSocket, using your own ws client with the authed endpoint:
const { wsUrl, headers } = existing.endpoint(8080, '/socket');
const ws = new WebSocket(wsUrl, { headers }); // e.g. the `ws` packageInstall
npm install smolmachinesRequires Node.js ≥ 18 on a host the engine supports (macOS Apple Silicon, or Linux with KVM).
Bun 1.3.14 and newer uses the same API, including zero-configuration local machines—the package automatically configures its bundled hypervisor, boot helper, and guest rootfs:
bun add smolmachines
bun run app.tsFused multi-policy rollouts
import { RolloutClient } from 'smolmachines';
const rollouts = new RolloutClient('http://127.0.0.1:8080/api/v1', 'qwen');
await rollouts.ensureVllmExecutor({
endpoint: 'http://127.0.0.1:8000',
adapterRoot: '/var/lib/smol/adapters',
fallbackPool: 'isolated-rollouts',
});
await rollouts.publishPolicy('experiment-a', 'step-40', '/var/lib/smol/adapters/a-40');
const result = await rollouts.generate({
idempotencyKey: 'experiment-a-step-40-batch-7',
policy: 'experiment-a',
prompts: [[1, 2, 3]],
sampling: { maxTokens: 64, temperature: 0.9, logprobs: 1 },
});Inside a branched rollout worker, new RolloutClient() discovers its authenticated
node assignment from /etc/smolvm/branch-env and automatically groups workers
from the same branch batch into a bounded cohort. Set autoForkCohort: false only
when the application already supplies an explicit cohort.
The client targets the loopback rollout API on a CUDA node; it publishes content-verified LoRA versions and submits cross-policy cohorts without exposing vLLM's unrestricted adapter loader.
Usage
import { Machine } from 'smolmachines';
const m = await Machine.create({ resources: { cpus: 2, memoryMb: 1024 } });
try {
// Run a command in a container image
const res = await m.run('python:3.12', ['python', '-c', 'print(2 ** 10)']);
res.assertSuccess();
console.log(res.stdout); // "1024\n"
// Or exec directly in the VM, move files in/out
await m.writeFile('/tmp/hello.txt', 'hi');
const back = await m.readFile('/tmp/hello.txt');
console.log(back.toString()); // "hi"
} finally {
await m.delete();
}API
await machine.pause() saves execution durably and stops the VM;
await machine.resume() restores its processes, RAM and disks under the same identity.
Use a branchable machine. Unlike stop/start, resume does not boot a fresh guest.
Local saves need the machine's data directory; cloud saves use object storage.
Existing network connections may need to reconnect.
Machine.create(config?, conn?)— create and start a machine; cloud waits forready === truebefore returning.Machine.connect(id, conn?)— attach to an existing machine without waiting; callwaitUntilReady()before use.Machine.list(conn?, { labels? })— every machine the target knows about, including ones other processes created, asMachineSummaryrows.machine.ready()/machine.readyAt()/machine.waitUntilReady({ timeoutMs, intervalMs })(cloud).machine.exec(command, opts?)/machine.run(image, command, opts?)→ExecResult.machine.execStream(command, opts?)→AsyncGenerator<ExecEvent>.machine.readFile(path)/machine.writeFile(path, data, mode?).machine.pullImage(image)/machine.listImages().machine.branch(name, options?)/machine.branchBatch(options).machine.checkpoint(output, { store? }),Machine.restoreCheckpoint(...),Machine.exportCheckpoint(...), andMachine.pruneCheckpointStore(...).machine.resize({ cpus?, memoryMb?, storageGb?, overlayGb? })(local) — add CPUs, RAM or disk to a running machine without rebooting it. Sizes are totals; returns the newMachineResources.machine.stop()/machine.delete()/await machine.state(). Cloud"started"means VM launched, not ready for work.
Errors are typed: SmolError (with .code), ExecutionError, NotSupportedError, InvalidConfigError.
Machines that outlive the process (local)
A local machine normally dies with the process that started it: the engine
arms a parent-death watchdog, and the SDK stops the machines it owns on
SIGINT/SIGTERM. A long-running host service whose own restarts must not take
its machines down opts out with detach, labels its machines so it can tell
them from anyone else's, and reclaims them after a restart with list +
connect:
await Machine.create({ name: 'worker-1', detach: true, labels: { owner: 'hostd' } });
// … this process is killed and starts again …
for (const m of await Machine.list({}, { labels: { owner: 'hostd' } })) {
const machine = await Machine.connect(m.name); // still running; no reboot
}detach implies persistent and is remembered by the machine, so later
starts and branches are detached too. smol machine ls shows the same
machines. Nothing reaps a detached machine but delete().
One prepared checkpoint, a network policy per session (local)
Prepare an environment once with open egress, checkpoint it, and start every session from it with its own network policy, applied before the session first boots:
const prep = await Machine.create({
image: 'node:22',
branchable: true,
resources: { network: true, networkBackend: 'virtio-net' },
});
await prep.exec(['npm', 'install', '-g', 'pnpm']);
await prep.checkpoint('prepared.smolcheckpoint');
const session = await Machine.restoreCheckpoint('prepared.smolcheckpoint', 'session-1', undefined, {
networkPolicy: { allowHosts: ['registry.npmjs.org', '*.github.com'] }, // or 'deny-all' / 'allow-all'
});allowHostsentries are exact names or*.subdomain wildcards, which don't match the bare domain.- A stopped machine's policy can be replaced with
machine.setNetworkPolicy(policy). - The prepared machine must use
networkBackend: 'virtio-net', which enforces allow lists on the host. A restored machine keeps its checkpoint's backend, so an allow list on a TSI checkpoint is refused rather than left unenforced.
Building from source
This package's native core lives alongside it (Rust, src/*.rs) and links the
sibling smolvm repo's engine + libkrun. From this directory:
npm install
npm run build # napi build (native) + tsc (types) + bundleThe native build needs the Rust toolchain, @napi-rs/cli, and libkrun available
in the smolvm repo's lib/ (this package expects the smolvm repo checked out
three levels up).
License
Apache-2.0
