Repository navigation
Releases: aryanmehrotra/sbx
Release list
v0.16.1
sbx v0.16.1 — sbx env uses only this backend's record of a sandbox
Released 2026-09-30.
Upgrade if one machine reaches more than one backend - docker and kubernetes, firecracker, or two
docker engines - with sandboxes of the same name. Nothing else changes: no spec field, command, flag
or wake-path behaviour differs from v0.16.0.
Before you upgrade
- Behaviour change: a record of how a sandbox was created (
~/.sbx/origins) that another backend
wrote is ignored, so the command falls back to--specor thesandbox.jsonin the working
directory. Do this: nothing. Records written before v0.16.0 name no backend and are still used.
Fixes
sbx env,sbx forkand other commands that look up how a sandbox was created no longer use the spec of a same-named sandbox on another backend.
Upgrade
brew upgrade sbx # first install: brew install aryanmehrotra/tap/sbx
curl -fsSL https://raw.githubusercontent.com/aryanmehrotra/sbx/main/scripts/install.sh | VERSION=v0.16.1 sh
go install github.com/aryanmehrotra/sbx@v0.16.1
sbx doctorBinaries for macOS, Linux, FreeBSD and Windows (amd64, arm64) are attached with SHA256SUMS.
Cluster activator: ghcr.io/aryanmehrotra/sbx-activator:v0.16.1.
Full changelog: v0.16.0...v0.16.1
What's Changed
- fix(origin): ignore an origin record another backend wrote by @aryanmehrotra in #20
- docs(release-notes): v0.16.1 by @aryanmehrotra in #21
Full Changelog: v0.16.0...v0.16.1
v0.16.0
sbx v0.16.0 — sbx ready and sbx egress you can trust on any engine
Released 2026-09-30.
Upgrade now if you run v0.15.1 or earlier on colima or Docker Desktop with an egress policy: the
filter let a sandbox reach the VM, other containers and your Mac's loopback. This release also makes
sbx create, ready and wake say "serving" only when something outside can connect, and fixes
data loss in sbx with and sbx snapshot. Advisory: https://github.com/aryanmehrotra/sbx/blob/v0.16.0/SECURITY.md
Before you upgrade
- Breaking:
egress_allow,egress_policyandegress: "allow"carry only ports 80 and 443. Do this: write"host:port"inegress_allowfor another port. - Breaking:
sbx with <name>refuses a name that exists; it used to reuse that sandbox, then delete it and its volumes. Do this:sbx envorsbx execagainst an existing sandbox. - Breaking:
sbx snapshot <sandbox> <name>refuses an existing name instead of merging into it. Do this:--replace. - Breaking:
cap_add: ["ALL"]is refused. Do this: list the capabilities the workload needs. - Breaking:
$${in anenvvalue is a literal${; v0.15 expanded$${X}. Do this: checkenvvalues withsbx validate. - Behaviour change:
sbx listhas an ISOLATION column before ADDRESS, andsbx addjoins the sandbox's isolation tier, refusing a different--isolationorSBX_ISOLATION. Do this: readsbx list --jsonin scripts. - Behaviour change:
sbx createstops with an error naming the holder when a lock stays held for 10 minutes; it went ahead unlocked after 90 s. Do this: nothing. - Behaviour change:
sbx ready,wakeandcreatecheck inside each container that something listens where outside can reach it, and fail at once on a verdict that cannot change. Do this: bind0.0.0.0, not127.0.0.1. - Behaviour change:
sbx validaterefuses unknowncap_addnames, non-plain${...}inenv, andcpu,memoryorgpusvalues no provider accepts. Do this:sbx validateeach spec. - Behaviour change:
sbx gclists and--forceremoves dead lock files and origin records; records written before v0.16.0 are counted, never removed. Do this: nothing. - New requirement: restart
sbx serveafter upgrading, and do not run two sbx versions at once. Lock files, pause marks and the egress filter changed format. Do this: stop the oldsbx serve, start the new one.
Highlights
- Allow one port for one host:
"egress_allow": ["db.example.com:5432"]. - Delete one snapshot:
sbx snapshot --rm golden. It refuses while a fork still runs from it. - A snapshot pauses running services while it copies, so a database such as ClickHouse is saved at one instant.
sbx listshows each service's isolation tier andfrozenservices;--jsonaddsisolationandstate.sbx envprints<SERVICE>_HOSTand_PORTfor services no export names, such as ones fromsbx add.sbx init --from-devcontainertranslates${localEnv:X}and lists what it could not evaluate.- Every command takes flags before, between or after names:
sbx logs -f main redis.
Fixes
- Security: the egress filter no longer reaches the VM, the default bridge or host loopback, a network created after it, or itself;
CONNECTno longer tunnels raw TCP to any port. egress_allowworks under--isolation gvisor, keeps every service's hosts, and follows an edited spec onsbx create.- A
sbx egresschange can no longer be reverted by the daemon writing the policy at the same moment. sbx create,readyandwakeno longer print ✓ or "serving" for a service that then fails, exited, or could not be asked; scratch and distroless images are checked too.sbx withremoves a sandbox whose create failed or was interrupted, and two of one name no longer remove it under each other.- Ctrl-C or SIGTERM during
sbx createsays what it kept, releases its locks and exits 130 or 143. - Concurrent creates no longer share a slot or wait on each other's health checks; a pid reused by another process no longer holds a lock.
- The daemon no longer serves the old ports after
sbx rmthensbx createof one name, or binds the ports of a sandbox a running--onlydaemon serves. - A failed or interrupted
sbx snapshotleaves nothing behind, and--rmfinds what an older one left.sbx forkwrites its own spec file, so forks of one snapshot or ofbuildservices work. - A service sleeps within about a second of its
idlewindow, and wakes the services itdepends_onaftersbx sleep. sbx resumerefuses a running service;sbx execpasses piped stdin and exits with the command's status.
Upgrade
brew upgrade sbx # first install: brew install aryanmehrotra/tap/sbx
curl -fsSL https://raw.githubusercontent.com/aryanmehrotra/sbx/main/scripts/install.sh | VERSION=v0.16.0 sh
go install github.com/aryanmehrotra/sbx@v0.16.0
sbx doctorBinaries for macOS, Linux, FreeBSD and Windows (amd64, arm64) are attached with SHA256SUMS.
Cluster activator: ghcr.io/aryanmehrotra/sbx-activator:v0.16.0.
Full changelog: v0.15.1...v0.16.0
What's Changed
- fix: everything the functional QA sweep found — egress host door, sbx with data loss, stale daemon, and 40 more by @aryanmehrotra in #18
- docs(release-notes): v0.16.0 by @aryanmehrotra in #19
Full Changelog: v0.15.1...v0.16.0
v0.15.1
sbx v0.15.1 — sbx create over an asleep sandbox no longer fails on files
Released 2026-09-29.
Upgrade if you re-run sbx create over sandboxes that may be asleep and declare files. Nothing
else changes: no spec field, command, flag or wake-path behaviour differs from v0.15.0.
Before you upgrade
- Behaviour change:
sbx createover a sandbox that already exists and is asleep leaves each
asleep service as it is, and does not re-run its mount check, health wait orinit. Do this:
to re-run them,sbx wake <sandbox>and thensbx createagain.
Fixes
sbx createre-run over an asleep sandbox no longer reports afilesentry as "did not mount as a file".
Upgrade
brew upgrade sbx # first install: brew install aryanmehrotra/tap/sbx
curl -fsSL https://raw.githubusercontent.com/aryanmehrotra/sbx/main/scripts/install.sh | VERSION=v0.15.1 sh
go install github.com/aryanmehrotra/sbx@v0.15.1
sbx doctorBinaries for macOS, Linux, FreeBSD and Windows (amd64, arm64) are attached with SHA256SUMS.
Cluster activator: ghcr.io/aryanmehrotra/sbx-activator:v0.15.1.
Full changelog: v0.15.0...v0.15.1
What's Changed
- fix(create): re-running create over an asleep sandbox no longer reports a mount failure by @aryanmehrotra in #16
- Release v0.15.1 by @aryanmehrotra in #17
Full Changelog: v0.15.0...v0.15.1
v0.15.0
sbx v0.15.0 — sbx install adds what sbx doctor says is missing
Released 2026-09-29.
sbx doctor reports what this machine lacks, including the gVisor, Kata and checkpoint runtimes
that --isolation and sbx checkpoint need. sbx install now installs them, and the tools doctor
lists, after showing every command and asking first. sbx doctor itself still changes nothing.
Before you upgrade
- Behaviour change:
sbx install checkpointrestarts dockerd, andsbx install gvisorreloads it.
Do this: read the plan withsbx install --dry-runfirst. The plan warns before a restart. - Behaviour change: each runtime is merged into
/etc/docker/daemon.json, and the previous file is
saved asdaemon.json.sbx-bak. Do this: nothing, unless you manage that file another way.
Highlights
- Install what is missing:
sbx install(everything it can), or by name:sbx install gvisor. - See the plan without running it:
sbx install gvisor --dry-run. - gVisor, a runtime that gives containers their own user-space kernel, comes from a release pinned
by sha512 (amd64, arm64) and is registered with dockerd asrunsc. Running containers are untouched. - Checkpoint installs CRIU, the Linux tool that saves a running process to disk, and turns on
dockerd's experimental mode. Kata comes from the distribution's package (dnf) and needs/dev/kvm. - Tools come from apt-get, dnf, pacman, apk or brew: docker, kubectl, cloudflared, redis-cli,
mkfs.ext4, iptables. With sudo, the plan runs through it. - New docs: a quickstart, a CLI reference, one how-to guide, self-hosting and an FAQ, all rewritten
against the code: https://github.com/aryanmehrotra/sbx/blob/v0.15.0/docs/QUICKSTART.md
Fixes
- An
egress_allowhost no longer answers 502 "operation was canceled" to busyboxwget(every alpine image) over HTTPS. - An OpenSandbox API sandbox stopped right after it started now shuts down gracefully, not with exit -1.
sbx serve --helplistsfirecrackeramong the--providervalues.
Known limitations
- On Docker Desktop, colima or Rancher Desktop, and so on every Mac, the VM that runs docker owns
its config. The runtimes are refused there with that reason. Tools still install through brew. - Checkpoint was installed but not proven: the test machine's kernel cannot run CRIU. On such a
kernel doctor still shows✓ docker checkpoint, andsbx installwarns aftercriu checkfails.
Ubuntu 24.04 droppedcriu; the plan namesppa:criu/ppa. - Not run for real: Kata, the dnf, pacman, apk and brew paths (unit-tested), and a restart through
systemd. Without systemd, install stops at the restart and says to restart dockerd yourself. - Kata from its upstream release is not offered, because that release is not pinned here yet.
Upgrade
brew upgrade sbx # first install: brew install aryanmehrotra/tap/sbx
curl -fsSL https://raw.githubusercontent.com/aryanmehrotra/sbx/main/scripts/install.sh | VERSION=v0.15.0 sh
go install github.com/aryanmehrotra/sbx@v0.15.0
sbx doctor # the last line names what `sbx install` can add hereBinaries for macOS, Linux, FreeBSD and Windows (amd64, arm64) are attached with SHA256SUMS.
Cluster activator: ghcr.io/aryanmehrotra/sbx-activator:v0.15.0.
Full changelog: v0.14.0...v0.15.0
What's Changed
- feat: sbx install — install what sbx doctor reports missing, including gVisor, Kata and checkpoint by @aryanmehrotra in #11
- fix(execd): register signal handlers before serving, so an early SIGTERM is a graceful stop by @aryanmehrotra in #13
- docs: rebuild the repo's presentation - README, guides, release notes, benchmarks, visuals, and a docs contract CI enforces by @aryanmehrotra in #10
- site: a landing page for GitHub Pages, deployed only once Pages is switched on by @aryanmehrotra in #14
- fix(egress): half-closed clients no longer get 502 · release v0.15.0 by @aryanmehrotra in #15
Full Changelog: v0.14.0...v0.15.0
v0.14.0
A microVM's escape reaches less, and a create copies nothing
v0.14.0 hardens the Firecracker provider. v0.13 put every VMM under the jailer. This release
closes two of the gaps v0.13 listed as open, removes the per-VM image copy, and stops a slow sleep
from losing a VM's memory.
| v0.13 | v0.14 | |
|---|---|---|
| a jailed VMM's network | the host's namespace | its own namespace: only its tap, no address, no route |
| one file a compromised VMM can write | unbounded | its largest drive + 64 MiB, then SIGXFSZ |
| a new VM's root filesystem | a copy of the image | the image, read-only and shared, plus its own sparse layer |
| a sleep whose seal is not confirmed | the VM stops and wakes cold | asked again (10 s, 20 s, 30 s); if still unconfirmed, the VM keeps running and retries |
Nothing changes on docker or Kubernetes.
What proves it
These results come from CI on the merged code, on a GitHub-hosted runner with /dev/kvm and the
jailer on:
| check | result |
|---|---|
fc-jail-watch across every microVM step |
153 VMMs seen, each chrooted, in a network namespace of its own, and running as a non-root uid no other VMM had |
upstream tests/go (v0.10.0 tier) on microVMs |
48 passed · 0 failed · 4 skipped |
| the same, with a warm pool on microVMs | 27 passed · 0 failed · 1 skipped |
docker: v0.10.0 tier · pool · e2e |
50 · 0 · 2, 20 / 20, and 3 / 3 |
| real jailer, microVM e2e, pool e2e, host guard | pass |
The e2e test now checks that the VMM's /proc/<pid>/ns/net is its VM's namespace and not the
host's, with no routes. The real-jailer test reads Max file size from /proc/<pid>/limits.
Burst TTI on microVMs (4 concurrent creates, then node -v, 3 rounds, node:22-slim) is
unchanged within runner noise: 144 ms from a frozen pool, 759 ms from an asleep pool, 2,637 ms
cold (v0.13: 141, 699 and 2,821). That image is small, so it cannot show the layered root's gain,
which is in large images. v0.12 measured about 16 s per create to copy the 7.4 GiB
code-interpreter image on a CI runner. A layered create hard-links it instead.
What changed
- A network namespace per VM. With the jailer on, each VMM runs in
/var/run/netns/sbxfc<slot>-<index>. That namespace holds only its tap (owned by the VM's uid),
bridged through a veth into the sandbox's guarded bridge, with no address and no route. A VMM
escape reaches what the guest reaches, not the host's network. Unjailed
(SBX_FC_JAILER=off), there is no namespace. - A file-size limit. A jailed VMM runs with
RLIMIT_FSIZE, set to the largest file it may
legitimately write plus 64 MiB. It bounds each file, not how many: the state filesystem still has
no per-uid quota, and SECURITY.md says what the operator can do about that. - A layered root. A create hard-links the image's cached
rootfs.ext4read-only as its base,
and gives the VM an empty, sparseupper.ext4ofSBX_FC_DISK_SIZE(10G by default). The guest
lays overlayfs over both. Snapshots link the base and clone only the layer. - A sleep that is not confirmed keeps the VM. Seal is asked up to three times with a longer
bound each time. If none is confirmed, execd is re-keyed, the VM stays running with its memory,
and the next idle check tries again. After three unconfirmed sleeps in a row it stops as before,
so a guest cannot keep its memory by stalling forever. - The daemon's root, written down. SECURITY.md lists the capabilities the daemon needs and
why it keeps them, for a host that bounds it withCapabilityBoundingSet=. No VMM holds any
capability (CapEff 0, checked in CI).
⚠️ Before you upgrade a microVM host
- The host needs
ip netns. That means iproute2, a writable/var/run/netns, and
CONFIG_NET_NSplusCONFIG_VETH, which every distribution kernel has. A host that cannot make
a namespace now refuses a jailed microVM, and the error quotesip. sbx running inside a
container that forbids namespaces must move to the host, or accept the risk with
SBX_FC_JAILER=off. See TROUBLESHOOTING.md. - A custom guest kernel needs
CONFIG_OVERLAY_FS=y. The pinned kernels have it. Otherwise set
SBX_FC_ROOTFS=copyto keep v0.13's full-copy layout. - Existing VMs and snapshots keep their layout. A VM or snapshot made before v0.14 keeps its
wholerootfs.ext4; only new creates are layered. Volumes on a layered VM start one drive later
(vdd). - A VM's writes are bounded by
SBX_FC_DISK_SIZE. The default is 10G, sparse. A workload that
writes more must raise it, or write to a volume.
What it still does not do
- The daemon runs as root. It redoes privileged work on every create and wake, so it cannot
drop root after starting up. - No quota on the whole state filesystem. Put
SBX_FC_STATEon a filesystem of its own, or
enable project quotas.sbx doctorwarns when it shares/. - No bare-metal benchmark, and the helper VM path (M3+ Mac, Windows 11) is not yet run end to
end on a Mac.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.14.0
sbx doctor # checks /dev/kvm, the jailer, iptables, the bridge guardsWhat's Changed
- microVM hardening: per-VM network namespace, shared read-only base, fsize limit, seal retry (K3, K4, K6) by @aryanmehrotra in #8
- v0.14.0: release notes (microVM netns, layered root, fsize, seal retry) by @aryanmehrotra in #9
Full Changelog: v0.13.2...v0.14.0
v0.13.2
Closer to upstream's OpenSandbox API: pool, e2e, and honest refusals
v0.13.1 was tagged and never published. The release gate (
go test -race ./...) caught a
flaky test,TestSleepAndWakeReachAScopedDaemon, which failed 6 times in 900 runs. The
test's own readiness probe could reach the service aftersbx sleepand wake it again, which
is correct behaviour for a connection. The test now waits for that probe to land before it
sleeps, and passed 900 of 900. This release is v0.13.1 plus that test fix; no product code
differs.
Upgrade if you drive sbx through the OpenSandbox SDKs. This release fixes two places where sbx
was stricter than upstream's own server, so upstream's pool and e2e test files now pass. It
also corrects error messages that promised features in a release that never shipped them.
Nothing changes for sbx.json, the wake path, docker, Kubernetes, or the microVM provider.
What proves it
These are upstream's tests/go at release-1.1.0, unmodified, run in CI on every change:
| check | v0.13.0 | v0.13.2 |
|---|---|---|
| v0.10.0 tier, docker | 50 passed · 0 failed · 2 skipped | same |
| v0.10.0 tier, microVMs (code_interpreter included) | 48 · 0 · 4 | same |
pool (v0.11.0 tier), with a Redis |
4 passed · 8 failed · 8 skipped (not gated) | 20 passed · gated |
e2e (v0.12.0 tier), keyless |
never ran | 3 passed · gated |
The rest of those tiers, isolated_session (48 tests) and credential_vault (4 tests), is
not built. ROADMAP.md §4 lists what is left.
What changed
- A renew may shorten the expiry. sbx refused any
renew-expirationthat did not move the
expiry later. Upstream's server checks only that the new time is in the future
(ensure_future_expiration). Its SDK pool relies on that: when it hands out a member, it
renews it to the caller's timeout, which is usually earlier. That broke 8 pool tests. A renew to
now or to the past is still refused with 400. - Running means execd answers. sbx used to hold a code-interpreter sandbox
Pendinguntil its
Jupyter answered. Upstream'se2ecreates that image withtail -f /dev/nullas the entrypoint,
so Jupyter never starts, and every e2e test timed out. Upstream reportsRunningonce the
container runs and leaves Jupyter to its SDK, whoseCreateCodeInterpreterwaits for it. sbx
now does the same, one step stricter: it still waits for execd. A client that does not use the
SDK still gets a bounded wait on its first/codecall, inside execd. The reversal and its
reason are in DECISIONS.md ("Running means usable"). - Refusals no longer promise a release. Nine parts of the API that sbx has not built answered
501 saying they arrive in sbx v0.11.0, and v0.11.0 shipped none of them. They now name the feature
and point to the roadmap. The nine are isolated sessions, server-side pools, registry
credentials,secureAccess,credentialProxy, lifecycle hooks, renew-on-access,
server-proxied endpoints and signed endpoints.
Fixed in the tooling
scripts/osb-conformance.sh --no-keyworks. Underpipefail, its probe for
--osb-insecure-no-keyalways read as "flag unknown". The daemon therefore started with a key
and answered 401, and upstream'se2efile had never run.
Docs
- BENCHMARKS.md adds create → first command on microVMs, from CI: 141 ms
from a frozen pool, 699 ms from an asleep pool, 2,821 ms cold. These ran on a nested-KVM
runner, not on bare metal. - COMPARISON.md adds agent sandboxes: sbx beside OpenSandbox, E2B, Daytona,
Modal, Cloudflare, Vercel and isorun, plus ComputeSDK's Burst TTI leaderboard. Every vendor
figure is linked to its source.
⚠️ Behaviour you may notice
- A code-interpreter sandbox now reports
Runningbefore its Jupyter answers. The upstream
SDKs wait for Jupyter themselves. If you call/codedirectly, the first call waits inside
execd for a Jupyter that is still starting. - A renew with an earlier, future
expiresAtnow succeeds and shortens the sandbox's life. It
used to be refused.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.13.2What's Changed
- v0.13.2: fix the flaky scoped-wake test that stopped v0.13.1's release by @aryanmehrotra in #7
Full Changelog: v0.13.1...v0.13.2
v0.13.0
A microVM warm pool, and the jailer on by default
v0.13.0 makes the OpenSandbox API on microVMs both faster and confined. v0.12.0 served the
API from Firecracker with no pool and an unconfined root VMM. Both have changed:
- A warm pool on microVMs.
--osb-poolworks on--provider firecracker. Members are parked
asleep (snapshot on disk) or frozen (paused in memory), and each one is re-keyed when claimed. - Every VMM runs under Firecracker's jailer by default. Each VM gets its own non-root uid, a
chroot inside its own directory, and a cgroup v2 sized from the spec. - The host guard fails closed. If a VM's bridge cannot be guarded, the VM is refused.
sbx serve --provider firecracker --osb-addr 127.0.0.1:8090 --osb-pool node:22-slim=4The warm pool's design is in the spec, and the reasoning is in
DECISIONS.md ("Every VMM runs under Firecracker's jailer").
What proves it
These results come from CI on the release commit, on a runner with /dev/kvm:
| check | result |
|---|---|
upstream tests/go conformance (v0.10.0 tier) on microVMs |
48 passed, 0 failed, 4 skipped |
| the same suite on microVMs with a warm pool | 27 passed, 0 failed, 1 skipped |
| the same suite on docker | 50 passed, 0 failed, 2 skipped |
the real jailer (TestTheRealJailer), microVM e2e and pool e2e |
pass |
fc-jail-watch across every microVM step |
63 VMMs seen, each chrooted and running as a non-root uid that no other running VMM had |
Burst TTI on microVMs: 3 rounds of 4 concurrent create → runCommand('node -v'), image
node:22-slim, on a GitHub-hosted x86_64 runner:
| mode | median | p95 |
|---|---|---|
| cold create | 2,821 ms | 5,769 ms |
warm pool, frozen (--osb-pool-freeze) |
141 ms | 207 ms |
| warm pool, asleep | 699 ms | 957 ms |
All 12 pooled creates were served from the pool. A frozen claim's create() takes a median of
about 37 ms; most of the rest is node -v itself. These are numbers from a shared CI runner,
not a bare-metal benchmark.
Every CI job is green on the release commit. zeropod is skipped, as it is on every run.
Confinement: what the jailer changes
- The VMM is not root. Each VMM runs under its own uid, taken from a range
(SBX_FC_JAILER_UID_BASEmoves it). The uid range must not hold a real account; sbx does not
check/etc/passwd. - A chroot per VM. The chroot holds that VM's kernel, drives, snapshot files,
/dev/kvm,
/dev/net/tun,/dev/urandomand its sockets. The shared kernel stays root's and read-only.
So does any drive the VMM only reads, meaning the agent drive and read-only volumes: a
compromised VMM cannot rewrite them for the next sandbox. - Sockets are checked by peer uid. A socket symlink planted in a jail is never followed. The
API and vsock sockets are accepted only from the jail's own uid (SO_PEERCRED); anything else
is refused as a foreign socket rather than read as "asleep". - A pool claim respects jail mode. A claim refuses a member that was parked in the other jail
mode. - The IPv6 guard is rechecked as well. The guard's IPv6 half is rechecked along with the
iptables half, on every wake and on every in-place resume. sbx doctornow grades the jailer, the host firewall mode, and whether the microVM state
directory shares/.
Opting out, deliberately
SBX_FC_JAILER=offbrings back v0.12's risk exactly. It is meant for a development host
that cannot run the jailer (no cgroup v2, no mknod), and it warns on every use. The OpenSandbox
API refuses to serve on firecracker without the jailer unless you pass
--osb-insecure-no-jailer.--fc-firewall=unmanagedtells sbx that the operator owns the host firewall, so a bridge
that cannot be guarded is not refused.
Also in this release
- The OpenSandbox API through the helper VM (M3+ Mac, Windows 11).
sbx serve --osb-addr
serves the API from inside the helper VM and fronts it on the host. It is unit-tested but
not yet run end to end on a Mac.--osb-poolis not carried into the helper VM yet. - A daemon started with
--onlyis now found. It writes~/.sbx/daemons/<pid>.json, so
create,list,ui,doctorand the egress note recognise the sandboxes it fronts.
Previously they all reported that no daemon was running. (Known issue in v0.10.0.) - A wake mounts only the host directories that create validated. The canonical bind paths
are pinned on the container. A start or restore re-checks them, and if a path changed while the
sandbox slept (a swapped symlink, for example), sbx refuses withHostBindChangedrather than
following it. (Known issue in v0.10.0: the host-volume TOCTOU.) Killnow signals the recorded VMM even when its argv no longer names the VM.- A running VMM is addressed in the jail mode it was launched with.
What it does not do yet
⚠️ No network namespace per VM. The VMM shares the host's network namespace. A VMM escape
reaches whatever a non-root uid on the host's network can reach.⚠️ No disk quota per VM. One sandbox, or a compromised VMM, can fill the state filesystem
(SBX_FC_STATE). Put that directory on a filesystem of its own or enable project quotas;
sbx doctorwarns when it shares/.- The daemon still runs as root. It needs root for taps, bridges, iptables and the jailer
itself. - Each VM still copies its image's data where reflink is unavailable.
- No warm pool through the helper VM, and no bare-metal benchmark yet.
The first three are the remaining prerequisites before anonymous or untrusted use. See
SECURITY.md.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.13.0
sbx doctor # checks /dev/kvm, cgroup v2, the jailer, iptables and the bridge guards
sbx serve --provider firecracker --osb-addr 127.0.0.1:8090 --osb-pool node:22-slim=4
bridge, now refuses microVM creates that v0.12 would have started. sbx doctor names the
fix. Setting SBX_FC_JAILER=off or --fc-firewall=unmanaged restores the old behaviour.
Nothing changes on docker or Kubernetes.
What's Changed
- v0.13: microVM warm pool + jailer on by default, guard fails closed by @aryanmehrotra in #2
Full Changelog: v0.12.0...v0.13.0
v0.12.0
The OpenSandbox API on microVMs
v0.12.0 serves the OpenSandbox API from Firecracker microVMs. v0.11.0 gave sandboxes their
own kernel and refused --osb-addr on that provider. That refusal is gone:
sbx serve --provider firecracker --osb-addr 127.0.0.1:8090Every sandbox the API creates is then a microVM with execd inside it. The SDKs, the upstream
conformance suite and the endpoints are the same as on docker. The plan is in
the spec.
What proves it
| check | result |
|---|---|
upstream tests/go conformance (v0.10.0 tier) on microVMs, CI with /dev/kvm |
48 passed, 0 failed, 4 skipped (skips are listed in expected.tsv) |
| same suite on docker | 50 passed, 0 failed, 2 skipped |
bridge guard against the real kernel (TestGuardLive) |
pass |
microVM end to end (TestFirecrackerE2E) |
pass |
Every CI job is green on the release commit (15 run; zeropod is skipped, as on every run).
What changed underneath
- execd runs the workload itself (
RunsAgent). The image's entrypoint starts as execd's
child inside the VM, so code-interpreter images start their Jupyter the way they do in a
container. - The API owns the token. A VM's execd secrets come from the API, and neither the image nor
the caller can choose them. - Volumes. A
pvcvolume is an ext4 block drive. Host-path volumes are refused by name,
because a microVM has no virtio-fs. - Snapshots are disk-only for API sandboxes.
- Readiness is honest. An image that runs Jupyter is
Runningonly once Jupyter answers.
Jupyter gets its own bound (3 min by default), and a timeout says which part was slow. - A Failed sandbox always says why, in the status, the daemon log and the history.
- Faster rootfs copies. Where reflink is unavailable, a VM's root filesystem is cloned by
its data extents, so holes are never read. In CI the code-interpreter image is 7.4 GiB of data
and clones in 16 s; the VM then boots and serves in 1.1 s.
Fixed on the way
- Over vsock, only the first request on a connection had a live context. execd's vsock
connection returned*fs.PathError, which net/http does not treat as a network error. After
the first response net/http cancelled the whole connection's context, so every later request
on that keep-alive connection arrived already cancelled. Any handler that consulted its
context failed — Jupyter readiness and the/coderoutes among them. The connection now
returns*net.OpError, like TCP does. /etc/hostsinside a VM now haslocalhostand the hostname, as docker's does.
Network: a microVM's only door is its filter
- Each bridge gets its own egress filter on its gateway address. Everything else on the host is
closed to the guests on that bridge. - By default a VM filter refuses RFC1918, CGNAT and ULA ranges and the host's own subnets. A
sandbox's policy cannot reopen what the host refuses. - The bridge guard drops guest traffic in
manglebefore docker's DNAT can forward it. The
guard is re-checked on every wake and daemon reconcile. sbx doctorgrades iptables and each bridge guard.
What it does not do yet
⚠️ The VMM runs as unconfined root. There is no jailer in this release. Do not use it
for untrusted or anonymous workloads. The jailer, with a guard that fails closed, is v0.13.- No warm pool on microVMs.
--osb-poolis refused on the firecracker provider; the microVM pool is v0.13. - A copy per VM without reflink. On a filesystem without reflink, every VM still copies its
image's data: about 16 s for a 7.4 GiB image in CI. On XFS withreflink=1or on btrfs the
clone is instant. - No bare-metal benchmark yet. The only numbers claimed are the ones above.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.12.0
sbx doctor # checks /dev/kvm, iptables and the bridge guards
sbx serve --provider firecracker --osb-addr 127.0.0.1:8090Nothing changes for docker or Kubernetes, or for API servers already running on docker.
What's Changed
- v0.12.0: the OpenSandbox API on Firecracker microVMs by @aryanmehrotra in #1
New Contributors
- @aryanmehrotra made their first contribution in #1
Full Changelog: v0.11.0...v0.12.0
v0.11.0
A sandbox with its own kernel
v0.11.0 adds a third backend: Firecracker microVMs. The same sandbox.json, with a
dedicated guest kernel underneath instead of the host's - and a sleep that keeps memory and
running processes, because sleeping a microVM is a snapshot and waking it is a restore.
sbx create web --provider firecracker --template nginxIt is the item ROADMAP.md called "the big one", and it lands smaller than planned
because a spike measured the risky parts first - see
the spike.
Where it runs
| host | what happens | proven by |
|---|---|---|
Linux with /dev/kvm |
Firecracker directly | e2e inside a Linux VM with KVM; a new CI job runs it wherever the runner exposes /dev/kvm (its first run is this release's) |
| Mac, M3 or later, macOS 15+ | sbx manages a small Linux helper VM (colima) and runs Firecracker inside it; sbx on the Mac works unchanged, and a connection on the Mac still wakes the sandbox |
e2e on an M4: 16/16 |
| Mac on lima instead of colima | same design | not yet run end to end - colima is the verified driver |
| Windows 11 with WSL2 | same design, in a WSL distro | built and unit-tested; not yet run on a Windows host |
Kubernetes with a kata-fc RuntimeClass |
--isolation firecracker |
unit-tested only |
| anything else | refused, with the one thing to change | - |
sbx doctor says which of these applies to your machine and why.
What it measures
On an Apple M4 through nested virtualisation (helper VM 2 vCPU / 2 GiB, nginx, 12 interleaved
rounds - scripts/fc-anywhere-e2e.sh):
| median | p95 | |
|---|---|---|
| wake from a snapshot to first byte | 224 ms | 282 ms |
| request to an awake sandbox | 8.8 ms | - |
sbx exec round trip |
788 ms | - |
sbx sleep |
709 ms | - |
sbx create |
11.5 s | - |
On a Mac this is parity for development, not speed: nested virtualisation pays for every
page the guest touches after a restore, and the docker warm pool from v0.10.0 remains the fast
path there. On bare-metal Linux the restore should be far cheaper; that has not been measured
yet, and the numbers above are the only ones this release claims. Where the wake time goes, and
where create's goes, is broken down phase by phase in BENCHMARKS.md.
How it holds together
- execd is the in-VM agent, over vsock - the same binary that serves the OpenSandbox API
inside containers.sbx exec,sbx cpandhealthgo through it. - A restored VM never answers with its parent's identity. execd is sealed before every
snapshot and re-keyed with a fresh token and a rotated secret before a wake is let through. - The API choices fail closed. A failed sleep stops the VM instead of leaving it half-sealed;
a slow VMM is an error, not "asleep"; wake and sleep take the VM's lock before reading its
record.
Two independent reviews - one of the design, one of correctness and security - went over this
provider before release. Every finding either became a fix with a test that failed before it,
or is listed below.
What it costs you, and what it does not do yet
- A sleeping microVM is not free at rest. Its memory snapshot is a file roughly the size of
the VM's RAM - 256 MiB each by default.sbx doctorshows the total. - Refused by name, not ignored:
build,files,mounts,init,gpus,cap_add,
egress_allow/egress_policy, and images whoseUSERis not root. The OpenSandbox API
(--osb-addr) is refused on this provider; it stays docker-backed. - Networking: a microVM has no route out (
egress: "deny"). sbx writes no firewall rules, so
isolation between microVMs depends on the host'sFORWARDpolicy beingDROP-sbx doctor
checks it andcreatewarns when it is not - and a guest can reach host services listening on
0.0.0.0. SECURITY.md has the detail. - Known issue: under heavy host memory pressure a sleep's seal can time out (once in 46 rounds
here). The VM is then stopped rather than snapshotted, so that sandbox wakes with a cold boot
and loses what was only in memory. - Not yet: forking a microVM snapshot, the OpenSandbox API and warm pool on microVMs, a
bare-metal benchmark.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.11.0
sbx doctor # which microVM path this machine has
sbx create box --provider firecracker --template nginxNothing changes for docker or Kubernetes sandboxes, or for the OpenSandbox API.
Full Changelog: v0.10.0...v0.11.0
v0.10.0
A sandbox in 14 ms, and the rest of OpenSandbox
v0.9 made sbx serve speak OpenSandbox. v0.10.0 makes it fast, and fills in what was missing:
a warm pool that answers create in about 14 ms, code interpreter contexts, terminals,
snapshots and templates, volumes, and a port proxy. OpenSandbox's own e2e suite now runs 52
tests against sbx, up from 38, and they pass.
It also carries every fix from v0.9.1: if you are on v0.9.0 and run the API without
a key, read that first.
A warm pool: create answers before a container could start
sbx serve --osb-addr 127.0.0.1:8080 --osb-pool python:3.11-slim=8sbx keeps that many sandboxes of the image already created, with execd answering. create
claims one, gives it a fresh execd token and the caller's env, and returns it Running. The pool
refills behind it. A request the pool cannot honour exactly - volumes, a snapshot, a template, a
network policy, any field it does not know - takes the ordinary path instead, so a member is never
handed out without something the caller asked for.
Measured with scripts/osb-bench.sh, create through the first successful command, the way
ComputeSDK's Burst-TTI benchmark times it (M4, colima 3 vCPU / 3 GiB):
| median | p95 | success | |
|---|---|---|---|
| warm pool, one at a time | 13.7 ms | 41 ms | 10/10 |
| cold, one at a time | 207 ms | 356 ms | 10/10 |
| warm pool, 100 at once | 472 ms | 567 ms | 100/100 |
Cold creates got faster too. A create used to cost about 4 s: 3 s asking the registry about an
image already on the machine, and the rest waiting for the SDK's next 2-second poll. Now the
image is pulled only when absent, and create answers once the sandbox is running, at about
170 ms. A burst of 100 cold creates is too noisy on this machine to state, varying 6x between
runs; the pool is the answer to bursts.
New through the API
| Code interpreter | Jupyter contexts per language, state kept between cells, results as mime bundles (images, HTML), interrupt. Upstream's code_interpreter suite: 7/7 |
| Terminals | PTY sessions over WebSocket, with resize, one writer and read-only viewers, and replay from any point after a reconnect |
| Port proxy | GetEndpoint(port) for any port in the sandbox, over HTTP and WebSocket, authenticated like the rest of execd |
| Snapshots and templates | snapshot a running sandbox, create new ones from it, and name images as templates. The snapshot is the container's filesystem, the same as OpenSandbox's docker runtime |
| Volumes | pvc as docker named volumes (namespaced, so a caller can only name its own), and host only under roots you allow with --osb-host-paths |
| Egress env | OPENSANDBOX_EGRESS_* variables go to the filter and never reach the workload |
Tested as people use it
Beyond upstream's suite, scripts/osb-usecases-e2e.sh runs fifteen end-to-end flows against a
real daemon: an agent over MCP, a coding loop through the SDK, the code interpreter, idle freeze
keeping a counter's memory, pause, expiry across a daemon crash, a live egress change, snapshot
and fork, volumes, a terminal, the port proxy, a classic sandbox.json sandbox beside API ones,
scope safety, ten concurrent sandboxes that leave nothing behind, and the pool.
That suite, and the Linux run of the unit tests, found four bugs, fixed here:
sbx rmleft the image's own data volumes behind - a redis's/data, a postgres's data
directory - with the sandbox's data in them. It now removes them with the sandbox.sbx rmsaid it removed an egress filter that never existed.sbx history <sandbox> --jsonignored--jsonwhen it came after the name.sbx mcp: cancelling acommand_runin the moment between execd starting the command and the
client reading its id left the command running with no interrupt. It is now interrupted.
An independent design review also found, and this release fixes: a failed pause or resume could
leave a sandbox unreachable with no call able to release it; a snapshot image carried the source
sandbox's live execd token; a host volume path could be swapped for a symlink between the check
and the mount; readonly_volumes and volume_mounts had leaked into the public sandbox.json
format. Each fix came with a test that failed before it.
What it costs you
- Pool members are running containers. Fifty python members held about 620 MiB here. Size the
pool for your burst, not for your fleet, or pass--osb-pool-freezeto freeze them while they
wait - which saves their CPU and costs each claim a thaw. - API sandboxes check health once a minute, quickly only while they start: 12x fewer
docker execcalls than v0.9 at the same sandbox count (116 per minute for ten sandboxes, now 10). - The OpenSandbox API is docker-only. Kubernetes answers
501with the reason; a cluster needs
an init-container way to place execd, which does not exist yet. - Known issue: a daemon started with
--onlyis not found bysbx sleep/sbx wake, which
then fail with "never became ready". Drive a scoped daemon through the API instead. - Known issue: a host volume's source is re-resolved by docker on every container start, so a
sandbox that sleeps withsbx.idle=sleepand wakes again follows a symlink swapped in since. The
default freeze-on-idle does not restart the container and is not affected. - Not yet: isolated sessions, the credential vault, server-proxy mode, and a microVM provider.
They answer501naming the release that adds them.
Adopting it
brew upgrade sbx # or: go install github.com/aryanmehrotra/sbx@v0.10.0
sbx serve --osb-addr 127.0.0.1:8080 --osb-pool python:3.11-slim=8
export OPEN_SANDBOX_API_KEY="$(cat ~/.sbx/osb/key)"Check it yourself: scripts/osb-conformance.sh --tier v0.10.0 and scripts/osb-usecases-e2e.sh,
each against a docker engine with no sandboxes on it.
Full Changelog: v0.9.0...v0.10.0