Vanilla vs Pilot — A/B report

App io.pilot.smolvm · delivered from the Pilot R2 artifact registry · generated by scripts/ab_report.py

Summary

CommandVanilla (ms)Pilot (ms)Δ overheadMatch
Version60833+773
List machines42456+414
Run command in an ephemeral Alpine microVM58545492-362
Compute in a Python microVM93749501+127

Adapter-generated help — smolvm.help (local, no backend), 471 ms

Pilot · smolvm.help (generated by the adapter)
{
  "app": "io.pilot.smolvm",
  "version": "1.2.0",
  "description": "Run portable, lightweight microVMs (smol machines) from the app store.",
  "duration_classes": {
    "fast": "<~1s \u2014 status or cheap call",
    "med": "~1-5s \u2014 moderate work",
    "slow": "~5-30s \u2014 heavy / multi-step"
  },
  "methods": [
    {
      "method": "smolvm.version",
      "kind": "utility",
      "summary": "Print the smolvm version.",
      "duration": "fast"
    },
    {
      "method": "smolvm.exec",
      "kind": "utility",
      "summary": "Run any smolvm subcommand; payload {\"args\":[...]}.",
      "params": {
        "args": "array \u2014 verbatim argv forwarded to smolvm"
      },
      "duration": "slow"
    },
    {
      "method": "smolvm.help",
      "kind": "meta",
      "summary": "This document \u2014 every method with params, kind, and duration class.",
      "duration": "fast",
      "typical_roundtrip": "instant (local, no backend call)"
    }
  ]
}
Vanilla · smolvm --help (37 ms)
# smolvm — Agent Reference

A tool to build and run portable, self-contained virtual machines locally. <200ms boot time. No daemon, no Docker.

## Quick Reference

```bash
# Ephemeral (cleaned up after exit)
smolvm machine run --net --image alpine -- echo hello
smolvm machine run --net -it --image alpine -- /bin/sh   # interactive shell
smolvm machine run --net --image python:3.12-alpine -- python3 script.py

# Persistent (survives across exec sessions and stop/start)
smolvm machine create --net --name myvm
smolvm machine start --name myvm
smolvm machine exec --name myvm -- apk add python3   # installs persist
smolvm machine exec --name myvm -- which python3      # still there
smolvm machine shell --name myvm               # interactive shell (auto-starts if stopped)
smolvm machine stop --name myvm
smolvm machine delete --name myvm

# Image-based persistent (filesystem changes persist across exec sessions)
smolvm machine create --net --image ubuntu --name myvm
smolvm machine start --name myvm
smolvm machine exec --name myvm -- apt-get update
smolvm machine exec --name myvm -- apt-get install -y python3
smolvm machine exec --name myvm -- which python3      # still there after exit+re-exec

# SSH agent forwarding (git/ssh without exposing keys)
smolvm machine run --ssh-agent --net --image alpine -- ssh-add -l
smolvm machine create --name myvm --ssh-agent --net

# Inject secrets into workload env (referenced from host env var / file)
smolvm machine run --secret-env OPENAI_API_KEY=OPENAI_API_KEY -- ./app
smolvm machine run -s Smolfile -- ./app   # Smolfile [secrets] resolves at launch

# Pack into portable executable
smolvm pack create --image python:3.12-alpine -o ./my-python
./my-python run -- python3 -c "print('hello')"

# Create machine from packed artifact (fast start, no pull)
smolvm machine create --name my-vm --from ./my-python.smolmachine
smolvm machine start --name my-vm
smolvm machine exec --name my-vm -- pip install requests

# Use local container images (CI, air-gapped, fast iteration)
docker save myapp:latest -o myapp.tar
smolvm machine run --image ./myapp.tar -- ./app           # from a docker/podman save archive
docker save myapp:latest | smolvm machine run --image - -- ./app   # from stdin
smolvm machine run --image ./rootfs/ -- ./app             # from an unpacked rootfs dir
smolvm machine create --name myvm --image ./myapp.tar     # persistent, from a local archive
```

## When to Use What

| Goal | Command |
|------|---------|
| Run a one-off command in isolation | `smolvm machine run --net --image IMAGE -- CMD` |
| Interactive shell (ephemeral) | `smolvm machine run --net -it --image IMAGE -- /bin/sh` |
| Interactive shell (persistent) | `smolvm machine shell --name NAME` |
| Persistent dev environment | `machine create` → `machine start` → `machine exec` |
| Ship software as a binary | `smolvm pack create --image IMAGE -o OUTPUT` |
| Fast persistent machine from packed artifact | `machine create --name NAME --from FILE.smolmachine` |
| Use local container images (CI / air-gapped / fast iteration) | `--image ./archive.tar`, `--image -` (stdin), or `--image ./rootfs/` |
| Use git/ssh with private keys safely | Add `--ssh-agent` to run or create |
| Inject API keys / tokens without putting them on the command line | `--secret-env`/`--secret-file` flags or Smolfile `[secrets]` |
| Minimal VM without image | `smolvm machine run -s Smolfile` (bare VM) |
| Change mounts/ports/resources on existing VM | `machine update --name NAME -v ./src:/app -p 8080:8080` |
| Declarative VM config | Create a Smolfile, use `--smolfile`/`-s` flag |

### Persistence Model

- **`machine run`** — ephemeral. All changes are discarded when the command exits.
- **`machine exec`** — persistent. Filesystem changes (package installs, config edits) persist across exec sessions for the same machine, whether bare or image-based. Changes are stored in an overlay on the machine's storage disk.
- **`machine stop` + `start`** — changes persist across restarts. The persistent overlay is remounted preserving previous changes.
- **`pack run`** — ephemeral. Each run starts fresh from the packed image.
- **`pack start` + `exec`** — daemon mode. `/workspace` persists across exec sessions and stop/start. Container overlay resets per exec (package installs don't persist — use `/workspace` for durable data).
- **`machine create --from .smolmachine`** — creates a persistent named machine from a packed artifact. Boots from pre-extracted layers (~250ms, no image pull). Full `machine exec` persistence — package installs, file writes all survive across exec and stop/start.

## CLI Structure

All commands use named flags (no positional args except `machine create --name NAME` and `machine delete --name NAME`).

```
smolvm machine run --image IMAGE [-- COMMAND]     # ephemeral
smolvm machine exec --name NAME [-- COMMAND]      # run in existing VM
smolvm machine shell [--name NAME]                # interactive shell (auto-starts)
smolvm machine create --name NAME [OPTIONS]              # create persistent
smolvm machine create --name NAME --from FILE.smolmachine  # from packed artifact
smolvm machine start [--name NAME]                # start (default: "default")
smolvm machine stop [--name NAME]                 # stop
smolvm machine delete --name NAME [-f]                   # delete
smolvm machine status [--name NAME]               # check state
smolvm machine ls [--json]                        # list all
smolvm machine update --name NAME [OPTIONS]              # modify stopped machine settings
smolvm machine cp SRC DST                         # copy files (host↔VM)
smolvm machine exec --stream --name NAME -- CMD   # streaming output
smolvm machine monitor [--name NAME]              # foreground health + restart

smolvm pack create --image IMAGE -o PATH          # package
smolvm pack create --from-vm NAME -o PATH         # pack from VM snapshot
smolvm pack run [--sidecar PATH] [-- CMD]         # run .smolmachine

smolvm serve start [--listen ADDR:PORT|PATH]      # HTTP API
smolvm config registries edit                     # registry auth

# Secrets are references to host env vars / files, resolved at launch — no
# built-in store. Attach them on the command line or in a Smolfile [secrets].
smolvm machine run    --secret-env GUEST_VAR=HOST_VAR     # from host env var
smolvm machine run    --secret-file GUEST_VAR=/abs/path   # from host file
smolvm machine create --name NAME --secret-env GUEST_VAR=HOST_VAR  # persists the ref
smolvm machine exec --name NAME --secret-env GUEST_VAR=HOST_VAR -- cmd
```

## Artifact References

Artifact references follow OCI conventions and support both tags and digests:

```
python-dev                                        # bare name (default registry + latest)
python-dev:v1.0                                   # name + tag
binsquare/custom:v1                               # namespace + name + tag
smolmachines.com/python-dev:latest                # registry + name + tag
smolmachines.com/binsquare/custom:v1              # registry + namespace + name + tag
python-dev@sha256:abcdef0123...                   # digest reference (immutable)
```

Default registry: `registry.smolmachines.com`. Digest references require `sha256:` followed by exactly 64 hex characters.

### Local container images

`--image` also accepts a local source — useful for CI, air-gapped hosts, and fast
local iteration. smolvm stays a microVM runtime and delegates all image work
(flatten, whiteouts, config) to container tooling (`crane`/`docker`/`podman`); the
archive is flattened with `crane export`.

```
./image.tar  ./image.tar.gz  ./image.tgz   # a `docker save` / `podman save` archive (gzip ok)
-                                           # the same archive streamed on stdin
./rootfs/                                   # an already-unpacked root filesystem directory
```

A source is treated as local when it starts with `/`, `./`, `../`, is `-`, or ends in
`.tar`/`.tar.gz`/`.tgz`; everything else is a registry reference (so bare `alpine`
still pulls). Archives are cached content-addressed by hash and re-resolved on
`machine start`. `--image -` cannot be combined with `-i`/`-t` (both read stdin).

smolvm boots images, it does not build them: a Dockerfile passed to `--image` is
rejected with a hint to build first (`docker build … && docker save … | … --image -`).

## Key Flags

| Flag | Short | Used on | Description |
|------|-------|---------|-------------|
| `--image` | `-I` | run, create, pack create | OCI image, or a local source: a `docker save` archive (`./img.tar`, or `-` for stdin) or unpacked rootfs dir (`./rootfs/`) |
| `--name` | `-n` | run, start, stop, status, exec, update | Machine name (default: "default") |
| `--net` | | run, create | Enable outbound networking (off by default) |
| `--gpu` | | run, create | Enable GPU acceleration (Vulkan via virtio-gpu) |
| `--gpu-vram` | | run, create | GPU shared-memory region size in MiB (default: 4096). Ignored without `--gpu`. |
| `--volume` | `-v` | run, create, update | Mount host dir: `HOST:GUEST[:ro]` |
| `--port` | `-p` | run, create, update | Port mapping: `HOST:GUEST` |
| `--smolfile` | `-s` | run, create, pack create | Load config from Smolfile |
| `--interactive` | `-i` | run, exec | Keep stdin open |
| `--tty` | `-t` | run, exec | Allocate pseudo-TTY |
| `--allow-cidr` | | run, create | CIDR egress filter (implies --net) |
| `--allow-host` | | run, create | Hostname egress filter, resolved at VM start (implies --net) |
| `--ssh-agent` | | run, create | Forward host SSH agent (git/ssh without exposing keys) |

## Smolfile Reference

A Smolfile is a TOML file declaring a VM workload. Use with `--smolfile`/`-s`.

```toml
# Top-level: workload definition
image = "python:3.12-alpine"          # OCI image (omit for bare Alpine)
entrypoint = ["/app/run"]             # overrides image ENTRYPOINT
cmd = ["serve"]                       # overrides image CMD
env = ["PORT=8080", "DEBUG=1"]        # environment variables
workdir = "/app"                      # working directory

# Resources
cpus = 2                              # vCPUs (default: 4)
memory = 1024                         # MiB (default: 8192, elastic via balloon)
net = true                            # outbound networking (default: false)
gpu = true                            # GPU acceleration (default: false)
gpu_vram = 4096                       # GPU VRAM MiB (default: 4096, ignored unless gpu=true)
storage = 40                          # storage disk GiB (default: 20)
overlay = 4                           # overlay disk GiB (default: 2)

# Network policy — egress filtering by hostname and/or CIDR
[network]
allow_hosts = ["api.stripe.com"]      # resolved at VM start (implies net)
allow_cidrs = ["10.0.0.0/8"]         # IP/CIDR ranges (implies net)

# Dev profile (used by `machine run` and `machine create`)
[dev]
volumes = ["./src:/app"]              # host bind mounts
ports = ["8080:8080"]                 # port forwarding
init = ["pip install -r requirements.txt"]  # run on every VM start
env = ["APP_MODE=dev"]                # dev-only env (extends top-level)
workdir = "/app"                      # dev-only workdir

# Artifact profile (used by `pack create`)
[artifact]
cpus = 4                              # override resources for distribution
memory = 2048
entrypoint = ["/app/run"]             # override entrypoint for packed binary
oci_platform = "linux/amd64"          # target OCI platform

# Health check (used by `machine monitor`)
[health]
exec = ["curl", "-f", "http://127.0.0.1:8080/health"]
interval = "10s"
timeout = "2s"
retries = 3
startup_grace = "20s"

# Credential forwarding
[auth]
ssh_agent = true                      # forward host SSH agent into the VM

# Secrets — references to host sources, resolved at workload launch
[secrets]
DATABASE_URL   = { from_env   = "PROD_DB_URL" }      # host env var (at launch)
GCP_CREDS      = { from_file  = "/abs/creds.json" }  # host file (at launch)
```

### Merge Precedence

CLI flags override Smolfile values:

```
image:      --image flag > Smolfile image > None (bare Alpine)
entrypoint: Smolfile entrypoint > image metadata
cmd:        trailing args (after --) > Smolfile cmd > image metadata
env:        top-level env + [dev].env + CLI -e (all merged)
volumes:    [dev].volumes + CLI -v (all merged)
ports:      [dev].ports + CLI -p (all merged)
init:       [dev].init + CLI --init (all merged)
cpus/mem:   CLI flag > Smolfile > defaults (4 CPU, 8192 MiB)
```

## Networking

- **Off by default** — VMs have no outbound access unless `--net` is specified
- `--net` enables full outbound (TCP/UDP, DNS)
- `--allow-host api.stripe.com` enables egress only to resolved IPs of that hostname (implies `--net`). Also enables DNS filtering — only allowed hostnames can be resolved.
- `--allow-cidr 10.0.0.0/8` enables egress only to specified IP ranges (implies `--net`)
- `--allow-host` and `--allow-cidr` can be combined and used multiple times
- `--outbound-localhost-only` restricts to 127.0.0.0/8 and ::1 (implies `--net`)
- `-p HOST:GUEST` forwards a host port to the VM (TCP)
- Smolfile: use `[network] allow_hosts` and `[network] allow_cidrs`

### Proxy Support

Pass proxy settings into VMs with `-e` when behind a corporate proxy or VPN:

```bash
smolvm machine run --net \
  -e https_proxy=http://proxy.corp:3128 \
  -e http_proxy=http://proxy.corp:3128 \
  -e no_proxy=localhost,127.0.0.1 \
  --image alpine -- wget -q -O /dev/null https://example.com
```

Or declare them in a Smolfile:

```toml
net = true
env = [
  "https_proxy=http://proxy.corp:3128",
  "http_proxy=http://proxy.corp:3128",
  "no_proxy=localhost,127.0.0.1"
]
```

Proxy vars are NOT forwarded automatically — each VM gets exactly the env you specify. The VM uses the host's DNS server (from `/etc/resolv.conf`) for name resolution.

## SSH Agent Forwarding

Forward the host's SSH agent into the VM so git, ssh, and scp work with your keys — without the private keys ever entering the VM.

```bash
# CLI flag
smolvm machine run --ssh-agent --net --image alpine -- ssh-add -l
smolvm machine create --name myvm --ssh-agent --net

# Smolfile
# [auth]
# ssh_agent = true
```

Inside the VM, `SSH_AUTH_SOCK` is set automatically. Any tool that uses the SSH agent protocol (git, ssh, scp) works transparently:

```bash
smolvm machine exec --name myvm -- git clone git@github.com:org/private-repo.git
smolvm machine exec --name myvm -- ssh deploy@server "systemctl restart app"
```

The host SSH agent signs challenges but never sends private keys across the boundary. Even with root inside the VM, keys cannot be extracted — this is enforced by the SSH agent protocol and the hypervisor isolation.

Requires `SSH_AUTH_SOCK` to be set on the host. If missing, smolvm exits with an error and remediation instructions.

## GPU Acceleration

Enable the host GPU inside a VM with `--gpu`. Guest Vulkan talks to the host GPU via virtio-gpu/Venus; ANGLE uses it as the WebGL/OpenGL ES backend.

**Host setup:**
- macOS — bundled, no extra installs needed.
- Linux — install virglrenderer from the system package manager before use:
  - Alpine: `apk add virglrenderer mesa-vulkan-intel` (or `mesa-vulkan-ati` for AMD)
  - Debian/Ubuntu: `apt install virglrenderer0 mesa-vulkan-drivers`

```bash
# One-shot GPU workload
smolvm machine run --gpu --image alpine -- sh -c '
  apk add --no-cache mesa-vulkan-virtio vulkan-tools
  VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/virtio_icd.x86_64.json \
  vulkaninfo --summary 2>/dev/null | grep deviceName
'
# → deviceName = Virtio-GPU Venus (Intel(R) UHD Graphics ...)

# Persistent GPU machine
smolvm machine create --name browser --gpu --gpu-vram 2048
smolvm machine start --name browser
smolvm machine exec --name browser -- \
  chromium --headless=new --no-sandbox --use-gl=angle --use-angle=vulkan \
    --screenshot=/tmp/out.png --window-size=1280,800 https://example.com
```

The guest must set `VK_ICD_FILENAMES` so the Vulkan loader finds the virtio ICD. Put it in `env` in a Smolfile to avoid repeating it on every exec:

```toml
gpu = true
gpu_vram = 2048
env = ["VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/virtio_icd.x86_64.json"]
```

For a complete working example see [`examples/headless-browser/browser.smolfile`](examples/headless-browser/browser.smolfile).
## Secrets

smolvm stores no secret material. A secret is a *reference* to a value that
already lives on the host — a host environment variable or a host file — and is
resolved into the workload's process environment at launch time. Bring your own
secrets manager (Vault, 1Password, AWS, sops, your shell): render the value into
an env var or file, then point a ref at it. Only the reference is ever
persisted; the resolved value never lands in the VM record, the database, or a
`.smolmachine` pack.

Attach refs on the command line:

```bash
# From a host environment variable (GUEST_VAR=HOST_VAR)
smolvm machine run    --secret-env OPENAI_API_KEY=OPENAI_API_KEY -- ./app
smolvm machine create --name web --secret-env DATABASE_URL=PROD_DB_URL   # persists the ref
smolvm machine exec --name web --secret-env TOKEN=CI_TOKEN -- ./deploy

# From a host file (GUEST_VAR=/absolute/path)
smolvm machine run --secret-file GCP_CREDS=/abs/creds.json -- ./app

# Bridge any external manager through the env/file seam, e.g. 1Password:
op run --env-file=secrets.env -- smolvm machine run -- ./app
```

Or reference them from a Smolfile. The left-hand key becomes the env var name in
the guest workload:

```toml
[secrets]
DATABASE_URL = { from_env  = "PROD_DB_URL" }    # host env var (at launch)
GCP_CREDS    = { from_file = "/abs/creds.json" } # absolute host file (at launch)
```

Exactly one of `from_env`, `from_file` must be set per entry; `from_file` paths
must be absolute. Resolved values are appended *after* top-level `env` and CLI
`-e` flags. Resolution is late-bound, so rotating the underlying env var or file
takes effect at the next launch with nothing to re-sync.

**Threat model:** this is defense-in-depth, not zero-knowledge. The target
process sees plaintext in its own environment, and root inside the guest can
read any `/proc/*/environ`. Use SSH agent forwarding instead when a secret must
never leave the host.

**Where they're resolved:** `machine run`, `machine create` + `machine start`,
and `machine exec` resolve refs against *this host* under a trusted-local scope.
Untrusted surfaces — HTTP API request bodies and portable `.smolmachine` packs —
are treated as untrusted callers and may carry **no** resolvable secret ref:
`from_env` would expose the server's env and `from_file` would be an arbitrary
host-file read, so both are rejected. Configure secrets locally instead.

## File Copy

Copy files between the host and a running machine using `machine:path` syntax:

```bash
# Upload a file to the VM
smolvm machine cp ./script.py myvm:/workspace/script.py

# Download a file from the VM
smolvm machine cp myvm:/workspace/output.json ./output.json
```

**Image-based VMs (--image):** Files copied with `cp` are visible to
`exec` at the same path, and vice versa. This works for any path —
`/tmp`, `/home`, `/workspace`, etc. Under the hood, `cp` routes
through the container's overlay filesystem so both commands see the
same files.

**`/workspace` shared directory:** Every machine has a `/workspace`
directory — bare VMs, image-based VMs, and machines created from
`.smolmachine` artifacts. It persists across `exec` sessions and
across `stop`/`start` cycles. It's a good default location for
scripts, data, and results. Passing `-v /host/dir:/workspace` replaces
the default storage-disk workspace with your host directory for that
run — the host mount takes priority and the storage workspace is skipped:

```bash
# Typical agent workflow: copy code in, execute, extract results
smolvm machine create --name r-sandbox --image r-base:latest --net
smolvm machine start --name r-sandbox

smolvm machine cp analysis.R r-sandbox:/workspace/analysis.R
smolvm machine exec --name r-sandbox -- Rscript /workspace/analysis.R
smolvm machine cp r-sandbox:/workspace/results.csv ./results.csv

smolvm machine stop --name r-sandbox
```

**Behavior and limits:**

- Files up to 1 MiB transfer as a single message — no perceptible
  overhead beyond the agent round-trip.
- Larger files stream automatically: 1 MiB chunks for upload, 16 MiB
  chunks for download. The split is asymmetric because the
  host→guest direction has tighter socket-buffer headroom.
- Per-transfer cap is **4 GiB** in either direction. Files at or
  above this size are rejected up front (`total_size exceeds maximum`
  on upload; `exceeding the byte cap` on download). For larger
  blobs, mount a host directory with `--volume` instead of copying.
- A throttled progress line prints to stderr while large transfers
  run, including bytes-so-far, percentage (uploads), and rate.
  Pipe captures (`> file`) only see the upload/download summary,
  not the progress noise.
- Atomic on the guest side: a partially-written file never appears
  at the target path. If the transfer fails or the connection drops
  mid-stream, the staging file is cleaned up and the original
  destination (if any) is unaffected.

Typical throughput on macOS (Apple Silicon): ~35-42 MB/s upload,
~170 MB/s download.

## Streaming Exec

Stream command output in real-time instead of buffering:

```bash
# CLI — prints output as it arrives
smolvm machine exec --stream --name myvm -- python3 train.py

# API — Server-Sent Events
POST /api/v1/machines/:name/exec/stream
Content-Type: application/json
{"command": ["python3", "train.py"]}

# Response: text/event-stream
# event: stdout
# data: Epoch 1/10...
# event: exit
# data: {"exitCode":0}
```
## Bare VM Mode

`machine run` works without `--image` when a Smolfile provides the workload config, or for direct Alpine shell access:

```bash
# Bare Alpine shell
smolvm machine run -it

# Smolfile with entrypoint/cmd (no container overhead)
smolvm machine run -s Smolfile

# Bare VM with init setup, detached
smolvm machine run -d -s Smolfile
```

Bare VMs run commands directly in the Alpine rootfs — no OCI image pull needed. Use this when you need a minimal Linux environment.

## Packed Binaries (.smolmachine)

`smolvm pack create` produces two files:
- `my-app` — stub binary with embedded VM runtime (platform-specific)
- `my-app.smolmachine` — VM payload: rootfs, OCI layers, storage (cross-platform)

The packed binary runs as a normal executable:
```bash
./my-app run -- python3 -c "print('hello')"  # ephemeral, cleaned up after exit
./my-app start                               # persistent daemon mode
./my-app exec -- pip install x               # exec into daemon
./my-app stop                                # stop daemon
```

Alternatively, create a named machine from the `.smolmachine` for full lifecycle management:
```bash
smolvm machine create --name my-vm --from my-app.smolmachine
smolvm machine start --name my-vm            # ~250ms boot, no image pull
smolvm machine exec --name my-vm -- pip install x   # fully persistent
smolvm machine stop --name my-vm
smolvm machine ls                            # shows my-vm
```

The `.smolmachine` manifest includes registry-oriented metadata:
- `host_platform` — host OS+arch this machine runs on (e.g., `darwin/arm64`), distinct from `platform` which is the guest
- `created` — RFC 3339 timestamp of when the machine was packed
- `smolvm_version` — version of smolvm that built it

## HTTP API

Start with `smolvm serve start --listen 127.0.0.1:8080` or `smolvm serve start --listen $XDG_RUNTIME_DIR/smolvm.sock`. Key endpoints:

```
POST   /api/v1/machines                    Create machine
GET    /api/v1/machines                    List machines
GET    /api/v1/machines/:name              Get machine
POST   /api/v1/machines/:name/start        Start machine
POST   /api/v1/machines/:name/stop         Stop machine
DELETE /api/v1/machines/:name              Delete machine
POST   /api/v1/machines/:name/exec         Execute command
POST   /api/v1/machines/:name/exec/stream  Streaming exec (SSE)
PUT    /api/v1/machines/:name/files/*path  Upload file
GET    /api/v1/machines/:name/files/*path  Download file
GET    /api/v1/machines/:name/logs         Stream logs (SSE)
POST   /api/v1/machines/:name/images/pull  Pull OCI image
```

OpenAPI spec: `smolvm serve openapi`

## Important Defaults

- Machine name defaults to `"default"` when `--name` is omitted
- Network is **off** by default (security-first)
- CPUs: 4, Memory: 8192 MiB, Storage: 20 GiB, Overlay: 2 GiB
- Packed binaries use the same defaults (CPUs: 4, Memory: 8192 MiB)
- Memory and CPU are elastic via virtio balloon — the host only commits what the guest actually uses and reclaims the rest

## Important Behaviors

- **Observational commands don't stop running VMs.** `machine images`, `machine status`, `machine ls` and similar read-only commands leave a running VM in its current state. If the VM was already running before the command, it stays running after.
- **`machine prune` works on a running VM.** Regular prune only removes unreferenced layers and is safe while containers are active. `prune --all` requires the VM to be stopped first since it deletes manifests for layers that may be in use.
- **`machine exec` persists filesystem changes.** Package installs, config edits, and file writes inside `exec` survive across sessions. This works for both bare VMs and image-based VMs (created with `--image`).
- **`machine update` modifies a stopped machine.** Add/remove mounts, ports, env vars, or change CPU/memory without recreating the VM. Changes take effect on next `machine start`. Requires the machine to be stopped.
- **`machine run` is always ephemeral.** The VM is created, the command runs, and everything is cleaned up. No state carries over.
- **`-v host:/workspace` replaces the default workspace.** Every image-based VM exposes `/workspace` backed by the VM's storage disk. Mounting a host directory at `/workspace` takes priority — the host share is used instead and the storage-disk workspace is not mounted. Any other target path (e.g. `/data`, `/app`) does not affect `/workspace`.


Usage: smolvm-bin <COMMAND>

Commands:
  machine  Manage machines (create, start, stop, exec) [aliases: vm]
  serve    Start the HTTP API server for programmatic control
  pack     Package and run self-contained VM executables
  config   Manage smolvm configuration (registries, defaults)
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

Agents: run `smolvm --help` for full documentation including CLI reference and Smolfile schema

Per-command detail

Version

enumerated method → `smolvm --version`

Vanilla CLI
smolvm --version
exit 060 ms
smolvm 1.2.0
Pilot app store
pilotctl appstore call io.pilot.smolvm smolvm.version '{}'
exit 0833 ms
smolvm 1.2.0
adapter overhead: +773 ms (vanilla 60 ms · pilot 833 ms)

List machines

passthrough → `smolvm machine ls`

Vanilla CLI
smolvm machine ls
exit 042 ms
No machines found
Pilot app store
pilotctl appstore call io.pilot.smolvm smolvm.exec '{"args": ["machine", "ls"]}'
exit 0456 ms
No machines found
adapter overhead: +414 ms (vanilla 42 ms · pilot 456 ms)

Run command in an ephemeral Alpine microVM

boots a real isolated VM (separate kernel)

Vanilla CLI
smolvm machine run --net --image alpine -- sh -c echo hello from microVM; uname -a; cat /etc/alpine-release
exit 05854 ms
hello from microVM
Linux container 6.12.87 #1 SMP Fri May  8 14:25:15 CEST 2026 aarch64 Linux
3.24.1
── stderr ──
Starting ephemeral machine (vm-c39e250d)...
Pulling image alpine...
Pulling image alpine... [====================] 100%
Pulling image alpine... [====================] 100% — syncing...
Pulling image alpine... done.
Pilot app store
pilotctl appstore call io.pilot.smolvm smolvm.exec '{"args": ["machine", "run", "--net", "--image", "alpine", "--", "sh", "-c", "echo hello from microVM; uname -a; cat /etc/alpine-release"]}'
exit 05492 ms
hello from microVM
Linux container 6.12.87 #1 SMP Fri May  8 14:25:15 CEST 2026 aarch64 Linux
3.24.1
── stderr ──
Starting ephemeral machine (vm-3b055ee1)...
Pulling image alpine...
Pulling image alpine... [====================] 100%
Pulling image alpine... [====================] 100% — syncing...
Pulling image alpine... done.
adapter overhead: -362 ms (vanilla 5854 ms · pilot 5492 ms)

Compute in a Python microVM

pulls python:3.12-alpine, runs Python in the VM

Vanilla CLI
smolvm machine run --net --image python:3.12-alpine -- python3 -c print('2**100 =', 2**100)
exit 09374 ms
2**100 = 1267650600228229401496703205376
── stderr ──
Starting ephemeral machine (vm-25a8264d)...
Pulling image python:3.12-alpine...
Pulling image python:3.12-alpine... [=====>              ] 25%
Pulling image python:3.12-alpine... [==========>         ] 50%
Pulling image python:3.12-alpine... [===============>    ] 75%
Pulling image python:3.12-alpine... [====================] 100%
Pulling image python:3.12-alpine... [====================] 100% — syncing...
Pulling image python:3.12-alpine... done.
Pilot app store
pilotctl appstore call io.pilot.smolvm smolvm.exec '{"args": ["machine", "run", "--net", "--image", "python:3.12-alpine", "--", "python3", "-c", "print('2**100 =', 2**100)"]}'
exit 09501 ms
2**100 = 1267650600228229401496703205376
── stderr ──
Starting ephemeral machine (vm-215457f3)...
Pulling image python:3.12-alpine...
Pulling image python:3.12-alpine... [=====>              ] 25%
Pulling image python:3.12-alpine... [==========>         ] 50%
Pulling image python:3.12-alpine... [===============>    ] 75%
Pulling image python:3.12-alpine... [====================] 100%
Pulling image python:3.12-alpine... [====================] 100% — syncing...
Pulling image python:3.12-alpine... done.
adapter overhead: +127 ms (vanilla 9374 ms · pilot 9501 ms)