zygo-cli 0.1.4

zygo — a daemonless, rootless warm sandbox runtime
zygo-cli-0.1.4 is not a library.

Zygo — a warm sandbox per request

CI crates.io PyPI npm Hex Licence: Apache-2.0

Zygo forks a warm, sandboxed interpreter for every request: 1.4 ms through its API, and every request starts from a process that has never served one. Rootless, OCI images, and no daemon to install: the one long-lived process is a supervisor under your own user, not a system service.

This page is the first page of the Zygo book, also on the web at mhmtskrc2.github.io/zygo.

zygo serve ./handler.py --name resize        # a warm zygote: ~150 ms, once
zygo exec resize '{"url": "..."}'            # a fresh, sandboxed process: 2.8 ms (1.4 by API)
zygo exec resize '{"url": "..."}'            # and again, from the same clean copy

Why

A warm worker that serves many requests is fast and dirty: request n sees whatever request n-1 left behind. A container per request is clean and slow. Zygo is the third thing. zygo serve starts an interpreter, lets it do its imports, and parks it inside a sandbox. zygo exec forks it. Each child has its own cgroup, deadline and secrets, and is thrown away afterwards.

The same import-heavy Python script, run fresh for each call on one host (a Linux 6.8 VM, 2 vCPU, aarch64; chapter 25 has the method, and make bench-embed repeats it):

usually what it pays for
docker run --rm 542 ms daemon, containerd, shim, runc, a container object to remove
zygo run 70.8 ms a fresh sandbox — namespaces, cgroup, mounts — in one process
zygo exec 2.8 ms a fork() of the warm interpreter, plus starting the CLI

Most of the 70.8 ms is Python importing those sixteen modules: the sandbox itself is about 3.6 ms, and python3 -c pass inside one is 12 ms. Through the HTTP API rather than the CLI, a warm request is 1.44 ms usually and 10.5 ms for 1 in 100, and one function sustains 1,108 requests a second. zygo bench all reproduces every number on your own machine.

The zygote idea is older than Zygo: Android starts apps that way, and serverless research forked handlers from a pre-imported process in SOCK (Oakes et al., USENIX ATC 2018) and restored them from a snapshot in Catalyzer (Du et al., ASPLOS 2020). Zygo's part is the packaging — one static binary, rootless, every limit on, an agent protocol any language can speak — not the idea.

Why not bubblewrap, nsjail, nono, sandbox-runtime or kern?

They are good at what they do, and none of them keeps a sandboxed process warm and forks it per request.

What it is What Zygo adds
bubblewrap, nsjail building blocks for one confined process images, mandatory limits, an egress allowlist — and the warm fork
nono, sandbox-runtime confinement for a command you were running anyway (Landlock and seccomp, or bubblewrap and Seatbelt) a sandbox with its own root filesystem and limits, for code you did not write
kern a daemonless, rootless container per call, in a few ms a warm interpreter: no interpreter start and no imports on the request path
E2B, Modal, Daytona a microVM or gVisor per session, in their cloud runs on your hardware, and costs a fork rather than a VM per call
Sandlock, Zeroboot a copy-on-write fork of a Landlock-confined process, or of a Firecracker snapshot a fork that lands in a sandbox with its own root, pid namespace, cgroup and network — and the tenants, secrets and API around it

Similar projects compares all of them but sandbox-runtime, with measurements against nsjail and kern.

The boundary

The default backend, ns, is the host kernel: namespaces, cgroup v2, a seccomp allowlist, Landlock, no capabilities and a read-only root. That is the right wall for code that is semi-trusted — your customers' scripts, an agent's tools. A kernel bug is a way through it, as it is for every container. The same spec also runs on gvisor (a kernel in user space) or vm (libkrun), one-shot only. Neither is a full wall for hostile code yet. Both lack a network. A rootless gvisor cannot enforce its limits, and vm has none inside the guest and a kernel you build yourself. For anonymous code, the honest answer today is a separate machine.

make escape-linux attempts 21 of the vectors in the threat model, with 0 escapes, and every syscall number is swept against the seccomp profiles. The same chapter lists the tenant-against-tenant vectors not attempted yet, and says where the boundary is weaker than it looks. No external audit has been done. Fork safety, question by question covers what a fork shares with its parent and what it does not.

Install

# Linux, x86_64 or aarch64: one static binary, checked against the release's checksums
url=https://github.com/mhmtskrc2/zygo/releases/latest/download
curl -fsSLO "$url/zygo-$(uname -m)-unknown-linux-musl.tar.gz"
curl -fsSL "$url/SHA256SUMS" | sha256sum -c --ignore-missing
tar xzf zygo-*-unknown-linux-musl.tar.gz && sudo install -m 0755 zygo-*/zygo /usr/local/bin/

brew install mhmtskrc2/zygo/zygo      # macOS: the shim, and a Linux VM it manages
cargo install zygo-cli                # from source

Linux needs kernel 5.3 or newer (6.1 recommended), unprivileged user namespaces and delegated cgroup v2. On a Mac every command runs in a Linux VM, about 22 ms away. There is also a signed container image. Getting started covers all of it, signatures included.

zygo doctor                           # can this host run sandboxes? prints the fix if not
zygo run --mem 128M --timeout 10s python:3.12-slim python3 -c 'print("hello")'   # pulls the image
echo 'def handler(event): return {"got": event}' > handler.py
zygo serve ./handler.py --name echo && zygo exec echo '{"n": 1}'   # serve never pulls
zygo stop --all                       # everything serve started; on a Mac, the VM too

What else is in the box

  • One file per project. sandbox.toml declares functions, their images, limits, network and secrets; zygo up deploys it blue/green and pins image digests in zygo.lock. Chapter 20
  • Every limit is on by default — memory, CPU, pids, wall clock, scratch, open files — and the deadline kills the request's whole process tree.
  • The network is off by default. egress is an allowlist of names; private ranges and the cloud metadata address stay closed. Chapter 14
  • Secrets are files, written from outside the sandbox for one request: never environment variables, never in the warm process's memory.
  • Any language. Python and Node agents ship; anything else is a fresh process per request in a held sandbox (about 1.4 ms), or an agent of your own against the protocol.
  • For programs: an HTTP API, dependency-free Python and Node clients (zygo-sdk), an Elixir client (zygo_sdk), and an MCP server. Chapter 17
  • For a multi-tenant product: tenants with their own tokens and budgets, a script or a whole workspace (a tar) sent with each request, streamed output, and cancellation — all over the API.

Building on it? Read 13 (warm functions), then 17 (the API, the SDKs, MCP), then 23 (the threat model), in that order.

Status

v0.1.x: one machine; Linux in production, macOS for development. The warm path is ns-only by decision (ADR 0002). ROADMAP.md says what comes next.

Not for: an interactive session or a REPL (a request is one call, not a shell you keep); more than one machine (no scheduler, no cluster); GPUs; Windows without WSL2.

More

Contributing · Security policy · Changelog · Roadmap · Licence: Apache-2.0

Zygo is not affiliated with Zygo Corporation, the metrology company.


Next: The Zygo book → · in the repository