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.
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
-eor 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, sostd.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 thequickjs extra plus a WASI build of quickjs-ng
(the qjs-wasi.wasm asset from a
quickjs-ng release):
qjs-wasi.wasm with the home
option on the runtime entry (or the MIRAGE_QUICKJS_HOME environment
variable):
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.