> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding a New VFS

> Ship your own backend on the public `mirage` surface, or contribute a builtin VFS using Mirage's VFS, command, and snapshot conventions.

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:

```python theme={null}
from functools import partial

from pydantic import BaseModel, SecretStr

from mirage import (Accessor, CommandIO, FileStat, GenericVFS,
                    stream_from_bytes)


class JiraConfig(BaseModel):
    site: str
    token: SecretStr


class JiraAccessor(Accessor):
    def __init__(self, config: JiraConfig) -> None:
        self.client = make_client(config)


async def readdir(accessor, path, index=None) -> list[str]: ...
async def read_bytes(accessor, path, index=None) -> bytes: ...
async def stat(accessor, path, index=None) -> FileStat: ...


class JiraVFS(GenericVFS):
    CONFIG_CLS = JiraConfig

    def __init__(self, config: JiraConfig) -> None:
        super().__init__(
            name="jira",
            accessor=JiraAccessor(config),
            io=CommandIO(
                readdir=readdir,
                read_bytes=read_bytes,
                read_stream=partial(stream_from_bytes, read_bytes),
                stat=stat,
                is_mounted=lambda a: True,
                local=False,
            ),
            prompt="Issues rendered as .json files.",
        )
```

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:

```python theme={null}
from mirage import register_vfs

register_vfs("jira", JiraVFS, JiraConfig)
```

or ship it as a normal package with an entry point — discovered automatically at registry-build time:

```toml theme={null}
[project.entry-points."mirage.vfs"]
jira = "mypackage.backends:JiraVFS"
```

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:

```yaml theme={null}
mounts:
  /jira:
    vfs: ./jira.py:JiraVFS
  /wiki:
    vfs: mypackage.backends:WikiVFS
```

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

```text theme={null}
python/mirage/
  vfs/<name>/
    __init__.py
    config.py
    prompt.py
    <name>.py
  accessor/<name>.py
  core/<name>/
    read.py
    readdir.py
    stat.py
  ops/<name>/
    __init__.py          # OPS derived from the CommandIO table
  commands/builtin/<name>/
    __init__.py
    io.py                # the CommandIO table
    <vfs-specific commands>.py
```

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.

```python theme={null}
from pydantic import BaseModel, SecretStr


class MyConfig(BaseModel):
    token: SecretStr
```

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:

```python theme={null}
from mirage.accessor.base import Accessor
from mirage.cache.index import IndexCacheStore
from mirage.types import FileStat, PathSpec


async def stat(
    accessor: Accessor,
    path: PathSpec,
    index: IndexCacheStore | None,
) -> FileStat:
    ...
```

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:

```python theme={null}
from mirage.commands.builtin.my_vfs.io import IO
from mirage.ops.generic import make_generic_ops

OPS = make_generic_ops("my_vfs", IO)
```

`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:

```python theme={null}
from mirage.core.my_vfs.grep import grep_bytes
from mirage.ops.registry import op
from mirage.types import PathSpec


@op("grep", vfs="my_vfs")
async def grep(accessor, paths: list[PathSpec], pattern: str, *, index,
               **kwargs) -> bytes:
    return await grep_bytes(accessor, paths, pattern, index)


OPS = [*make_generic_ops("my_vfs", IO), grep]
```

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:

```python theme={null}
from mirage.accessor.my_vfs import MyAccessor
from mirage.commands.builtin.my_vfs import COMMANDS
from mirage.ops.my_vfs import OPS as MY_VFS_OPS
from mirage.vfs.base import BaseVFS
from mirage.vfs.my_vfs.config import MyConfig
from mirage.types import VFSName


class MyVFS(BaseVFS):
    name: str = VFSName.MY_VFS
    caches_reads: bool = True

    def __init__(self, config: MyConfig) -> None:
        super().__init__()
        self.config = config
        self.accessor = MyAccessor(config)
        for fn in COMMANDS:
            self.register(fn)
        for fn in MY_VFS_OPS:
            self.register_op(fn)
```

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.
