Skip to main content
Shell lines like node script.mjs or js -e "..." need a JavaScript engine. Mirage calls that engine a runtime, and node and js are two names for the same command family. One runtime ships today:

What it is for

node/js runs small, self-contained scripts and pipe transforms: compute, JSON munging, regex text processing, code generation. It is a bare modern engine (ES2023 syntax, ES modules, JSON, Promise, top-level await), not Node: there is no require, no npm, no process, no fs, no fetch. Anything that needs those belongs in a sandboxed deployment, not the workspace runtime.
Three input forms, mirroring python3:
  • -e <code> evaluates the argument.
  • A file (node script.js) is read through the workspace before the run, so a mounted script executes.
  • stdin with no -e or file runs the piped text as the program.
scriptArgs holds the arguments after the code or script, and std.in.readAsString() reads piped stdin (the quickjs-ng std/os globals are exposed). Output follows the real engine: console.log and the print global append a newline, std.out.puts/std.err.puts write raw, std.out.printf/std.err.printf C-format and return the characters written, and there is no console.error (write stderr with std.err.puts). These match the TypeScript quickjs runtime, so a script behaves the same in both languages.

Modules

A .mjs file runs as an ES module automatically (top-level import/export/await); .js and -e run as a classic script. Pass -m/--module to force module mode for inline code:

Isolation

The engine runs under WebAssembly capability isolation: it sees only what the run passes it. Host files and the network are invisible. Workspace mounts are visible with no setup: mirage intercepts the sandbox’s filesystem calls and bridges them through the workspace dispatch, so std.open('/data/f.txt', 'r') reads the mount live, and os.stat/os.mkdir/os.rename/os.remove stat and mutate it (0 or -errno in WASI numbering, like the real engine) — RAM, Redis, S3, or a virtual backend — and writes are immediately visible to every command, with the same cache, write modes, and per-session mount narrowing as cat. A read-only mount (or a session narrowed to read) fails the open, so std.open returns null instead of writing. This is capability isolation, not a resource sandbox: the sandboxed code cannot reach your files or the network, but its CPU and memory are bounded only by command_limits timeouts. For untrusted code or hard resource limits, run behind a sandboxed deployment. Each run gets its own epoch-interruption engine, so a command_limits timeout traps the run and reclaims the thread instead of leaking it.

Setup

The runtime needs the quickjs extra plus a WASI build of quickjs-ng (the qjs-wasi.wasm asset from a quickjs-ng release):
Point mirage at the directory holding qjs-wasi.wasm with the home option on the runtime entry (or the MIRAGE_QUICKJS_HOME environment variable):
The first run compiles qjs-wasi.wasm and caches the compilation as qjs-wasi.cwasm next to it; runs after that boot in milliseconds.

Resource limits

node/js is a command like any other: the same command_limits that guard cat or python3 guard it, enforced at the same central point. A run that exceeds timeout_seconds answers with exit 124, and max_bytes/max_lines cap its output. Firing the guard also cancels the run and reclaims the engine at the deadline.

Policy engine

The quickjs runtime carries the evaluator capability, so a JS-only world has a policy engine: policy: ./policy.js evaluates JavaScript per line with ctx bound as a global, and the completion value (the last expression) is the verdict.