> ## 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 Resource

> Ship your own backend with the SDK, or contribute a builtin resource using Mirage's VFS, command, and snapshot conventions.

A resource 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 `mirage.sdk`. 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 (SDK)

Everything an out-of-tree backend needs is exported from `mirage.sdk` — that module is the stable public contract. Write the core functions over your data source, put them on a `CommandIO`, and `GenericResource` wires the full generic command set (`ls`, `cat`, `grep`, `find`, `head`, `wc`, ...) plus glob resolution:

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

from mirage.sdk import (Accessor, CommandIO, FileStat, GenericResource,
                        stream_from_bytes)


class JiraAccessor(Accessor):
    def __init__(self, config):
        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: ...


def make_jira_resource(config) -> GenericResource:
    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,
    )
    return GenericResource(name="jira", accessor=JiraAccessor(config), io=io,
                           prompt="Issues rendered as .json files.")
```

Mount it like any builtin: `Workspace({"/jira/": make_jira_resource(cfg)})`.

The escape hatches mirror what builtins use: optional `CommandIO` fields unlock more surface (`write` enables the byte-mutation family; `find`/`du_total`/`du_all` become native fast paths; `is_dir_name` hints virtual directories), `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.sdk import register_resource

register_resource("jira", JiraResource, 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.resources"]
jira = "mypackage.backends:JiraResource"
```

The entry point resolves to the resource class; declare a `CONFIG_CLS` class attribute when the constructor takes a typed config. See `examples/python/other/custom_resource.py` for a complete runnable backend in one file.

## Contribute a Builtin Resource

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 resource or behavior should also be added to TypeScript.

Use a recent resource 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/
  resource/<name>/
    __init__.py
    config.py
    <name>.py
  accessor/<name>.py
  core/<name>/
    read.py
    readdir.py
    stat.py
  ops/<name>/
    __init__.py
    read.py
    readdir.py
    stat.py
  commands/builtin/<name>/
    __init__.py
    <resource-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 resource name to `ResourceName`, export the config/resource from `resource/<name>/__init__.py`, and add a lazy `ResourceEntry` to `mirage/resource/registry.py`. The registry entry is required for YAML, snapshots, and the daemon to construct the resource.

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 resources 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 thin typed adapters from the workspace dispatcher to core functions. Declare dispatcher-injected arguments explicitly and keep `**flags: object` opaque.

```python theme={null}
from mirage.accessor.base import Accessor
from mirage.cache.index import IndexCacheStore
from mirage.core.my_resource.read import read_bytes
from mirage.ops.registry import op
from mirage.types import PathSpec


@op("read", resource="my_resource")
async def read(
    accessor: Accessor,
    path: PathSpec,
    *,
    index: IndexCacheStore | None,
    **flags: object,
) -> bytes:
    return await read_bytes(accessor, path, index)
```

Mark mutation ops with `write=True`. Export the decorated functions as `OPS` from `ops/<name>/__init__.py`.

## 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 resource-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. Resource Class

Import commands and ops at module scope, then register them in the constructor:

```python theme={null}
from mirage.accessor.my_resource import MyAccessor
from mirage.commands.builtin.my_resource import COMMANDS
from mirage.ops.my_resource import OPS as MY_RESOURCE_OPS
from mirage.resource.base import BaseResource
from mirage.resource.my_resource.config import MyConfig
from mirage.types import ResourceName


class MyResource(BaseResource):
    name: str = ResourceName.MY_RESOURCE
    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_RESOURCE_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.
