> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Route Policy

> Script which runtime serves each command line; route-policy scripts run on the workspace's evaluator runtime.

A workspace can hold several runtimes for the same command, with different
trade-offs. `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 **route policy**
decides per line which one serves it:

```yaml theme={null}
mode: exec
runtimes:
  - monty # captures python3 by default: sandboxed, stdlib-only
  - name: docker
    captures: ["python3"] # your container, has pandas installed
    config:
      container: my-box
  - vfs
route_policy: ./policy.py
```

```python policy.py theme={null}
# Jobs under /jobs need the container's packages; anything else
# stays on the fast in-process sandbox. The LAST EXPRESSION is
# the verdict.
stage = ctx["commands"][0]
in_jobs = any(p.startswith("/jobs/") for p in stage["paths"])
"docker" if in_jobs else "monty"
```

So `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`:

```json theme={null}
{
  "line": "python3 /jobs/train.py --epochs 3",
  "commands": [
    {
      "command": "python3",
      "words": ["python3", "/jobs/train.py", "--epochs", "3"],
      "builtin": true,
      "paths": ["/jobs/train.py"]
    }
  ],
  "command": "python3",
  "builtin": true,
  "cwd": "/",
  "env": {},
  "session_id": "019fb2fc-19a3-77cf-bbbe-c76069eed523",
  "agent_id": "",
  "mounts": ["/.bash_history/", "/data/", "/dev/", "/"]
}
```

* `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. `builtin` means mirage has
  a builtin spec for the command (`python3` is a builtin that hands its
  code to a runtime), as opposed to an unknown name.
* `env`: the session environment (empty here, no `export` yet).
* `session_id`: the executing session's id. `agent_id` is empty until an
  agent identity is attached.
* `mounts`: the workspace's mount prefixes, including built-ins like
  `/dev/`.

The payload round-trips through JSON (`RouteContext.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 the `runtimes` list 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:

```python theme={null}
from mirage.runtime.routing import (DenyResult, RouteContext, RouteOutcome,
                                   RouteResult)

def policy(ctx: RouteContext) -> RouteOutcome | None:
    if any(p.startswith("/prod/") for p in ctx.commands[0].paths):
        return DenyResult("writes under /prod are blocked")
    return RouteResult("docker") if "/jobs/" in ctx.line else None
```

Besides the global `route_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.

## Route, then admit

Routing is the first of two judgments a typed line clears, and the only
one that sees the line whole. The order is fixed: the syntax gate (an
unparsable line exits 2 and no policy hears about it), then the route
decision, then the [policy engine](/home/policy-engine) admits each
command — whichever lane routing picked, since a whole line handed to a
runtime clears the same per-command gate before the runtime sees a byte
of it, and every VFS op the runtime performs still clears `pre_ops`.

A route deny therefore short-circuits: the line dies whole and the
admission hooks are never consulted, so one line produces at most one
refusal. Both layers refuse in the same voice — `<command>: policy denied: <reason>` at exit 126 — on purpose: an agent reads one
grammar, and the host tells the layers apart by what they judge (the
route policy speaks once per line; the policy engine per command, per
op, per env write, and only it can [ask](/home/permissions#asks) instead
of denying). Nested lines (`$()`, `eval`, `source`, `xargs`) inherit the
outer line's route decision and never re-route, but every nested
command clears admission itself.

## What runs the scripts

`policy.py` is never imported: the workspace evaluates its source on an
**evaluator runtime**, an entry in the `runtimes` list with the evaluator
capability. The script's file extension picks the engine: `route_policy:
./policy.py` runs on the first python evaluator (monty, or pyodide in
TypeScript) and `route_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,
`route_policy=` also accepts a plain function with the same decision contract,
sync or async. It is called directly, so no evaluator is involved:

```python theme={null}
from mirage.runtime.routing import RouteContext

def policy(ctx: RouteContext) -> str | None:
    stage = ctx.commands[0]
    in_jobs = any(p.startswith("/jobs/") for p in stage.paths)
    return "docker" if in_jobs else "monty"

ws = Workspace(mounts, runtimes=runtimes, route_policy=policy)
```

A per-entry `script=` likewise accepts `(ctx) -> bool` in code.

## Bring your own

The capability is open: inherit `EvaluatorMixin` (Python) or implement
`Evaluator` with the `EVALUATOR` brand (TypeScript) on your own runtime,
and it becomes eligible to evaluate the scripts. `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](https://github.com/strukto-ai/mirage/blob/main/examples/python/runtimes/docker/docker_eval.py),
[typescript](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/runtimes/docker/docker_eval.ts).
