1. AI Agent and Application
The agent (or any application embedding Mirage) issues bash commands, VFS calls, or syscalls. One vocabulary, every backend.2. Mirage Bash and VFS
The action surface. Mirage Bash parses commands with tree-sitter and runs them against the Mirage VFS, a unified filesystem API over every mount. A FUSE Adapter exposes the same tree to host tools when you want it. A Command Registry and VFS Registry describe what verbs and mounts are available.3. Dispatcher & Cache
The Mirage Dispatcher routes each operation to the mount that owns the path, joining pipelines that span systems. The Index & File Cache absorbs repeats: the first directory walk hits the API, the next serves from cache; the first read streams bytes, later reads are local. Mount mode and session grants govern every mutation at the operation dispatcher, including symlinks, permissions and extended attributes stored in the namespace. Write admission precedes backend initialization and capability lookup. A read-only mount therefore returns a read-only refusal even when its backend has no handler for that operation. Backend capabilities describe what an admitted operation can do; they do not grant permission. Shell actions use these same operations:>> dispatches append, and find -delete
dispatches unlink or rmdir. Any append emulation runs below admission, so a refused
append never fetches file contents. The dispatcher also owns cache invalidation and
namespace cleanup after mutations.
File sizes describe rendered bytes. An unknown size remains unknown: find -empty
requires a confirmed zero-length regular file or a directory with no entries. It does
not fetch a rendered file merely to determine its size. This differs from GNU’s local
filesystem view, where a regular file’s size is available from stat.
4. Infrastructure and Remote
Whatever you mount: RAM, Disk, Redis, S3 / R2 / GCS / OCI / Supabase, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Email, MongoDB / Postgres / LanceDB / Qdrant, SSH, and more. Each speaks the same filesystem semantics from the agent’s point of view. Browse the VFS Matrix for the full list.Where to go next
- VFS Matrix to pick a backend to mount.
- Python Quickstart for working code in minutes.
- TypeScript Quickstart for the same Workspace API in Node, browser, or edge.
Managed execution
The workspace also owns a process supervisor. Shell lines, background jobs, pipeline stages, subshells andWorkspace.spawn share this inventory. A process is an asynchronous Mirage
execution; its PID is workspace-local and is never a native worker PID. Native
runtimes still execute in the environment described by their reach guarantee.
spawn accepts literal argv, opens bounded asynchronous byte pipes, and returns
an immediate handle. Arguments enter the same command lookup, admission and
runtime routing as shell commands. For example, ['printf', '%s', '$(whoami)']
prints the literal operand. To interpret a script, explicitly spawn
['sh', '-c', script]. Command and CLI handlers receive the same scoped spawn
door on opts.processes and inv.doors.processes; runtime contexts carry it too.
Use communicate(input) to send input and drain both output pipes concurrently.
Calling wait without consuming a piped output can apply backpressure, just as
with a native pipe. Termination requests cancellation; the process remains
stopping until its runner settles. Parent cancellation
reaches its live managed children. Workspace shutdown awaits tracked runners before closing their mounts
and job consoles. Python delivers the cancellation at the runner’s next await, so only a runner that
swallows CancelledError keeps shutdown pending. A JavaScript promise cannot be cancelled, so
TypeScript gives a runner the grace a cancelled line gets (250 ms) and then leaves it stopping;
its later console writes are dropped. Metadata includes
parent and execution-group IDs. A provider’s native descendants are not inferred from these IDs.
The shell keeps its job table separately: %1 is local to a session, while $!
and jobs -p return the managed PID. wait PID resolves a child job and numeric
kill PID can reach an authorized managed runner even after disown removed
its job entry. ps, ps aux, ps -e and ps -ef list visible live runners,
including foreground work. Their deliberately compact format is PID<TAB>COMMAND:
Mirage has no CPU, RSS, TTY or native process-tree accounting to populate GNU’s
columns. Subshells keep private job tables while sharing the workspace process supervisor.
$$ is the session’s virtual shell identity, inherited by shell forks; an idle
shell is not a live runner in ps. Signals, stopped jobs and native process
groups are not emulated. Existing shell cancellation retains exit status 137;
the Debian bash reference reports 143 for its default SIGTERM.
A session always sees and stops its own processes. Its profile says how far past
the session it reaches, and how many processes it may hold:
list and kill accept session (the default) or workspace; a bare scope sets
both, and kill cannot reach past list. list governs ps and a handler’s
process view; kill governs numeric kill and the view’s terminate,
and a process the session sees but may not stop answers
kill: (PID) - Operation not permitted, as bash does.
max counts the session’s live processes, its running line included, as
ulimit -u counts the shell: background jobs, pipeline stages, subshells and
spawned children. Stopping processes count until their runners exit. Top-level
lines also require a slot; a full session can be stopped through host handles or
an authorized session with spare capacity. Past the cap a fork
fails as bash’s does at its process limit: bash: fork: Resource temporarily unavailable, the rest of the line abandoned with status 254, and a subshell that
hits it reports 254 to its parent. Bash first retries with backoff; Mirage refuses
at once. A host or guest spawn past the cap raises EAGAIN, BlockingIOError in
Python and in a Pyodide guest’s subprocess.
A spawned child’s output is its handle’s result, so the command limits
bound it as they bound a typed line’s, wherever the parent’s own output goes. Inline permissions only narrow these grants.
Two sessions using the same profile do not thereby share their processes.
Replacing a profile revokes captured doors and requests cancellation of that
session’s executions; closing the session also cancels its managed work.
Output remains owned by the returned child handle or shell job console: listing
a process does not grant its output stream or replay. Only live
processes appear in the supervisor; retain a handle to inspect its final result.
For a workspace backed by stored sessions, hydrate with ensure_sessions_loaded
(Python) or ensureSessionsLoaded (TypeScript) before spawning. A session replaced
by hydration fails a pending spawn instead of executing with stale permissions.
Nested Monty and Pyodide calls use independent execution capacity and have a
maximum nesting depth of 16. A parent never waits for a child queued behind its
own interpreter. Pyodide blocks only its worker for the synchronous Python API;
the host answers asynchronously. Guest mutations flush before spawning and guest
file caches invalidate afterward. A host without shared-memory worker support
gets an explicit unsupported error. Pyodide supplies
Popen, so standard run,
call and check_output remain ordinary Python code. Live pipes, polling, waits,
communication, timeout retry, shell=True, merged stderr, and text conversion
use the supervisor. An explicit Python env replaces the inherited environment.
Program lookup uses the profile-visible executable view; shell functions and
shell-only builtins do not participate. Host spawn retains environment overlay
by default and exposes replace_env / replaceEnv for adapters.
Unfinished Pyodide children are cancelled and joined when their guest invocation
ends. Stdin inheritance transfers buffered unread input, not a shared kernel
descriptor. Native descriptor passing, PTYs, signal handlers, and Python’s
asyncio subprocess transport are not implemented. TERM and KILL both request
managed cancellation; their negative return codes identify the request, not a
native OS signal delivery. Provider adapters may buffer their output;
the byte-pipe API does not turn a buffered provider into an incremental one.