Skip to main content
A runtime decides where a program runs. Write one when the agent’s programs should run somewhere Mirage does not ship: your team’s job runner, a GPU machine or a build farm. A runtime is a class that extends Runtime, names the programs it captures and implements one entry point. You ship it as a file next to your workspace YAML and name it by file and class:
The part before the colon is a file path, relative to the YAML file, or an importable module (mypkg.runtimes:RemoteRuntime). The part after it is the class. Mirage loads the file, checks that the class is a Runtime subclass and builds it with the entry’s captures and config, like a runtime it ships. Loading runs the file’s top-level code in the Mirage process, so name only code you trust.

Runtime or CLI?

A CLI is a command Mirage implements: its spec, help and exit codes live in Mirage, and its handler calls a service through an accessor. A runtime is where Mirage hands off programs it does not implement: code the agent writes, or any binary. It answers “where does this run”, not “what does this command do”. To give the agent one command with a known interface, such as deploy or tickets, write a CLI. To choose where python3, node or any other program runs, write a runtime.

Choose an entry point

A runtime captures the program names you give it. @external captures every program no builtin, CLI or other runtime claims, and @line takes every line. Interpreter names (python3, python, node, js) are never left over, so capture the ones you want by name. Only a line executor can take @line, and only a line or process executor can take @external.

Reach files through ctx.vfs

The machine your runtime sends a program to cannot see a mount. Read and write workspace files through ctx.vfs, the session’s files. Each call goes through the workspace dispatcher, so mount modes, profiles, policy and the cache apply to it as they apply to cp. A runtime that opens host paths directly skips all of them. Read with raw when you copy a file out to write it back: it returns the stored bytes, not a form a filetype registration renders.

Example: an execution service

This runtime sends each program to an HTTP execution service with a copy of the session’s working directory, then writes back the files the program created or changed. The service runs the program in a scratch directory and answers with its output, exit code and changed files. The full example includes a small stand-in service, so it runs on one machine.
remote_runtime.py
The Python runtime opens one aiohttp session as its engine: open_engine runs once, at the first program, and close_engine when the workspace closes. The TypeScript one needs no engine, because fetch is global. In TypeScript the third constructor argument lists the config keys the runtime accepts, and they arrive in camelCase (max_bytes in the YAML is maxBytes); in Python the config class’s fields do. Either way, an unknown key in the YAML fails when the workspace loads. With /work a writable RAM mount and /archive a read-only disk folder, each holding report.py and orders.csv:
python3 runs on the service while cat, grep and the pipe stay in the workspace. The output file comes back into the mount, stdin reaches the program, and the read-only mount refuses the write-back. The refusal’s wording is the host’s: TypeScript prints python3: mount /archive/ is read-only. The complete examples, with the YAML, the stand-in service and a script that runs these lines, are in examples/python/runtime/custom and examples/typescript/runtime/custom. The stand-in service accepts work only with the token it was started with, and your real service should authenticate callers too: it runs whatever program it is sent.

Stop when the run stops

timeout, a closed session or a reader that stops early cancels the run. In Python the cancellation arrives as asyncio.CancelledError in your coroutine; in TypeScript, ctx.signal aborts. Pass the cancellation on to whatever runs the program, and make sure that stops it:
  • The example’s request is cancelled with the run (ctx.signal in TypeScript), and the service kills the program when the connection drops. It starts each program in its own process group and kills the group, so children the program started stop too.
  • A runtime that spawns a process itself kills it and waits for it before it removes the process’s files.
  • A process that exits without reading its input closes the pipe. In Node, give the child’s stdin an error listener that treats EPIPE as that, not as a failure; without one, the stream error stops the host.

What the copy holds

The example sends the working directory’s own mount, never a mount below it, so a run from / does not try to copy /dev or every other mount. It sends stdin whole too, because the service takes a run’s input up front; it stops reading at max_bytes, so an endless producer fails the run with stdin is over ... bytes instead of hanging it. A service that streams input would feed stdin as it arrives instead. The example copies files out and writes them back whole. Two runs that change the same file at the same time each write back their own copy, and the later write replaces the earlier one. That is fine for one agent’s sequential commands. A runtime shared by concurrent sessions should check that a file is unchanged before writing it back, or lock it for the run.

Reach

A runtime declares reach: workspace when ctx.vfs is its only way to files, process or remote when its code can act around that gate. The example declares remote: the program runs on another machine and can reach whatever that machine can. The default is process, so a runtime that does not declare its reach claims no promise. Embedders read it to decide whether “agent code cannot bypass mount modes and policy” holds for the workspace.

Next

Routing orders your runtime among the others, and Route Runtime picks one per command line.