- Ship your own backend — a single Python file in your own project or package, built on what the
miragepackage exports at its root. No Mirage fork, no edits to Mirage source. - Contribute a builtin — the four-layer layout inside the Mirage repo, mirrored in TypeScript.
Ship Your Own Backend
Everything an out-of-tree backend needs is exported from themirage package root, the same front door that hands out Workspace and the twin of what @struktoai/mirage-core gives a TypeScript backend; there is no separate SDK module. Write the core functions over your data source, put them on a CommandIO, and GenericVFS wires the full generic command set (ls, cat, grep, find, head, wc, …) plus glob resolution:
Workspace({"/jira/": JiraVFS(cfg)}). A class rather than a
factory function, because that is what the registry and the config reference below both name;
examples/python/other/custom_vfs.py is the same shape end to end.
The escape hatches mirror what builtins use: optional CommandIO fields unlock more surface (write enables the byte-mutation family; find and du become native fast paths), overrides= suppresses a generic command you replace, and commands=[...] adds bespoke @command verbs.
VFS/FUSE ops are derived from the same table automatically (make_generic_ops under the hood): read/readdir/stat plus whatever mutations the table carries. Pass ops=[...] only for irregular handlers (they shadow same-named derived ops), or auto_ops=False to opt out.
To make the backend constructible by name (workspace YAML, snapshots, the daemon), register it:
CONFIG_CLS class attribute when the constructor takes a typed config.
Neither step is needed to mount from YAML. A vfs value carrying a colon names the class directly, the same way a clis entry’s cli value names a spec tree, so a deployment can point at a file next to the config or at a class inside an installed package:
examples/python/other/custom_vfs.py for a complete runnable backend in one file.
Snapshots and versions rebuild a saved mount through the same door: the registered name, or the reference the config named (recorded beside the class path, since a class loaded from a script file cannot be imported back). What comes back depends on what the VFS owns. Content the VFS holds itself (an in-memory store) is mirage-owned state: override get_state and load_state to carry it, and a snapshot or a version restores the mount with that content and no override. Content that lives in a remote service is only observed: keep the default state, which says needs_override, set supports_snapshot=True and fill FileStat.fingerprint, and a snapshot pins what it read while Workspace.load asks for the live VFS back through mounts=. A forgotten override is a refusal to load, never a mount that comes back empty. The example shows both halves: a wiki page is written, the workspace is snapshotted, the page is changed, and the loaded workspace serves the page as it was, while a feed mount that keeps the default state is refused until the load hands it back through mounts=.
Contribute a Builtin VFS
Builtins live inside the Mirage repo with the four-layer layout below. Keep the core I/O layer independent from command parsing, and check whether the same VFS or behavior should also be added to TypeScript. Use a recent VFS such as Dify or Databricks Volume as the structural reference. Paths are alwaysPathSpec values inside the VFS; do not pass filesystem paths as raw strings.
File Structure
python/tests/ without creating __init__.py files in the test tree.
1. Config, Accessor, and Registry
Define a typed Pydantic config and keep secrets inSecretStr fields.
Accessor that owns the client or transport. Add the VFS name to VFSName, export the config/VFS from vfs/<name>/__init__.py, and add a lazy VFSEntry to mirage/vfs/registry.py. The registry entry is required for YAML, snapshots, and the daemon to construct the VFS.
Keep every import at module scope. If that creates a cycle, change the dependency direction instead of adding a function-local import.
2. Core VFS Operations
Implement only the operations the backend supports. Read-only API-backed mounts usually start with:readdir(accessor, path, index)returning child names.read_bytes(accessor, path, index)returning bytes.stat(accessor, path, index)returningFileStat.
make_resolve_glob(readdir, cap) (mirage.utils.glob_walk), or use the CommandIO.resolve_glob property.
Use explicit types:
3. Ops Layer
Ops are derived, not hand-written.ops/<name>/__init__.py builds the whole VFS/FUSE op family from the same CommandIO table the commands use:
make_generic_ops emits read/readdir/stat plus whatever mutations the table carries — a CommandIO slot updates commands and ops together, and ops whose table field is None are omitted. Knobs mirror backend semantics, e.g. make_generic_ops("databricks_volume", IO, mkdir_parents=True).
Write a dedicated op module only for an irregular handler with no generic equivalent (a native grep push-down, a semantic search), and append it to the derived list:
write=True so MountMode.READ remains a real boundary (derived ops carry this from the table).
4. Commands
Build standard commands withCommandIO and make_generic_commands; the generic command owns flag interpretation. Backend wrappers should only connect glob resolution and I/O functions.
For a VFS-specific command:
- Use the shared command spec.
- Mark every mutation with
write=TruesoMountMode.READremains a real boundary. - Declare injected parameters such as
stdin,index, andprefixexplicitly. - Use
FlagView(flags, spec=...)when the command itself must read a flag; never useflags.get(...). - Add a provision estimator when a useful estimate is possible. Otherwise the planner reports
precision=unknown.
COMMANDS from commands/builtin/<name>/__init__.py.
5. VFS Class
Import commands and ops at module scope, then register them in the constructor:caches_reads=True only for stable, read-mostly content. Implement get_state() with credentials redacted and close any network clients in close().
6. Snapshot Support
LeaveSUPPORTS_SNAPSHOT=False unless the complete drift contract is implemented:
stat()returns a stableFileStat.fingerprint.- Every read record includes the fingerprint that produced those bytes.
- If the backend supports immutable revisions, reads consult
revision_for(path.virtual)and record the resolved revision.
7. Verification
Add tests for config validation, path layout, every VFS op, command behavior, read-only enforcement, state redaction, and cleanup. For major features, add or update integration coverage underinteg/ and check Python/TypeScript parity before opening the PR.