Skip to main content
Shell lines like python3 script.py need an interpreter. Mirage calls that interpreter a runtime, and you pick it per workspace. Four runtimes ship today: The first two bridge file I/O through the workspace, so code sees your mounts and nothing else. The last two run real CPython on the host and see the host filesystem instead — sandlock bounded to the paths you name, local unbounded.

Monty (default)

Monty is a minimal, secure Python interpreter built for running AI-generated code: microsecond startup, no host filesystem, environment, or network access. Mirage wires Monty’s OS interface to the workspace dispatch, so code running under python3 sees the workspace mounts, and nothing else:
Reads, writes, pathlib traversal, and os.environ all route through the mounts. A python3 write is immediately visible to cat, and works the same whether the mount is RAM, Redis, S3, or a virtual backend like MongoDB’s database.json. Monty requires the monty extra:

Concurrency

Runs execute on Monty’s own worker pool and are fully async: the event loop stays free, and cancelling a run halts the interpreter. One ceiling to know about when a server runs many I/O-heavy scripts at once: each run waiting on workspace I/O holds one pool worker (the Python binding only takes synchronous OS callbacks, pydantic/monty#560), and the pool defaults to the machine’s core count. Set TOKIO_WORKER_THREADS to lift it; waiting workers cost kilobytes of stack and no CPU, so 100 parallel I/O-bound scripts on one core is a TOKIO_WORKER_THREADS=100 away. Without it, python3 exits with code 127 and an install hint; every other command keeps working.

Differences from CPython

Monty implements a Python subset. Notably:
  • Command-line arguments are the argv global (argv[0] is the script name); sys.argv does not exist.
  • No sys.stdin: piping into python3 needs the wasi or local runtime.
  • No third-party imports (numpy, requests, …).
  • File handles are not iterable (for line in f raises TypeError); f.read(), f.readline(), and f.readlines() all work.
  • os.environ reflects the session env only, never the host’s.

WASI

The wasi runtime runs a real CPython, compiled to WebAssembly, inside your process on wasmtime. It fills monty’s gaps (classes, the complete stdlib, sys.argv, sys.stdin) while staying sandboxed: host files and network sockets are invisible, and the run sees the interpreter’s own build directory plus the workspace mounts. Mirage intercepts the sandbox’s filesystem calls and bridges them through the workspace dispatch, so open('/data/f.txt') inside the code reads and writes the mount live — RAM, Redis, S3, or a virtual backend — with the same cache, write modes, and per-session mount narrowing as cat. Guest writes are immediately visible to every command, a read-only mount (or a session narrowed to read) answers PermissionError at open(), and none of this needs FUSE, a config key, or any setup. The interpreter’s own build directory is served read-only from the host; a mount at / coexists with it (mount prefixes win for their subtrees, the build serves the rest). It needs the wasi extra plus a CPython WASI build (download and unzip a release from cpython-wasi-build):
Point mirage at the build directory with the config home on the runtime entry (or the MIRAGE_WASI_HOME environment variable):
The first run compiles python.wasm (a few hundred milliseconds) and caches the compilation as python.cwasm next to it; runs after that boot in milliseconds. The build directory is read-only inside the sandbox: guest writes land in the workspace or answer PermissionError, never in the bundle.

Local

The local runtime runs a host interpreter as a subprocess: full CPython, host filesystem, host environment. Use it when the code needs stdin, third-party packages, or host files:
It defaults to the interpreter running mirage; point the config home on the runtime entry (or the MIRAGE_LOCAL_HOME environment variable) at another binary, e.g. a project venv whose packages the code needs:
Note that local code does not see workspace mounts; it sees the real host filesystem. For setup and its host-path security model, see the dedicated Sandlock page under Sandbox.

Selecting in YAML

Workspace config files take a top-level runtimes list: the ordered world of runtimes, each entry a name or a mapping with the uniform runtime options (captures, config, script). The first entry that captures a command binds it; vfs is the in-process catch-all:
Each runtime can have its own option block under its name. The home option locates the runtime’s interpreter or distribution, in the spirit of JAVA_HOME: a build directory for wasi, an interpreter path for local, a distribution URL for pyodide (TypeScript); it also falls back to a MIRAGE_<RUNTIME>_HOME environment variable. Only the selected runtime consumes its block and it rejects option keys it does not know, so one config stays portable across runtimes and languages. monty embeds its interpreter and has no options yet.

Resource limits

python3 is a command like any other: the same command_limits blocks that guard cat or grep guard it, enforced at the same central point. A run that exceeds timeout_seconds answers with exit 124 and python3: timed out after Ns on stderr, exactly like any other command; max_bytes and max_lines cap its output the same way. There is no python3-specific limit surface. Firing the guard also cancels the run, and every runtime honors cancellation: monty kills its worker process, wasi traps the run through wasmtime’s epoch interruption, and local kills the subprocess. A timed-out python3 run is reclaimed at the deadline, not left burning in the background.