Run code in isolated microVM sandboxes — from the command line, or embedded directly in your Node or Python app.
The same code runs on your laptop and in the cloud. One Machine API, two
transports: local boots a microVM on the machine you are sitting at (bundled
engine, no server, no account), cloud runs the identical calls on
smol cloud. Develop and test against a real VM offline, then move the same
program to managed capacity by changing where it connects — not what it does:
const spec = { image: 'alpine', network: true };
const dev = await Machine.create(spec); // local: this machine
const prod = await Machine.create(spec, { target: 'cloud' }); // smol cloudCreate, exec, files, state, stop, delete — and branching — behave the same on both. (A handful of local-only concepts, such as host bind mounts, are rejected up front on cloud rather than silently dropped.)
┌──────────────┐ ┌──────────────────────────── one Machine API ┐
│ smol CLI │ │ LocalTransport ──▶ embedded smolvm microVM │
│ (Rust) │ │ CloudTransport ──▶ smol cloud /v1 (REST) │
└──────────────┘ └──────────────────────────────────────────────┘
npm install smolmachines # Node
pip install smolmachines # Python
cargo add smolmachines # RustThe Rust crate does not bundle an engine: its local transport runs an installed
smolvm with the same major.minor version (and at least the crate's patch), or
downloads that smolvm release on first use.
Prebuilt packages bundle everything the local transport needs — the
libkrun libraries, a code-signed boot helper, and the guest rootfs — so
Machine.create() boots a microVM with no separate install (macOS Apple
Silicon and Linux x86_64/arm64, glibc ≥ 2.34). The SDK is in your process; the
VMM is not — the engine runs as a separate smol-vmm helper, confined by
seccomp and Landlock on Linux, so a guest escape lands there rather than in
your application. The cloud transport is
pure-language and runs anywhere. Booting a local microVM needs hardware
virtualization (macOS Hypervisor.framework or Linux /dev/kvm).
The standalone smol CLI (create / run / exec, pack .smolmachine
artifacts, container registry, cloud deploy) installs with a self-contained
bundle — no SDK or separate engine required:
curl -sSL https://raw.githubusercontent.com/smol-machines/smol/main/scripts/install.sh | bashThe script auto-detects your platform, downloads the matching release bundle
(which carries its own libkrun runtime + guest agent), verifies its checksum,
extracts it to ~/.smol, and symlinks smol onto your PATH (~/.local/bin).
Pin a release with SMOL_VERSION=v1.3.7; override locations with PREFIX /
BIN_DIR. Supports macOS Apple Silicon and Linux x86_64/arm64.
| Path | What |
|---|---|
sdk/node |
Node SDK — NAPI native core + TypeScript. Local (embedded) or cloud. |
sdk/python |
Python SDK — pyo3 native core + pure-Python layer. Same API. |
sdk/rust |
Rust SDK — drives a version-matched smolvm locally, or smol cloud over REST. Same API. |
src/ |
The smol CLI (Rust): create / run / exec / files / logs, plus cloud deploy + a container registry. |
docs/cli.md |
CLI command reference. |
Set a machine up once — toolchain, repo, dependencies, warm caches — then branch
it copy-on-write. A child inherits the source's RAM and disks, so nothing
re-installs: node_modules is there, caches are warm, a process mid-request
keeps running.
const source = await Machine.create({ image: 'alpine', network: true, branchable: true });
// ... install deps and warm the caches, once ...
const b = await source.branch('b1'); // typically under 200ms
await b.exec(['npm', 'test']); // full speed, isolated
const fleet = await source.branchBatch({ count: 8, namePrefix: 'run' });Branches are the unit of work for agents and CI: a fresh, VM-isolated machine per task without paying boot + setup each time. They work on both transports — locally against the machine in front of you, and on smol cloud, where a batch is atomic (all N branches or none). A branch is always node-local: it lives on the same host as its parent, because it shares that parent's memory pages.
import { Machine } from 'smolmachines';
// Local: no server, no config.
const m = await Machine.create({ resources: { cpus: 2, memoryMb: 1024, network: true } });
try {
const r = await m.run('python:3.12', ['python', '-c', 'print(2 ** 10)']);
console.log(r.stdout); // 1024
await m.writeFile('/tmp/in.txt', 'hi');
console.log((await m.readFile('/tmp/in.txt')).toString());
} finally {
await m.delete();
}
// Cloud: same API, just point at smol cloud.
const c = await Machine.create(
{ image: 'alpine:3.20' },
{ target: 'cloud' }, // uses SMOL_CLOUD_TOKEN
);
try {
// create() waits for "ready"; cloud state "started" only means VM launched.
console.log((await c.exec(['echo', 'ready for work'])).stdout);
} finally {
await c.delete();
}from smol import Machine, MachineConfig, ResourceSpec, ConnectOptions
# Local (embedded microVM) — context-managed, auto-deletes on exit.
with Machine.create(MachineConfig(resources=ResourceSpec(cpus=2, memory_mb=1024, network=True))) as m:
r = m.run("python:3.12", ["python", "-c", "print(2 ** 10)"]).assert_success()
print(r.stdout) # 1024
# Cloud — create() waits until the guest agent is reachable. A cloud state of
# "started" means only that the VM launched, not that it is ready for work.
m = Machine.create(
MachineConfig(image="alpine:3.20"),
ConnectOptions(target="cloud"), # uses SMOL_CLOUD_TOKEN
)
try:
print(m.exec(["echo", "ready for work"]).stdout)
finally:
m.delete()smol run python:3.12 -- python -c "print(2**10)" # ephemeral one-shot
smol machine create --name mybox --image alpine:3.20 --net # persistent machine
smol machine start --name mybox
smol machine exec --name mybox -- apk add curl
smol machine ls # lists local + cloud
smol machine rm --name mybox
# smol cloud
smol auth login
smol cloud deploy --image alpine:3.20
smol machine ls --cloudSee docs/cli.md for the full command reference, and run
smol <command> --help for flags.
On a CUDA rollout node, smol rollout declaratively registers a loopback vLLM
backend and publishes immutable LoRA versions without exposing vLLM's adapter
loader directly. Python and Node training loops use the matching
RolloutClient to submit cross-policy cohorts at their generation boundary;
unsupported work can retain an ordinary isolated fork pool as the fallback.
smol rollout executor ensure qwen --endpoint http://127.0.0.1:8000 \
--adapter-root /var/lib/smol/adapters --fallback-pool isolated-rollouts
smol rollout policy publish --executor qwen --policy experiment-a \
--version step-40 --adapter /var/lib/smol/adapters/experiment-a/step-40The command connects to the node API at http://127.0.0.1:8080/api/v1 by
default; override it with --api-url when the loopback listener uses another
port.
This repository is the open smol SDK + CLI (Apache-2.0). The microVM
engine it links — smolvm, which wraps libkrun — is also open source
(Apache-2.0), in its own repository:
smol-machines/smolvm. The
published packages bundle prebuilt engine binaries so you don't have to build it
yourself.
The Rust crates here (src/ CLI and the sdk/*/src native cores) declare a
path dependency on the engine (smolvm = { path = ".." }), so a native
build needs the smolvm repo checked out alongside this one — a checkout of
just this repo does not build standalone. You can still read and modify the
SDK/CLI source, the TypeScript/Python layers, tests, and docs without it; full
native builds are produced by maintainers' CI. Most contributions don't need a
native build — see CONTRIBUTING.md. To report a security
issue, see SECURITY.md.
