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:
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 asdeploy 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.- Python
- TypeScript
remote_runtime.py
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.signalin 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
stdinanerrorlistener that treatsEPIPEas 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 declaresreach: 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.