Skip to main content
Setup and behavior of every runtime are on the Runtimes pages. This page covers what is specific to the TypeScript SDK.

Differences from the Python SDK

  • Monty: each run executes in a crash-isolated worker. os.chdir() raises RuntimeError; use cd in the shell or the run’s ctx.cwd. os.stat() has only the named fields (st.st_size).
  • QuickJS: a std and os shim with the calls the QuickJS table lists. A write shows to other commands once the file is closed. A timeout stops the run, and the engine caps a run at 64 MiB of memory and 1 MiB of stack.
  • Local: in @struktoai/mirage-node only. With neither home nor MIRAGE_LOCAL_HOME, python3 is the one on PATH.
  • Sandboxes: in @struktoai/mirage-node; e2b also runs in browsers. mountType ssh and both need Node and the ssh2 package. The ssh runtime authenticates through SSH_AUTH_SOCK when there is no identityFile, and username defaults to the local user.
  • Config objects are camelCase in code (sandboxId, mountType); YAML spells the same keys in snake_case.
  • Pyodide is in the TypeScript SDK only; WASI is in the Python SDK only.
Runtime output uses the normal Workspace.shell() result: stdout and stderr are Uint8Array; use stdoutText and stderrText when you want decoded strings.

Pyodide

Scripts and flags

  • Each command and one-shot evaluation starts a fresh interpreter, so imports do not carry over; files on the mounts do. A console session keeps its own interpreter.
  • A script runs as in CPython: __file__ is absolute, tracebacks quote its lines, and sys.path[0] is its directory. -P leaves that entry off, and for -c and stdin omits the working directory entry. sys_path entries stay either way.
  • -P, -O/-OO and -B show in a read-only sys.flags. -O and -OO compile imported modules from source. Flags and import paths reset when the command ends, errors included.
  • An installed Python CLI gets bare argv (its name at index 0) and stdin (bytes, or None), as on Monty; Pyodide also keeps sys.argv and sys.stdin.
  • Standard streams support text and .buffer I/O, but do not expose native OS file descriptors.

Files

  • Native packages such as PIL and NumPy reach the same mounts as open().
  • A file is fetched on first access and cached for the run; edits made elsewhere during the run are not seen.
  • A create, delete, rename, mkdir, chmod or truncate reaches the mount when the call is made, and a write when the file is closed, so a refusal fails that call with the mount’s errno. Writes still open at the end land then; a failure there exits nonzero with a message. Concurrent writes are not conflict-checked.
  • A path no mount serves answers ENOTSUP to os.getxattr.
  • A console keeps its own os.chdir(), even through an exception; if its directory disappears, the next input fails, resets the working directory to / and keeps the variables.
  • Mount files under a prefix such as /data: a mount at / cannot replace Pyodide’s own filesystem, which holds its standard library.

Worker and browser setup

Pyodide runs in a worker, which Node needs no setup for. In a browser, the page needs one of two things:
  • Cross-origin isolation: the page is served with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp.
  • Mirage’s service worker, on any other page: copy node_modules/@struktoai/mirage-browser/dist/pyodide-sw.js to the folder your site serves at its root, and register it before the first python3:
The service worker answers only Pyodide’s file calls and lets every other request through. The check after register covers a first visit and a hard reload, which both leave the page uncontrolled. Service workers need HTTPS or localhost. A site that already has a service worker adds the bridge to it instead: call importScripts('/pyodide-sw.js') at the top of that worker (import '/pyodide-sw.js' in a module worker) and register it in place of /pyodide-sw.js. With neither, python3 exits 127 and says why. JSPI is not needed. In a browser, Pyodide loads from its CDN unless home names another place, such as a folder on your site holding the pyodide package’s files (home: '/pyodide/'). Mirage starts its workers from blob: URLs, so a Content-Security-Policy needs blob: in worker-src, and https://cdn.jsdelivr.net in script-src and connect-src while Pyodide loads from the CDN.

Concurrency and timeouts

  • max_concurrency (default 1) bounds active top-level commands, one-shot evaluations and inputs to distinct consoles; extra calls wait. Calls in one shell session stay ordered; give independent work its own session with ws.createSession(id) and { sessionId: id }. Nested subprocesses bypass the limit. In a browser the limit stays 1, so commands from every session take turns; a second Workspace runs alongside the first.
  • A timeout raises KeyboardInterrupt in the program, even in a busy loop. That needs SharedArrayBuffer. Without it, a workspace command’s worker is stopped instead, and a standalone run on the main thread blocks the event loop until it ends.

Sockets

connect and sendto raise ENETUNREACH, and listen, a UDP bind and socket.socketpair() raise ENETDOWN, so urllib.request.urlopen fails fast. Compare against errno.ENETUNREACH, not a number: the values are Emscripten’s. socket.socket(), a TCP bind and name lookups still answer. An init_module that calls useNodeSockFS() installs real sockets.

Containment

Guest Python can run JavaScript in its worker through Pyodide. On Node with module.registerHooks, Mirage hardens the worker after interpreter setup and before guest execution:
  • Further module loads are refused, including import('node:child_process') and imports of already loaded Node modules.
  • Host globals (process, global, fetch, WebSocket, and an injected require) are removed.
  • Exposed host filesystem and socket APIs are disabled, including mountNodeFS, mountNativeFS, useNodeSockFS, NODEFS, and host file preloading. Binary reads are restricted to configured local distribution and package directories, with traversal and symlink escape checks.
Automatic package loading still works from a local distribution. After the seal, network package loading and CDN fallback are unavailable. Packages listed in packages and trusted bootstrap_code run during initialization, before the seal. An init_module opts into host access (reach: process), so its worker is left unsealed. Node releases without module.registerHooks, including Node 20 and Node 22 before 22.15, also run unsealed; use them only for trusted code. Standalone SDK execution without a workspace can run on the main thread and is trusted-only. Workspace-backed execution requires a worker. Browser workers are left unsealed: JavaScript can still use fetch and module loading, subject to the page’s Content Security Policy. Each command gets its own worker. These measures close known host access paths. They do not make arbitrary JavaScript safe: Pyodide exposes mutable interpreter internals, and a worker thread shares the host process. For hostile code, use a sandbox with a process and kernel boundary.

Child processes

subprocess runs Mirage commands:
  • Supported: run, call, check_call, check_output, Popen with live pipes, poll, wait, communicate, termination, timeout retries, text mode, stderr=STDOUT, and shell=True through the workspace’s sh.
  • sys.executable is /usr/bin/python3, and shutil.which sees the profile’s programs. Functions and shell builtins are not programs.
  • The environment is inherited unless env replaces it. Writes land before each process call, and file caches reset after it.
  • A timed-out wait or communicate leaves the child running; run kills it. Children still running when the program ends are cancelled.
  • Not supported: native descriptors, PTYs, process groups, user switching, preexec_fn, asyncio.create_subprocess_exec and signal handlers. An unsupported option fails with an error.

Configuration

The package keys are on the Pyodide page. The others: init_module runs per command or evaluation and once per console, inside the worker. It has host privileges, so keep it in deployment config: POST /v1/workspaces rejects it, and setting it gives the runtime host access. Only an in-process shutdown calls its cleanup.

E2B in browsers

Import the runtime from core, which also registers e2b for YAML, and pass apiKey:
Keep account keys out of shared frontend bundles; have your backend authenticate the app. mountType ssh and both need Node, so a browser uses fuse or none.

Managing runtimes in code

new Workspace(mounts, { runtimes: [...] }) takes names and runtime objects, and adds workspace when the list leaves it out. On a live workspace, addRuntime appends an entry (the first capturer still wins), removeRuntime takes one out, and runtimes() lists them:
python3 moves to the new runtime at once. A running line finishes before its removed runtime closes, a removed instance cannot be added again, and workspace cannot be removed. registerRuntime(name, cls) makes your own runtime class available by name in YAML. The @external capture is also exported as EXTERNAL_COMMANDS. A sandbox subclass that overrides loadSdk() imports the SDK type from the provider’s sdk module:

Child processes

A host application starts a program through the same admitted entry point as the shell.