monty runs python3 in-process, sandboxed, stdlib-only:
perfect for quick one-liners and pipe transforms, useless for a script that
imports pandas. A docker entry is the opposite: your container, your
installed packages, but every line pays the exec round-trip. The policy
decides per line which one serves it:
policy.py
python3 /jobs/train.py --epochs 3 runs in the container, while
cat data.csv | python3 -c "import sys; print(len(sys.stdin.read()))"
stays on monty.
Input: the line’s context
The script sees one global,ctx, the parsed line before any routing.
This payload is captured from a live run of the config above, routing
python3 /jobs/train.py --epochs 3:
commands: one entry per pipeline stage (cat x | python3 -has two), each with its full words and its absolute-path operands.command,builtin: mirror the first stage.builtinmeans mirage has a builtin spec for the command (python3is a builtin that hands its code to a runtime), as opposed to an unknown name.env: the session environment (empty here, noexportyet).session_id: the executing session’s id.agent_idis empty until an agent identity is attached.mounts: the workspace’s mount prefixes, including built-ins like/dev/.
PolicyContext.from_dict), so you
can store one and replay a decision in tests.
Output: the verdict
- A runtime name (
"docker"): that entry serves every command it captures on this line. None: no opinion; the first capturer in theruntimeslist order serves each command (here,monty).{"deny": reason}: the line is refused before anything runs. It exits 126 with<command>: policy denied: <reason>on stderr.- Anything else is a routing error; the line fails rather than guessing.
{"runtime": "docker"} is the dict spelling of a name, and the dict is
where the verdict grows: new powers arrive as new keys, never as new
return types.
In code, the same two arms have a typed spelling, RouteResult and
DenyResult; the dict is their wire form, the only shape a script can
return from inside the evaluator sandbox:
policy, each runtime entry can carry its own script:
with the same ctx input but a boolean verdict: am I willing to serve this
line? Unwilling entries step aside and the first willing capturer wins.
What runs the scripts
policy.py is never imported: the workspace evaluates its source on the
policy engine, an entry in the runtimes list with the evaluator
capability. The script’s file extension picks the engine: policy: ./policy.py runs on the first python evaluator (monty, or pyodide in
TypeScript) and policy: ./policy.js on the first JS evaluator
(quickjs), even when an evaluator of the other language sits earlier in
the list; with no language match the first evaluator serves. A config
with policy scripts but no evaluator entry fails at the first decision.
Evaluation is bounded: a policy script that hangs fails the line with a
policy error after 10 seconds instead of freezing the workspace.
When you build the workspace in code rather than from a config file,
policy= also accepts a plain function with the same decision contract,
sync or async. It is called directly, so no evaluator is involved:
script= likewise accepts (ctx) -> bool in code.
Bring your own
The capability is open: inheritEvaluatorMixin (Python) or implement
Evaluator with the EVALUATOR brand (TypeScript) on your own runtime,
and it becomes eligible as the policy engine. eval(code, inputs=...)
returns the last expression’s value; how the value travels is your
runtime’s choice. The docker examples give a stock container the
capability by piping a harness to python3 -:
python,
typescript.