Skip to main content
A VFS maps an external system to Mirage’s filesystem operations and shell commands. There are two paths:
  • Ship your own backend — a single Python file in your own project or package, built on what the mirage package 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 the mirage 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:
Mount it like any builtin: 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:
or ship it as a normal package with an entry point — discovered automatically at registry-build time:
The entry point resolves to the VFS class; declare a 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:
A relative path resolves against the config file’s directory, not the server’s working directory. A registry name always wins over a reference, so a name can never be reread as code. See 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 always PathSpec values inside the VFS; do not pass filesystem paths as raw strings.

File Structure

Add matching tests under python/tests/ without creating __init__.py files in the test tree.

1. Config, Accessor, and Registry

Define a typed Pydantic config and keep secrets in SecretStr fields.
Create an 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) returning FileStat.
Glob resolution is not a per-backend file: bind it from readdir with make_resolve_glob(readdir, cap) (mirage.utils.glob_walk), or use the CommandIO.resolve_glob property. Use explicit types:
Add write, append, create, unlink, rename, or directory operations only when the backend has matching semantics. I/O should remain async-native.

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:
Mark a hand-written mutation op with write=True so MountMode.READ remains a real boundary (derived ops carry this from the table).

4. Commands

Build standard commands with CommandIO 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=True so MountMode.READ remains a real boundary.
  • Declare injected parameters such as stdin, index, and prefix explicitly.
  • Use FlagView(flags, spec=...) when the command itself must read a flag; never use flags.get(...).
  • Add a provision estimator when a useful estimate is possible. Otherwise the planner reports precision=unknown.
Export the final list as COMMANDS from commands/builtin/<name>/__init__.py.

5. VFS Class

Import commands and ops at module scope, then register them in the constructor:
Set 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

Leave SUPPORTS_SNAPSHOT=False unless the complete drift contract is implemented:
  1. stat() returns a stable FileStat.fingerprint.
  2. Every read record includes the fingerprint that produced those bytes.
  3. If the backend supports immutable revisions, reads consult revision_for(path.virtual) and record the resolved revision.
Setting the flag without recording fingerprints does not provide drift detection.

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 under integ/ and check Python/TypeScript parity before opening the PR.