Skip to main content
The pyodide and monty runtimes run python3 in-process on your machine. A sandbox runtime does the opposite: it takes a whole command line and runs it inside a separate machine, a local container or microVM or a cloud sandbox. Reach for it when a line needs a real OS, heavy packages, or a GPU that the in-process interpreters cannot give it. Five sandbox runtimes ship from @struktoai/mirage-node, all with the same surface: docker and smolvm run on your own machine, Daytona and e2b in the cloud, and ssh reaches anything you can already ssh into. The isolation differs too: a container shares your kernel, a smolvm microVM boots its own, and an ssh machine is whatever machine you point it at.
Looking for Sandlock? Mirage’s Sandlock adapter is Python-only today. It confines the host python3 interpreter rather than arbitrary captured lines, and the matching TypeScript adapter has not landed yet.

Routing: what goes to the sandbox

A sandbox runtime does not claim every line. Each runtime captures a set of commands; a captured line runs in the sandbox, everything else stays on the local vfs. You list the runtimes in order, and vfs is the catch-all:
So grep, cat, and ls run locally as always, while python3 train.py runs on the cloud box. Mirage never creates, provisions, or deletes sandboxes: you bring your own (a running container or microVM, or a live Daytona or E2B sandbox), the first captured line connects to it, and every captured line execs inside it verbatim.

The workspace inside the sandbox

For the job to see your mounts as ordinary files, the sandbox must serve the workspace itself, and provisioning that is yours, like everything else about the sandbox. Run Mirage inside it, with the same mounts at the same prefixes as the host workspace, each FUSE-mounted at its own prefix:
A read then pulls from the backend and a write streams straight back to it, with no upload or sync step: inside the sandbox, /data backed by S3 is a real directory. Because you write the sandbox-side config, it can differ from the host’s where it should: the endpoint that is 127.0.0.1:9000 on your laptop is host.docker.internal:9000 from inside a container, credentials can be scoped down, and none of it ever travels over the provider’s exec API. The flip side is that keeping the prefixes in step with the host workspace is your job; a sandbox serving different mounts fails loud only when a path misses. This needs an image with fuse3 and Mirage plus your backends installed (see The sandbox image). The in-sandbox mount is served by the Python Mirage build, so the image is the same for both language SDKs.
The ssh provider can skip all of this when the files live on the ssh machine itself: mount them over the ssh resource at a prefix equal to their remote absolute path, and captured lines open them natively with no FUSE and no remote Mirage. See Skip the FUSE on the SSH page.

Paths inside the sandbox

Mirage rewrites nothing: the line, its cwd, and every path in it pass through verbatim. With the sandbox serving the same prefixes, cd /data then python3 train.py works, and so does an absolute path like python3 /data/train.py, because /data means the same thing on both sides.

The sandbox image

The sandbox needs fuse3 plus Mirage with the backends you mount. The repo ships a Dockerfile that builds this image from the current checkout, so it always matches your code:
By default it installs every mountable backend plus fuse. Narrow it to just the backends you use for a smaller image, or extend it as a base:
The one image is the shared base for every machine provider: run it directly under Docker or smolvm, use it as a Daytona image/snapshot source, or use it as an E2B template base. Continue with the provider page in the Sandbox sidebar for connection and configuration details.

Selecting in YAML

Sandbox runtimes are ordinary runtimes entries: a name, its captures, and a config block describing the machine (mirroring a mount’s config block), with vfs as the in-process catch-all. Only the selected runtime consumes its entry, so one file stays portable:

Resource limits

A captured line is a command like any other: the same command_limits that guard cat or grep guard python3, including in the sandbox. A run that exceeds timeoutSeconds answers exit 124; maxBytes and maxLines cap its output the same way. There is no sandbox-specific limit surface.
In a sandbox a limit bounds the wait, not the run. Exit 124 means mirage stopped waiting; the command keeps going inside the sandbox. Mirage holds only a client, and the process lives in the sandbox’s own namespace, so dropping that client never signals it — a docker exec command is still burning CPU after its client is killed. Only the provider’s own timeout reclaims it, and mirage does not set one today:
  • Docker and smolvm have no server-side timeout: a runaway runs until you stop it or the container or microVM exits.
  • E2B applies its SDK default of 60 seconds, which cuts both ways — a runaway stops there, and so does a legitimate build or query that needs longer.
  • Daytona defaults to no timeout, so a runaway keeps running, and billing, until you stop it.
Cancelling a line has the same shape: it releases mirage’s side and leaves the sandbox side running. Treat commandLimits here as a bound on waiting rather than on execution, and set a lifecycle limit (idle-stop, auto-delete) on the sandbox itself.

Serving as the policy engine

A sandbox runs whole lines; it does not evaluate expressions, so it cannot run policy scripts. To make one eligible, subclass it and implement eval with your own transport. The docker eval examples do exactly that by piping a small harness to the container’s python3 -.