     1→import errno
     2→import os
     3→import stat
     4→import subprocess
     5→import sys
     6→import tempfile
     7→
     8→import pytest
     9→from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS
    10→
    11→import fuse
    12→from mirage.backend.local.memory import MemoryBackend
    13→from mirage.types import MountMode
    14→from mirage.workspace import Workspace
    15→
    16→_fuse_available = sys.platform in ("linux", "darwin")
    17→
    18→
    19→@pytest.fixture
    20→def seed_ws():
    21→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    22→    ws.write("/a.txt", b"hello world")
    23→    ws.write("/sub/b.txt", b"nested")
    24→    return ws
    25→
    26→
    27→@pytest.fixture
    28→def rw_ws():
    29→    return Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    30→
    31→
    32→def test_getattr_root(seed_ws):
    33→    fs = MirageFS(seed_ws)
    34→    attrs = fs.getattr("/")
    35→    assert attrs["st_mode"] & stat.S_IFDIR
    36→
    37→
    38→def test_getattr_file(seed_ws):
    39→    fs = MirageFS(seed_ws)
    40→    attrs = fs.getattr("/a.txt")
    41→    assert attrs["st_mode"] & stat.S_IFREG
    42→    assert attrs["st_size"] == len(b"hello world")
    43→
    44→
    45→def test_getattr_dir(seed_ws):
    46→    fs = MirageFS(seed_ws)
    47→    attrs = fs.getattr("/sub")
    48→    assert attrs["st_mode"] & stat.S_IFDIR
    49→
    50→
    51→def test_getattr_missing(seed_ws):
    52→    fs = MirageFS(seed_ws)
    53→    with pytest.raises(fuse.FuseOSError) as exc:
    54→        fs.getattr("/no_such_file.txt")
    55→    assert exc.value.errno == errno.ENOENT
    56→
    57→
    58→def test_readdir_root(seed_ws):
    59→    fs = MirageFS(seed_ws)
    60→    entries = fs.readdir("/", None)
    61→    assert "." in entries
    62→    assert ".." in entries
    63→    assert "a.txt" in entries
    64→    assert "sub" in entries
    65→
    66→
    67→def test_readdir_subdir(seed_ws):
    68→    fs = MirageFS(seed_ws)
    69→    entries = fs.readdir("/sub", None)
    70→    assert "b.txt" in entries
    71→
    72→
    73→def test_read_full(seed_ws):
    74→    fs = MirageFS(seed_ws)
    75→    fh = fs.open("/a.txt", os.O_RDONLY)
    76→    data = fs.read("/a.txt", 1024, 0, fh)
    77→    assert data == b"hello world"
    78→
    79→
    80→def test_read_offset(seed_ws):
    81→    fs = MirageFS(seed_ws)
    82→    fh = fs.open("/a.txt", os.O_RDONLY)
    83→    data = fs.read("/a.txt", 5, 6, fh)
    84→    assert data == b"world"
    85→
    86→
    87→def test_create_and_write(rw_ws):
    88→    fs = MirageFS(rw_ws)
    89→    fh = fs.create("/new.txt", 0o644)
    90→    fs.write("/new.txt", b"data", 0, fh)
    91→    fs.flush("/new.txt", fh)
    92→    assert rw_ws.cat("/new.txt") == b"data"
    93→
    94→
    95→def test_mkdir(rw_ws):
    96→    fs = MirageFS(rw_ws)
    97→    fs.mkdir("/newdir", 0o755)
    98→    entries = fs.readdir("/newdir", None)
    99→    assert "." in entries
   100→
   101→
   102→def test_unlink(rw_ws):
   103→    rw_ws.write("/todel.txt", b"bye")
   104→    fs = MirageFS(rw_ws)
   105→    fs.unlink("/todel.txt")
   106→    with pytest.raises(fuse.FuseOSError) as exc:
   107→        fs.getattr("/todel.txt")
   108→    assert exc.value.errno == errno.ENOENT
   109→
   110→
   111→def test_rename(rw_ws):
   112→    rw_ws.write("/old.txt", b"content")
   113→    fs = MirageFS(rw_ws)
   114→    fs.rename("/old.txt", "/new.txt")
   115→    assert rw_ws.cat("/new.txt") == b"content"
   116→
   117→
   118→def test_truncate(rw_ws):
   119→    rw_ws.write("/f.txt", b"hello world")
   120→    fs = MirageFS(rw_ws)
   121→    fs.truncate("/f.txt", 5)
   122→    assert rw_ws.cat("/f.txt") == b"hello"
   123→
   124→
   125→def test_write_at_offset(rw_ws):
   126→    rw_ws.write("/f.txt", b"hello world")
   127→    fs = MirageFS(rw_ws)
   128→    fh = fs.open("/f.txt", os.O_RDWR)
   129→    fs.write("/f.txt", b"WORLD", 6, fh)
   130→    fs.flush("/f.txt", fh)
   131→    assert rw_ws.cat("/f.txt") == b"hello WORLD"
   132→
   133→
   134→def test_truncate_extend(rw_ws):
   135→    rw_ws.write("/f.txt", b"hi")
   136→    fs = MirageFS(rw_ws)
   137→    fs.truncate("/f.txt", 5)
   138→    assert rw_ws.cat("/f.txt") == b"hi\x00\x00\x00"
   139→
   140→
   141→def test_mirage_dir_getattr(seed_ws):
   142→    fs = MirageFS(seed_ws)
   143→    attrs = fs.getattr(_MIRAGE_DIR)
   144→    assert attrs["st_mode"] & stat.S_IFDIR
   145→
   146→
   147→def test_mirage_whoami_getattr(seed_ws):
   148→    fs = MirageFS(seed_ws, agent_id="test-agent")
   149→    attrs = fs.getattr(_MIRAGE_WHOAMI)
   150→    assert attrs["st_mode"] & stat.S_IFREG
   151→    assert attrs["st_size"] > 0
   152→
   153→
   154→def test_mirage_dir_in_root_readdir(seed_ws):
   155→    fs = MirageFS(seed_ws)
   156→    entries = fs.readdir("/", None)
   157→    assert ".mirage" in entries
   158→
   159→
   160→def test_mirage_dir_readdir(seed_ws):
   161→    fs = MirageFS(seed_ws)
   162→    entries = fs.readdir(_MIRAGE_DIR, None)
   163→    assert "whoami" in entries
   164→    assert "." in entries
   165→    assert ".." in entries
   166→
   167→
   168→def test_whoami_content_has_agent(seed_ws):
   169→    fs = MirageFS(seed_ws, agent_id="claude")
   170→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   171→    data = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   172→    text = data.decode()
   173→    assert "agent: claude" in text
   174→    assert "cwd: /" in text
   175→    assert "mounts:" in text
   176→
   177→
   178→def test_whoami_content_offset(seed_ws):
   179→    fs = MirageFS(seed_ws, agent_id="x")
   180→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   181→    full = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   182→    partial = fs.read(_MIRAGE_WHOAMI, 5, 0, fh)
   183→    assert partial == full[:5]
   184→
   185→
   186→def test_whoami_open(seed_ws):
   187→    fs = MirageFS(seed_ws)
   188→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   189→    assert isinstance(fh, int)
   190→    assert fh > 0
   191→
   192→
   193→def test_agent_id_auto_generated(seed_ws):
   194→    fs = MirageFS(seed_ws)
   195→    assert fs.agent_id.startswith("agent-")
   196→
   197→
   198→def test_agent_id_from_param(seed_ws):
   199→    fs = MirageFS(seed_ws, agent_id="my-bot")
   200→    assert fs.agent_id == "my-bot"
   201→
   202→
   203→def test_agent_id_from_env(seed_ws, monkeypatch):
   204→    monkeypatch.setenv("MIRAGE_AGENT_ID", "env-agent")
   205→    fs = MirageFS(seed_ws)
   206→    assert fs.agent_id == "env-agent"
   207→
   208→
   209→def test_whoami_size_matches_content(seed_ws):
   210→    fs = MirageFS(seed_ws, agent_id="claude")
   211→    attrs = fs.getattr(_MIRAGE_WHOAMI)
   212→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   213→    content = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   214→    assert attrs["st_size"] == len(content)
   215→
   216→
   217→def test_fuse_dispatches_to_registry(seed_ws):
   218→    from mirage.fuse_ops.registry import FuseOpRegistry
   219→    registry = FuseOpRegistry()
   220→    fs = MirageFS(seed_ws, fuse_registry=registry)
   221→    fh = fs.open("/a.txt", os.O_RDONLY)
   222→    data = fs.read("/a.txt", 1024, 0, fh)
   223→    assert data == b"hello world"
   224→
   225→
   226→def test_fuse_write_buffered_flush(rw_ws):
   227→    from mirage.fuse_ops.registry import FuseOpRegistry
   228→    registry = FuseOpRegistry()
   229→    fs = MirageFS(rw_ws, fuse_registry=registry)
   230→    rw_ws.write("/f.txt", b"hello world")
   231→    fh = fs.open("/f.txt", os.O_RDWR)
   232→    fs.write("/f.txt", b"HELLO", 0, fh)
   233→    fs.flush("/f.txt", fh)
   234→    assert rw_ws.cat("/f.txt") == b"HELLO world"
   235→
   236→
   237→def test_fuse_release_cleans_handles(seed_ws):
   238→    fs = MirageFS(seed_ws)
   239→    fh = fs.open("/a.txt", os.O_RDONLY)
   240→    assert fh in fs._handles
   241→    fs.release("/a.txt", fh)
   242→    assert fh not in fs._handles
   243→
   244→
   245→def test_open_returns_unique_handles(seed_ws):
   246→    fs = MirageFS(seed_ws)
   247→    fh1 = fs.open("/a.txt", os.O_RDONLY)
   248→    fh2 = fs.open("/a.txt", os.O_RDONLY)
   249→    assert fh1 != fh2
   250→
   251→
   252→@pytest.mark.skipif(not _fuse_available,
   253→                    reason="FUSE not available on this platform")
   254→def test_mount_background_readable():
   255→    from mirage_fuse import mount_background
   256→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   257→    ws.write("/hello.txt", b"hi from memory")
   258→    with tempfile.TemporaryDirectory() as mountpoint:
   259→        t = mount_background(ws, mountpoint, agent_id="test-agent")
   260→        try:
   261→            path = os.path.join(mountpoint, "hello.txt")
   262→            assert os.path.exists(path)
   263→            with open(path, "rb") as f:
   264→                assert f.read() == b"hi from memory"
   265→        finally:
   266→            if sys.platform == "darwin":
   267→                subprocess.run(["diskutil", "unmount", "force", mountpoint],
   268→                               capture_output=True)
   269→            else:
   270→                subprocess.run(["fusermount", "-u", mountpoint],
   271→                               capture_output=True)
   272→            t.join(timeout=3)
   273→
   274→
   275→@pytest.mark.skipif(not _fuse_available,
   276→                    reason="FUSE not available on this platform")
   277→def test_mount_background_whoami_file():
   278→    from mirage_fuse import mount_background
   279→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   280→    with tempfile.TemporaryDirectory() as mountpoint:
   281→        t = mount_background(ws, mountpoint, agent_id="claude")
   282→        try:
   283→            whoami_path = os.path.join(mountpoint, ".mirage", "whoami")
   284→            assert os.path.exists(whoami_path)
   285→            content = open(whoami_path).read()
   286→            assert "agent: claude" in content
   287→        finally:
   288→            if sys.platform == "darwin":
   289→                subprocess.run(["diskutil", "unmount", "force", mountpoint],
   290→                               capture_output=True)
   291→            else:
   292→                subprocess.run(["fusermount", "-u", mountpoint],
   293→                               capture_output=True)
   294→            t.join(timeout=3)
   295→


     1→import pytest
     2→
     3→from mirage.backend.local.memory import MemoryBackend
     4→from mirage.commands.custom.registry import CommandRegistry
     5→from mirage.fuse_ops.registry import FuseOpRegistry
     6→from mirage.runtime import MemoryRuntime
     7→from mirage.runtime.accessors.local import (LocalCommandsAccessor,
     8→                                            LocalFuseOpsAccessor,
     9→                                            LocalWorkspaceOpsAccessor)
    10→from mirage.types import MountMode
    11→from mirage.workspace import Workspace
    12→
    13→
    14→def _make_ws():
    15→    return Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    16→
    17→
    18→def test_local_commands_list():
    19→    reg = CommandRegistry()
    20→    acc = LocalCommandsAccessor(reg)
    21→    cmds = acc.list()
    22→    assert "ls" in cmds
    23→    assert "cat" in cmds
    24→
    25→
    26→def test_local_commands_register():
    27→    reg = CommandRegistry()
    28→    acc = LocalCommandsAccessor(reg)
    29→    acc.register("myjq", "jq .")
    30→    assert "myjq" in acc.list()
    31→
    32→
    33→def test_local_fuse_ops_list():
    34→    reg = FuseOpRegistry()
    35→    acc = LocalFuseOpsAccessor(reg)
    36→    ops = acc.list()
    37→    assert any(o["pattern"] == "*" for o in ops)
    38→
    39→
    40→def test_local_fuse_ops_register():
    41→    reg = FuseOpRegistry()
    42→    acc = LocalFuseOpsAccessor(reg)
    43→    acc.register("*.parquet", "parquet")
    44→    ops = acc.list()
    45→    assert any(o["pattern"] == "*.parquet" for o in ops)
    46→
    47→
    48→def test_local_fuse_ops_register_unknown():
    49→    reg = FuseOpRegistry()
    50→    acc = LocalFuseOpsAccessor(reg)
    51→    with pytest.raises(ValueError, match="unknown handler"):
    52→        acc.register("*.xyz", "nonexistent_handler_xyz")
    53→
    54→
    55→def test_local_workspace_ops_status():
    56→    ws = _make_ws()
    57→    acc = LocalWorkspaceOpsAccessor(ws)
    58→    result = acc.status()
    59→    assert isinstance(result, list)
    60→
    61→
    62→def test_local_workspace_ops_sync():
    63→    ws = _make_ws()
    64→    acc = LocalWorkspaceOpsAccessor(ws)
    65→    result = acc.sync()
    66→    assert isinstance(result, list)
    67→
    68→
    69→def test_local_workspace_ops_status_after_write():
    70→    ws = _make_ws()
    71→    ws.write("/f.txt", b"hello")
    72→    acc = LocalWorkspaceOpsAccessor(ws)
    73→    result = acc.status()
    74→    paths = [s["path"] for s in result]
    75→    assert "/f.txt" in paths
    76→
    77→
    78→def test_runtime_has_accessors():
    79→    ws = _make_ws()
    80→    rt = MemoryRuntime(ws)
    81→    assert hasattr(rt, "commands")
    82→    assert hasattr(rt, "fuse_ops")
    83→    assert hasattr(rt, "workspace")
    84→    assert "ls" in rt.commands.list()
    85→    assert isinstance(rt.fuse_ops.list(), list)
    86→    assert isinstance(rt.workspace.status(), list)
    87→
    88→
    89→def test_runtime_register_command_then_run():
    90→    ws = _make_ws()
    91→    rt = MemoryRuntime(ws)
    92→    rt.commands.register("hello", "echo hello")
    93→    assert "hello" in rt.commands.list()
    94→


     1→import os
     2→import shlex
     3→import uuid
     4→
     5→from mirage.commands import _resolve
     6→from mirage.commands.builtin import nav
     7→from mirage.commands.custom.registry import CommandRegistry
     8→from mirage.fuse_ops.registry import FuseOpRegistry
     9→from mirage.runtime.accessors.base import (CommandsAccessor, FuseOpsAccessor,
    10→                                           WorkspaceOpsAccessor)
    11→from mirage.runtime.accessors.local import (LocalCommandsAccessor,
    12→                                            LocalFuseOpsAccessor,
    13→                                            LocalWorkspaceOpsAccessor)
    14→from mirage.workspace import Workspace
    15→
    16→_ENV_AGENT_ID = "MIRAGE_AGENT_ID"
    17→
    18→_BLOCKED = {
    19→    "python",
    20→    "python3",
    21→    "node",
    22→    "npm",
    23→    "npx",
    24→    "ruby",
    25→    "perl",
    26→    "php",
    27→    "bash",
    28→    "sh",
    29→    "zsh",
    30→    "fish",
    31→    "exec",
    32→    "eval",
    33→    "source",
    34→    "docker",
    35→    "kubectl",
    36→    "terraform",
    37→    "ansible",
    38→    "git",
    39→    "svn",
    40→    "ssh",
    41→    "scp",
    42→    "sftp",
    43→    "rsync",
    44→    "sudo",
    45→    "su",
    46→    "chmod",
    47→    "chown",
    48→    "kill",
    49→    "ps",
    50→}
    51→
    52→
    53→class CommandNotAllowedError(Exception):
    54→    pass
    55→
    56→
    57→class Runtime:
    58→
    59→    def __init__(self,
    60→                 workspace: Workspace,
    61→                 registry: CommandRegistry | None = None,
    62→                 agent_id: str | None = None) -> None:
    63→        self.ws = workspace
    64→        self._registry = registry or CommandRegistry()
    65→        self._cwd = "/"
    66→        self.agent_id = (agent_id or os.environ.get(_ENV_AGENT_ID)
    67→                         or f"agent-{uuid.uuid4().hex[:8]}")
    68→        self.commands: CommandsAccessor = LocalCommandsAccessor(self._registry)
    69→        self.fuse_ops: FuseOpsAccessor = LocalFuseOpsAccessor(FuseOpRegistry())
    70→        self.workspace: WorkspaceOpsAccessor = LocalWorkspaceOpsAccessor(
    71→            workspace)
    72→
    73→    def run(self, command: str) -> str:
    74→        parts = shlex.split(command)
    75→        if not parts:
    76→            return ""
    77→        cmd, argv = parts[0], parts[1:]
    78→        if cmd in _BLOCKED:
    79→            raise CommandNotAllowedError(f"{cmd!r} is not allowed in Runtime.")
    80→        if cmd == "cd":
    81→            if not argv:
    82→                self._cwd = "/"
    83→                return ""
    84→            self._cwd = nav.cd(self.ws, self._cwd,
    85→                               _resolve(self._cwd, argv[0]))
    86→            return ""
    87→        if cmd == "whoami":
    88→            return self._whoami()
    89→        if cmd == "identify":
    90→            if argv:
    91→                self.agent_id = " ".join(argv)
    92→            return self.agent_id or ""
    93→        return self._registry.run(self.ws, command, self._cwd)
    94→
    95→    def _whoami(self) -> str:
    96→        lines = [f"agent: {self.agent_id}", f"cwd: {self._cwd}"]
    97→        try:
    98→            mounts = list(self.ws._registry._mounts.keys())
    99→            lines.append(f"mounts: {', '.join(mounts)}")
   100→        except Exception:
   101→            pass
   102→        return "\n".join(lines)
   103→


     1→from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
     2→from mirage.fuse_ops import FUSE_OPS, FuseOp
     3→from mirage.fuse_ops.loader import load_fuse_op_class
     4→from mirage.fuse_ops.registry import FuseOpRegistry
     5→from mirage.runtime.accessors.base import (CommandsAccessor, FuseOpsAccessor,
     6→                                           WorkspaceOpsAccessor)
     7→from mirage.workspace import Workspace
     8→
     9→
    10→class LocalCommandsAccessor(CommandsAccessor):
    11→
    12→    def __init__(self, registry: CommandRegistry) -> None:
    13→        self._registry = registry
    14→
    15→    def list(self) -> list[str]:
    16→        return self._registry.list_commands()
    17→
    18→    def register(self,
    19→                 name: str,
    20→                 exec_str: str,
    21→                 description: str = "") -> None:
    22→        self._registry.register(
    23→            ExternalCommand(name=name,
    24→                            **{"exec": exec_str},
    25→                            description=description))
    26→
    27→
    28→class LocalFuseOpsAccessor(FuseOpsAccessor):
    29→
    30→    def __init__(self, registry: FuseOpRegistry) -> None:
    31→        self._registry = registry
    32→
    33→    def list(self) -> list[dict]:
    34→        return self._registry.list_ops()
    35→
    36→    def register(self, pattern: str, handler: str) -> None:
    37→        if handler in FUSE_OPS:
    38→            op: FuseOp = FUSE_OPS[handler]
    39→        else:
    40→            try:
    41→                cls = load_fuse_op_class(handler)
    42→                op = cls()
    43→            except ValueError:
    44→                raise ValueError(f"unknown handler: {handler!r}")
    45→        self._registry.register(pattern, op)
    46→
    47→
    48→class LocalWorkspaceOpsAccessor(WorkspaceOpsAccessor):
    49→
    50→    def __init__(self, ws: Workspace) -> None:
    51→        self._ws = ws
    52→
    53→    def sync(self) -> list[dict]:
    54→        results = self._ws.sync()
    55→        return [r.model_dump() for r in results]
    56→
    57→    def status(self) -> list[dict]:
    58→        inodes = self._ws.status()
    59→        return [{
    60→            "path": i.virtual_path,
    61→            "dirty": i.dirty,
    62→            "size": i.size
    63→        } for i in inodes]
    64→


     1→import os
     2→import signal
     3→import subprocess
     4→import sys
     5→import time
     6→import uuid
     7→from datetime import datetime, timezone
     8→
     9→from pydantic import BaseModel, ConfigDict, Field
    10→
    11→from mirage.backend.loader import load_backend_class
    12→from mirage.cli.config import parse_mount_uri, split_mount_spec
    13→from mirage.commands import COMMANDS, _resolve
    14→from mirage.commands.builtin import nav
    15→from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
    16→from mirage.fuse_ops import FUSE_OPS
    17→from mirage.fuse_ops.loader import (build_registry_from_config,
    18→                                    load_fuse_op_class)
    19→from mirage.fuse_ops.registry import FuseOpRegistry
    20→from mirage.observe.entries import (CommandEntry, CommandResult, DispatchInfo,
    21→                                    ParsedCommand, SessionEntry, WorkspaceInfo)
    22→from mirage.observe.session_log import get_or_create_log
    23→from mirage.runtime.base import _BLOCKED, CommandNotAllowedError
    24→from mirage.server.service.base import MirageService
    25→from mirage.server.service.cache import FileCache
    26→from mirage.types import (CommandStats, ConsistencyPolicy, FileStat, MountMode,
    27→                          RuntimeType, SyncPolicy)
    28→from mirage.workspace import Workspace
    29→
    30→
    31→class WorkspaceSession(BaseModel):
    32→    model_config = ConfigDict(arbitrary_types_allowed=True)
    33→
    34→    id: str
    35→    ws: Workspace | None
    36→    registry: CommandRegistry | None
    37→    cwd: str = "/"
    38→    fuse_mounted: bool = False
    39→    mode: MountMode = MountMode.WRITE
    40→    mountpoint: str | None = None
    41→    pid: int | None = None
    42→    mounts: list[dict] = Field(default_factory=list)
    43→    runtime_type: RuntimeType | None = None
    44→    fuse_registry: FuseOpRegistry = Field(default_factory=FuseOpRegistry)
    45→    command_stats: CommandStats = Field(default_factory=CommandStats)
    46→    created_at: float = Field(default_factory=time.monotonic)
    47→    reported_status: dict | None = None
    48→    reported_transfers: dict | None = None
    49→
    50→
    51→def _unmount_fuse(mountpoint: str, pid: int | None) -> None:
    52→    if sys.platform == "darwin":
    53→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    54→                       capture_output=True)
    55→    else:
    56→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    57→    if pid:
    58→        try:
    59→            os.kill(pid, signal.SIGTERM)
    60→        except ProcessLookupError:
    61→            pass
    62→
    63→
    64→class LocalService(MirageService):
    65→
    66→    def __init__(self) -> None:
    67→        self._sessions: dict[str, WorkspaceSession] = {}
    68→        self._file_cache = FileCache()
    69→
    70→    def _get_session(self, ws_id: str) -> WorkspaceSession:
    71→        session = self._sessions.get(ws_id)
    72→        if session is None:
    73→            raise KeyError(f"workspace {ws_id!r} not found")
    74→        if session.fuse_mounted:
    75→            raise ValueError("not supported for fuse workspaces")
    76→        return session
    77→
    78→    def create_workspace(self, config: dict) -> str:
    79→        mode = config.get("mode", "write")
    80→        consistency = config.get("consistency", "none")
    81→        default_mode = MountMode(mode)
    82→        default_consistency = ConsistencyPolicy(consistency)
    83→        mounts = config.get("mounts", {})
    84→
    85→        backends: dict = {}
    86→        for prefix, value in mounts.items():
    87→            if isinstance(value, str):
    88→                backends[prefix] = parse_mount_uri(value)
    89→            else:
    90→                backend_spec = value.get("backend")
    91→                if backend_spec:
    92→                    cls = load_backend_class(backend_spec)
    93→                    backend = cls(**(value.get("args") or {}))
    94→                else:
    95→                    backend = parse_mount_uri(value.get("uri") or "")
    96→                mount_mode = (MountMode(value["mode"])
    97→                              if value.get("mode") else default_mode)
    98→                mount_consistency = (ConsistencyPolicy(value["consistency"])
    99→                                     if value.get("consistency") else None)
   100→                has_override = (mount_mode != default_mode
   101→                                or mount_consistency is not None)
   102→                if has_override and mount_consistency is not None:
   103→                    backends[prefix] = (backend, mount_mode, mount_consistency)
   104→                elif has_override:
   105→                    backends[prefix] = (backend, mount_mode)
   106→                else:
   107→                    backends[prefix] = backend
   108→
   109→        ws = Workspace(
   110→            backends,
   111→            sync_policy=SyncPolicy(config.get("sync_policy", "staged")),
   112→            mode=default_mode,
   113→            consistency=default_consistency,
   114→            ttl_seconds=config.get("ttl_seconds", 300),
   115→        )
   116→
   117→        mounts_info: list[dict] = []
   118→        for prefix, value in mounts.items():
   119→            if isinstance(value, str):
   120→                uri = value
   121→            else:
   122→                uri = value.get("uri") or ""
   123→                if not uri and value.get("backend"):
   124→                    uri = f"{value['backend']}://"
   125→            mounts_info.append({"prefix": prefix, "uri": uri})
   126→
   127→        ws_id = str(uuid.uuid4())[:8]
   128→        registry = CommandRegistry()
   129→        fuse_ops_config = config.get("fuse_ops") or {}
   130→        fuse_registry = build_registry_from_config(fuse_ops_config)
   131→        runtime = config.get("runtime")
   132→        rt_type = RuntimeType(runtime) if runtime else None
   133→
   134→        self._sessions[ws_id] = WorkspaceSession(
   135→            id=ws_id,
   136→            ws=ws,
   137→            registry=registry,
   138→            mode=mode,
   139→            mounts=mounts_info,
   140→            runtime_type=rt_type,
   141→            fuse_registry=fuse_registry,
   142→        )
   143→
   144→        log = get_or_create_log(ws_id)
   145→        mount_info = {}
   146→        for m in ws._registry.mounts():
   147→            mount_info[m.prefix] = {
   148→                "type": type(m.backend).__name__,
   149→                "mode": m.mode.value,
   150→            }
   151→        session_entry = SessionEntry(
   152→            timestamp=datetime.now(timezone.utc).isoformat(),
   153→            workspace=WorkspaceInfo(
   154→                mounts=mount_info,
   155→                cache_limit=str(ws._cache.limit),
   156→                runtime_type=rt_type.value if rt_type else None,
   157→            ),
   158→            commands=registry.list_commands(),
   159→        )
   160→        log.write_session(session_entry)
   161→        return ws_id
   162→
   163→    def delete_workspace(self, ws_id: str) -> None:
   164→        session = self._sessions.get(ws_id)
   165→        if session is None:
   166→            raise KeyError(f"workspace {ws_id!r} not found")
   167→        if session.fuse_mounted and session.mountpoint:
   168→            _unmount_fuse(session.mountpoint, session.pid)
   169→        self._file_cache.clear(ws_id)
   170→        del self._sessions[ws_id]
   171→
   172→    def list_workspaces(self) -> list[dict]:
   173→        return [{
   174→            "id": s.id,
   175→            "fuse_mounted": s.fuse_mounted,
   176→            "mode": s.mode,
   177→            "mountpoint": s.mountpoint,
   178→            "mounts": s.mounts,
   179→            "runtime": s.runtime_type.value if s.runtime_type else None,
   180→        } for s in self._sessions.values()]
   181→
   182→    def exec(
   183→        self,
   184→        ws_id: str,
   185→        command: str,
   186→        cwd: str | None = None,
   187→        stdin: bytes | None = None,
   188→        input_files: dict[str, tuple[str, bytes | None]] | None = None,
   189→    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
   190→        session = self._get_session(ws_id)
   191→        if cwd is not None:
   192→            session.cwd = cwd
   193→
   194→        cache_status: dict[str, str] = {}
   195→        if input_files:
   196→            for path, (checksum, data) in input_files.items():
   197→                hit, _ = self._file_cache.get(ws_id, path, checksum)
   198→                if hit:
   199→                    cache_status[path] = "hit"
   200→                elif data is not None:
   201→                    self._file_cache.put(ws_id, path, checksum, data)
   202→                    session.ws.write(path, data)
   203→                    cache_status[path] = "miss"
   204→                else:
   205→                    return (False, f"cache miss: {path}", None, None)
   206→
   207→        cmd_name = command.strip().split()[0] if command.strip() else ""
   208→        session.command_stats.total_commands += 1
   209→        session.command_stats.command_counts[cmd_name] = (
   210→            session.command_stats.command_counts.get(cmd_name, 0) + 1)
   211→        session.command_stats.last_command = command
   212→        session.command_stats.last_timestamp = datetime.now(
   213→            timezone.utc).isoformat()
   214→        t0 = time.monotonic()
   215→        try:
   216→            if session.runtime_type is not None and cmd_name in _BLOCKED:
   217→                raise CommandNotAllowedError(
   218→                    f"{cmd_name!r} is not allowed in this runtime")
   219→            output = session.registry.run(session.ws,
   220→                                          command,
   221→                                          cwd=session.cwd,
   222→                                          stdin=stdin)
   223→            duration_ms = int((time.monotonic() - t0) * 1000)
   224→            if command.strip().startswith("cd "):
   225→                parts = command.strip().split(None, 1)
   226→                if len(parts) > 1:
   227→                    session.cwd = nav.cd(session.ws, session.cwd,
   228→                                         _resolve(session.cwd, parts[1]))
   229→            args = command.strip().split()[1:] if command.strip() else []
   230→            source = "builtin" if cmd_name in COMMANDS else "external"
   231→            log = get_or_create_log(ws_id)
   232→            entry = CommandEntry(
   233→                timestamp=datetime.now(timezone.utc).isoformat(),
   234→                raw=command,
   235→                parsed=ParsedCommand(
   236→                    pipeline=[command],
   237→                    args={cmd_name: args},
   238→                ),
   239→                dispatch=[
   240→                    DispatchInfo(
   241→                        command=cmd_name,
   242→                        handler=f"_{cmd_name}"
   243→                        if source == "builtin" else cmd_name,
   244→                        source=source,
   245→                    )
   246→                ],
   247→                result=CommandResult(
   248→                    exit_code=0,
   249→                    stdout_bytes=len(output.encode()) if output else 0,
   250→                    duration_ms=duration_ms,
   251→                ),
   252→            )
   253→            log.write_command(entry)
   254→
   255→            return (True, output, None, cache_status or None)
   256→        except Exception as e:
   257→            duration_ms = int((time.monotonic() - t0) * 1000)
   258→            session.command_stats.total_errors += 1
   259→            log = get_or_create_log(ws_id)
   260→            entry = CommandEntry(
   261→                timestamp=datetime.now(timezone.utc).isoformat(),
   262→                raw=command,
   263→                result=CommandResult(exit_code=1,
   264→                                     stdout_bytes=0,
   265→                                     duration_ms=duration_ms),
   266→            )
   267→            log.write_command(entry)
   268→            return (False, str(e), None, None)
   269→
   270→    def list_commands(self, ws_id: str) -> list[str]:
   271→        session = self._get_session(ws_id)
   272→        return session.registry.list_commands()
   273→
   274→    def register_command(self,
   275→                         ws_id: str,
   276→                         name: str,
   277→                         exec_str: str,
   278→                         description: str = "") -> None:
   279→        session = self._get_session(ws_id)
   280→        session.registry.register(
   281→            ExternalCommand(name=name, exec=exec_str, description=description))
   282→
   283→    def sync(self, ws_id: str) -> list[dict]:
   284→        session = self._get_session(ws_id)
   285→        results = session.ws.sync()
   286→        return [{"path": r.path, "status": r.status} for r in results]
   287→
   288→    def status(self, ws_id: str) -> list[dict]:
   289→        session = self._get_session(ws_id)
   290→        inodes = session.ws.status()
   291→        return [{
   292→            "path": i.virtual_path,
   293→            "dirty": i.dirty,
   294→            "size": i.size
   295→        } for i in inodes]
   296→
   297→    # ── File I/O (backend-level) ─────────────────────────────────────────
   298→
   299→    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
   300→        session = self._get_session(ws_id)
   301→        backend, backend_path, _ = session.ws._registry.resolve(path)
   302→        return backend.read(backend_path, offset, size)
   303→
   304→    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
   305→        session = self._get_session(ws_id)
   306→        backend, backend_path, _ = session.ws._registry.resolve(path)
   307→        return backend.write(backend_path, offset, data)
   308→
   309→    def io_stat(self, ws_id: str, path: str) -> FileStat:
   310→        session = self._get_session(ws_id)
   311→        backend, backend_path, _ = session.ws._registry.resolve(path)
   312→        return backend.stat(backend_path)
   313→
   314→    def io_readdir(self,
   315→                   ws_id: str,
   316→                   path: str,
   317→                   cursor: str | None = None) -> tuple[list[str], str | None]:
   318→        session = self._get_session(ws_id)
   319→        backend, backend_path, _ = session.ws._registry.resolve(path)
   320→        return backend.readdir(backend_path, cursor)
   321→
   322→    def io_create(self, ws_id: str, path: str) -> None:
   323→        session = self._get_session(ws_id)
   324→        backend, backend_path, _ = session.ws._registry.resolve(path)
   325→        backend.create(backend_path)
   326→
   327→    def io_unlink(self, ws_id: str, path: str) -> None:
   328→        session = self._get_session(ws_id)
   329→        backend, backend_path, _ = session.ws._registry.resolve(path)
   330→        backend.unlink(backend_path)
   331→
   332→    def io_mkdir(self, ws_id: str, path: str) -> None:
   333→        session = self._get_session(ws_id)
   334→        backend, backend_path, _ = session.ws._registry.resolve(path)
   335→        backend.mkdir(backend_path)
   336→
   337→    def io_rmdir(self, ws_id: str, path: str) -> None:
   338→        session = self._get_session(ws_id)
   339→        backend, backend_path, _ = session.ws._registry.resolve(path)
   340→        backend.rmdir(backend_path)
   341→
   342→    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
   343→        session = self._get_session(ws_id)
   344→        backend, src_path, _ = session.ws._registry.resolve(src)
   345→        _, dst_path, _ = session.ws._registry.resolve(dst)
   346→        backend.rename(src_path, dst_path)
   347→
   348→    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
   349→        session = self._get_session(ws_id)
   350→        backend, backend_path, _ = session.ws._registry.resolve(path)
   351→        backend.truncate(backend_path, size)
   352→
   353→    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
   354→        session = self._get_session(ws_id)
   355→        st = session.ws.stat(path)
   356→        op = session.fuse_registry.resolve(path, st.size or 0, session.ws)
   357→        ctx = op.on_open(session.ws, path, st.size or 0)
   358→        return op.read(session.ws, path, 0, st.size or 0, ctx)
   359→
   360→    # ── FuseOp management ────────────────────────────────────────────────
   361→
   362→    def list_fuse_ops(self, ws_id: str) -> list[dict]:
   363→        session = self._get_session(ws_id)
   364→        return session.fuse_registry.list_ops()
   365→
   366→    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
   367→        session = self._get_session(ws_id)
   368→        op = FUSE_OPS.get(handler)
   369→        if op is None:
   370→            try:
   371→                cls = load_fuse_op_class(handler)
   372→                op = cls()
   373→            except (ValueError, Exception) as e:
   374→                raise ValueError(f"unknown handler: {handler} ({e})")
   375→        session.fuse_registry.register(pattern, op)
   376→
   377→    # ── FUSE-specific (not part of MirageService ABC) ────────────────────
   378→
   379→    def register_fuse(self,
   380→                      mountpoint: str,
   381→                      pid: int,
   382→                      mount_specs: list[str] | None = None,
   383→                      runtime: str | None = None) -> str:
   384→        fuse_mounts: list[dict] = []
   385→        for spec in (mount_specs or []):
   386→            try:
   387→                uri, prefix = split_mount_spec(spec)
   388→                fuse_mounts.append({"prefix": prefix, "uri": uri})
   389→            except ValueError:
   390→                pass
   391→        ws_id = str(uuid.uuid4())[:8]
   392→        rt_type = RuntimeType(runtime) if runtime else None
   393→        self._sessions[ws_id] = WorkspaceSession(
   394→            id=ws_id,
   395→            ws=None,
   396→            registry=None,
   397→            fuse_mounted=True,
   398→            mountpoint=mountpoint,
   399→            pid=pid,
   400→            mounts=fuse_mounts,
   401→            runtime_type=rt_type,
   402→        )
   403→        mount_info = {m["prefix"]: {"type": m["uri"]} for m in fuse_mounts}
   404→        log = get_or_create_log(ws_id)
   405→        session_entry = SessionEntry(
   406→            timestamp=datetime.now(timezone.utc).isoformat(),
   407→            workspace=WorkspaceInfo(
   408→                mounts=mount_info,
   409→                runtime_type=rt_type.value if rt_type else None,
   410→            ),
   411→        )
   412→        log.write_session(session_entry)
   413→        return ws_id
   414→
   415→    def report_fuse_stats(self,
   416→                          mountpoint: str,
   417→                          status: dict,
   418→                          transfers: dict | None = None,
   419→                          ops: list[dict] | None = None) -> bool:
   420→        for s in self._sessions.values():
   421→            if s.fuse_mounted and s.mountpoint == mountpoint:
   422→                s.reported_status = status
   423→                s.reported_transfers = transfers or {}
   424→                if ops:
   425→                    log = get_or_create_log(s.id)
   426→                    for op in ops:
   427→                        log._append(op)
   428→                return True
   429→        return False
   430→


     1→import os
     2→import sys
     3→import threading
     4→import time
     5→from pathlib import Path
     6→from typing import TYPE_CHECKING, Annotated
     7→
     8→import typer
     9→
    10→from mirage.backend.loader import load_backend_class
    11→from mirage.cli.client import ClientError
    12→from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
    13→                               parse_mount_uri_full, split_mount_spec)
    14→from mirage.cli.helpers import _client, _ensure_server
    15→from mirage.fuse_ops.loader import build_registry_from_config
    16→from mirage.fuse_ops.registry import FuseOpRegistry
    17→from mirage.observe.inspector import (workspace_status_from_ws,
    18→                                      workspace_transfers_from_ws)
    19→from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    20→from mirage.workspace import Workspace
    21→
    22→if TYPE_CHECKING:
    23→    from mirage_fuse import MirageFS
    24→
    25→workspace_app = typer.Typer(no_args_is_help=True)
    26→
    27→REPORT_INTERVAL = 2
    28→
    29→
    30→def _report_loop(ws: Workspace, mountpoint: str, fs: "MirageFS") -> None:
    31→    from mirage.cli.client import MirageClient
    32→    client = MirageClient()
    33→    time.sleep(2)
    34→    while True:
    35→        try:
    36→            status = workspace_status_from_ws(ws)
    37→            transfers = workspace_transfers_from_ws(ws)
    38→            ops = fs.drain_ops()
    39→            client.report_fuse_stats(mountpoint, status, transfers, ops)
    40→        except Exception:
    41→            pass
    42→        time.sleep(REPORT_INTERVAL)
    43→
    44→
    45→def _parse_mounts_for_fuse(
    46→    specs: list[str],
    47→    cfg: dict,
    48→    default_mode: MountMode,
    49→    default_consistency: ConsistencyPolicy,
    50→    ttl: int,
    51→    agent_id: str | None,
    52→) -> tuple[dict, MountMode, ConsistencyPolicy, int, str | None]:
    53→    resolved_agent_id = agent_id
    54→    mounts: dict = {}
    55→    if cfg:
    56→        if "consistency" in cfg:
    57→            default_consistency = ConsistencyPolicy(cfg["consistency"])
    58→        if "ttl" in cfg:
    59→            ttl = cfg["ttl"]
    60→        if not resolved_agent_id and "agent_id" in cfg:
    61→            resolved_agent_id = cfg["agent_id"]
    62→        for prefix, value in cfg.get("mounts", {}).items():
    63→            if isinstance(value, str):
    64→                backend_obj, m, c = parse_mount_uri_full(
    65→                    value, default_mode, default_consistency)
    66→            elif isinstance(value, dict) and "backend" in value:
    67→                cls = load_backend_class(value["backend"])
    68→                backend_obj = cls(**(value.get("args") or {}))
    69→                m = (MountMode(value["mode"])
    70→                     if "mode" in value else default_mode)
    71→                c = (ConsistencyPolicy(value["consistency"])
    72→                     if "consistency" in value else default_consistency)
    73→            elif isinstance(value, dict):
    74→                backend_obj = parse_mount_uri(value.get("uri", ""))
    75→                m = (MountMode(value["mode"])
    76→                     if "mode" in value else default_mode)
    77→                c = (ConsistencyPolicy(value["consistency"])
    78→                     if "consistency" in value else default_consistency)
    79→            else:
    80→                continue
    81→            has_override = (m != default_mode or c != default_consistency)
    82→            if has_override:
    83→                mounts[prefix] = (backend_obj, m, c)
    84→            else:
    85→                mounts[prefix] = backend_obj
    86→    for spec in specs:
    87→        uri, prefix = split_mount_spec(spec)
    88→        backend_obj, m, c = parse_mount_uri_full(uri, default_mode,
    89→                                                 default_consistency)
    90→        has_override = (m != default_mode or c != default_consistency)
    91→        if has_override:
    92→            mounts[prefix] = (backend_obj, m, c)
    93→        else:
    94→            mounts[prefix] = backend_obj
    95→    return mounts, default_mode, default_consistency, ttl, resolved_agent_id
    96→
    97→
    98→def mount(
    99→    mount_specs: Annotated[
   100→        list[str] | None,
   101→        typer.Argument(help="Mount specs: backend:/path "
   102→                       "(e.g. memory:/data/, s3://bucket:/). "
   103→                       "Append #none|#lazy|#ttl|#always for "
   104→                       "consistency, @read|@write|@exec for mode.")] = None,
   105→    sync_policy: str = typer.Option("staged", help="staged or immediate"),
   106→    mode: MountMode = typer.Option(MountMode.WRITE, help="read or write"),
   107→    consistency: ConsistencyPolicy = typer.Option(ConsistencyPolicy.NONE),
   108→    ttl: int = typer.Option(300, help="TTL in seconds for ttl consistency"),
   109→    config: str | None = typer.Option(None,
   110→                                      help="Path to YAML workspace config"),
   111→    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
   112→                                        help="Runtime: memory or local"),
   113→    fuse: bool = typer.Option(False, help="Also mount as FUSE filesystem"),
   114→    mountpoint: str | None = typer.Option(
   115→        None, help="Local directory for FUSE mount (requires --fuse)"),
   116→    agent_id: str | None = typer.Option(
   117→        None, help="Agent identity exposed via /.mirage/whoami"),
   118→    backend: str | None = typer.Option(
   119→        None,
   120→        help="Backend URI mounted at /. Prefix specs override for subtree."),
   121→) -> None:
   122→    """Create a workspace and mount backends.
   123→
   124→    Each backend URI is routed by prefix. The longest matching prefix wins,
   125→    so /data/logs/ takes priority over /data/ for paths under /data/logs/.
   126→
   127→    With --fuse, also mounts the workspace as a local FUSE filesystem
   128→    (requires macFUSE / libfuse). The process blocks while FUSE is mounted.
   129→
   130→    URI formats:
   131→      memory             in-memory (no disk, no deps)
   132→      local              local filesystem at current directory
   133→      local:./path       local filesystem at ./path (converted to abs)
   134→      s3://bucket/pfx    AWS S3  (requires mirage[s3])
   135→      r2://account/bkt   Cloudflare R2  (requires mirage[s3])
   136→      https://host       HTTP read-only (requires mirage[http])
   137→    """
   138→    specs = list(mount_specs or [])
   139→    if backend:
   140→        specs.insert(0, f"{backend}:/")
   141→
   142→    if fuse:
   143→        _mount_fuse(specs, config, mode, consistency, ttl, agent_id,
   144→                    mountpoint, runtime)
   145→        return
   146→
   147→    _ensure_server()
   148→    mounts: dict[str, str | dict] = {}
   149→    if config:
   150→        cfg = load_workspace_yaml(config)
   151→        mounts.update(cfg.get("mounts", {}))
   152→        consistency_str = cfg.get("consistency")
   153→        if consistency_str:
   154→            consistency = ConsistencyPolicy(consistency_str)
   155→        ttl = cfg.get("ttl", ttl)
   156→    consistency_suffixes = ("#none", "#lazy", "#ttl", "#always")
   157→    for spec in specs:
   158→        try:
   159→            uri, prefix = split_mount_spec(spec)
   160→        except ValueError as e:
   161→            raise typer.BadParameter(str(e))
   162→        mount_consistency = None
   163→        for suffix in consistency_suffixes:
   164→            if uri.endswith(suffix):
   165→                mount_consistency = suffix[1:]
   166→                uri = uri[:-len(suffix)]
   167→                break
   168→        mount_mode = None
   169→        for suffix in ("@read", "@write", "@exec"):
   170→            if uri.endswith(suffix):
   171→                mount_mode = suffix[1:]
   172→                uri = uri[:-len(suffix)]
   173→                break
   174→        entry: dict[str, str] = {"uri": uri}
   175→        if mount_mode:
   176→            entry["mode"] = mount_mode
   177→        if mount_consistency:
   178→            entry["consistency"] = mount_consistency
   179→        if len(entry) == 1:
   180→            mounts[prefix] = uri
   181→        else:
   182→            mounts[prefix] = entry
   183→    ws_id = _client().create_workspace(
   184→        mounts,
   185→        sync_policy=sync_policy,
   186→        mode=mode.value,
   187→        consistency=consistency.value,
   188→        ttl_seconds=ttl,
   189→        runtime=runtime.value,
   190→    )
   191→    print(ws_id)
   192→
   193→
   194→def _mount_fuse(
   195→    specs: list[str],
   196→    config: str | None,
   197→    mode: MountMode,
   198→    consistency: ConsistencyPolicy,
   199→    ttl: int,
   200→    agent_id: str | None,
   201→    mountpoint: str | None,
   202→    runtime: RuntimeType,
   203→) -> None:
   204→    from mirage_fuse import MirageFS
   205→    from mirage_fuse import mount as fuse_mount_fn
   206→
   207→    from mirage.backend.network.proxy import ProxyBackend
   208→    from mirage.fuse_ops.remote import RemoteFuseOp
   209→
   210→    if not mountpoint:
   211→        raise typer.BadParameter("--mountpoint is required when using --fuse")
   212→    if not specs and not config:
   213→        raise typer.BadParameter(
   214→            "No backends specified. Use --backend URI or provide mount specs.")
   215→    for spec in specs:
   216→        if ":" not in spec:
   217→            raise typer.BadParameter(
   218→                f"Invalid mount spec: {spec!r}. Use backend:/path")
   219→
   220→    Path(mountpoint).mkdir(parents=True, exist_ok=True)
   221→    _ensure_server()
   222→    cfg = load_workspace_yaml(config) if config else {}
   223→
   224→    if runtime == RuntimeType.REMOTE:
   225→        url = cfg.get("url", "")
   226→        ws_id_remote = cfg.get("ws_id", "")
   227→        proxy = ProxyBackend(url, ws_id_remote)
   228→        ws = Workspace({"/": proxy}, mode=MountMode.WRITE)
   229→        fuse_registry = FuseOpRegistry()
   230→        fuse_registry.set_default(RemoteFuseOp(proxy))
   231→        fs = MirageFS(ws, agent_id=agent_id, fuse_registry=fuse_registry)
   232→    else:
   233→        mounts, mode, consistency, ttl, agent_id = _parse_mounts_for_fuse(
   234→            specs, cfg, mode, consistency, ttl, agent_id)
   235→        ws = Workspace(mounts,
   236→                       mode=mode,
   237→                       consistency=consistency,
   238→                       ttl_seconds=ttl)
   239→        fuse_ops_config = cfg.get("fuse_ops", {}) if cfg else {}
   240→        fuse_registry = build_registry_from_config(fuse_ops_config)
   241→        fs = MirageFS(ws, agent_id=agent_id, fuse_registry=fuse_registry)
   242→
   243→    pid = os.getpid()
   244→    _client().register_fuse(mountpoint, pid, specs, runtime=runtime.value)
   245→
   246→    thread = threading.Thread(target=_report_loop,
   247→                              args=(ws, mountpoint, fs),
   248→                              daemon=True)
   249→    thread.start()
   250→
   251→    fuse_mount_fn(mountpoint=mountpoint, foreground=True, fs=fs)
   252→
   253→
   254→def exec_cmd(
   255→        ws_id: str = typer.Argument(),
   256→        command: list[str] = typer.Argument(),
   257→) -> None:
   258→    """Execute a command in a running workspace."""
   259→    cmd_str = " ".join(command)
   260→    try:
   261→        output = _client().exec(ws_id, cmd_str)
   262→        if output:
   263→            print(output)
   264→    except ClientError as e:
   265→        print(f"Error: {e}", file=sys.stderr)
   266→        raise typer.Exit(1)
   267→
   268→
   269→def kill(ws_id: str = typer.Argument()) -> None:
   270→    """Stop a workspace daemon."""
   271→    _client().delete_workspace(ws_id)
   272→    print(f"Killed workspace {ws_id}")
   273→
   274→
   275→def sync(ws_id: str = typer.Argument()) -> None:
   276→    """Flush dirty inodes to backends for a workspace."""
   277→    try:
   278→        results = _client().sync(ws_id)
   279→        if results:
   280→            for r in results:
   281→                print(f"{r['path']}: {r['status']}")
   282→        else:
   283→            print("All clean.")
   284→    except ClientError as e:
   285→        print(f"Error: {e}", file=sys.stderr)
   286→        raise typer.Exit(1)
   287→
   288→
   289→def status(ws_id: str = typer.Argument()) -> None:
   290→    """Show dirty/clean inode status for a workspace."""
   291→    try:
   292→        inodes = _client().status(ws_id)
   293→        if inodes:
   294→            for i in inodes:
   295→                marker = "D" if i["dirty"] else " "
   296→                print(f"{marker} {i['path']}")
   297→        else:
   298→            print("No tracked files.")
   299→    except ClientError as e:
   300→        print(f"Error: {e}", file=sys.stderr)
   301→        raise typer.Exit(1)
   302→
   303→
   304→@workspace_app.command(name="ls")
   305→def workspace_ls() -> None:
   306→    """List active workspaces."""
   307→    _ensure_server()
   308→    workspaces = _client().list_workspaces()
   309→    if not workspaces:
   310→        print("No active workspaces.")
   311→        return
   312→    for w in workspaces:
   313→        parts = [w["id"], w.get("type", "workspace")]
   314→        if w.get("mountpoint"):
   315→            parts.append(w["mountpoint"])
   316→        print("\t".join(parts))
   317→


     1→"""
     2→Example: Run Claude Code inside a memory-backed FUSE filesystem.
     3→
     4→All file operations from Claude happen in-memory — nothing touches disk.
     5→After Claude exits, the workspace still holds everything Claude created.
     6→
     7→Usage:
     8→    python examples/claude-memory.py
     9→    python examples/claude-memory.py --agent-id my-bot \
    10→        --prompt "write a fibonacci.py"
    11→"""
    12→
    13→import argparse
    14→import os
    15→import subprocess
    16→import sys
    17→import tempfile
    18→
    19→from mirage_fuse import mount_background
    20→
    21→from mirage.backend.local.memory import MemoryBackend
    22→from mirage.types import MountMode
    23→from mirage.workspace import Workspace
    24→
    25→
    26→def unmount(mountpoint: str) -> None:
    27→    if sys.platform == "darwin":
    28→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    29→                       capture_output=True)
    30→    else:
    31→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    32→
    33→
    34→def main() -> None:
    35→    parser = argparse.ArgumentParser()
    36→    parser.add_argument("--agent-id", default="claude", dest="agent_id")
    37→    parser.add_argument("--prompt",
    38→                        default="write a hello world script in /hello.py")
    39→    args = parser.parse_args()
    40→
    41→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    42→
    43→    with tempfile.TemporaryDirectory() as mountpoint:
    44→        t = mount_background(ws, mountpoint, agent_id=args.agent_id)
    45→        print(f"Mounted memory filesystem at {mountpoint}")
    46→        print(f"Agent: {args.agent_id}")
    47→
    48→        try:
    49→            subprocess.run(
    50→                ["claude", "-p", args.prompt],
    51→                cwd=mountpoint,
    52→                env={
    53→                    **os.environ, "MIRAGE_AGENT_ID": args.agent_id
    54→                },
    55→            )
    56→        finally:
    57→            unmount(mountpoint)
    58→            t.join(timeout=3)
    59→
    60→    print("\n=== Files Claude created (in memory) ===")
    61→    for path in ws.ls("/"):
    62→        try:
    63→            content = ws.cat(path)
    64→            print(f"\n--- {path} ({len(content)} bytes) ---")
    65→            print(content.decode(errors="replace"))
    66→        except (IsADirectoryError, ValueError):
    67→            print(f"\n--- {path}/ (directory) ---")
    68→
    69→
    70→if __name__ == "__main__":
    71→    main()
    72→


     1→"""
     2→Example: Use the Claude Agent SDK with a Mirage FUSE-backed workspace.
     3→
     4→The agent reads, writes, and edits files — all intercepted by Mirage's
     5→FUSE layer.  Every file operation is recorded and can be inspected after
     6→the agent finishes.
     7→
     8→Usage:
     9→    pip install claude-agent-sdk
    10→    python examples/claude-sdk.py
    11→    python examples/claude-sdk.py --agent-id code-bot \
    12→        --prompt "create a python calculator module with tests"
    13→"""
    14→
    15→import argparse
    16→import subprocess
    17→import sys
    18→import tempfile
    19→
    20→import anyio
    21→from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
    22→from mirage_fuse import MirageFS, mount_background
    23→
    24→from mirage.backend.local.memory import MemoryBackend
    25→from mirage.types import MountMode
    26→from mirage.workspace import Workspace
    27→
    28→
    29→def unmount(mountpoint: str) -> None:
    30→    if sys.platform == "darwin":
    31→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    32→                       capture_output=True)
    33→    else:
    34→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    35→
    36→
    37→def print_workspace(ws: Workspace, prefix: str = "/") -> None:
    38→    for path in ws.ls(prefix):
    39→        try:
    40→            content = ws.cat(path)
    41→            print(f"\n--- {path} ({len(content)} bytes) ---")
    42→            print(content.decode(errors="replace"))
    43→        except (IsADirectoryError, ValueError):
    44→            print(f"\n--- {path}/ (directory) ---")
    45→
    46→
    47→def print_ops(ops: list[dict]) -> None:
    48→    for op in ops:
    49→        ts = op["timestamp"]
    50→        print(f"  [{ts}] {op['op']:6s}  {op['path']}")
    51→
    52→
    53→async def run_agent(mountpoint: str, prompt: str, agent_id: str) -> None:
    54→    options = ClaudeAgentOptions(
    55→        cwd=mountpoint,
    56→        allowed_tools=["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
    57→        permission_mode="bypassPermissions",
    58→        allow_dangerously_skip_permissions=True,
    59→        env={"MIRAGE_AGENT_ID": agent_id},
    60→    )
    61→    async for message in query(prompt=prompt, options=options):
    62→        if isinstance(message, ResultMessage):
    63→            print(f"\n=== Agent result ===\n{message.result}")
    64→
    65→
    66→def main() -> None:
    67→    parser = argparse.ArgumentParser()
    68→    parser.add_argument("--agent-id", default="claude-sdk", dest="agent_id")
    69→    parser.add_argument(
    70→        "--prompt",
    71→        default=(
    72→            "Create a file called /hello.py that prints 'Hello from Mirage!' "
    73→            "and a file called /utils.py with a function"
    74→            " that reverses a string."),
    75→    )
    76→    args = parser.parse_args()
    77→
    78→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    79→
    80→    with tempfile.TemporaryDirectory() as mountpoint:
    81→        fs = MirageFS(ws, agent_id=args.agent_id)
    82→        t = mount_background(ws, mountpoint, agent_id=args.agent_id)
    83→        print(f"Mounted memory workspace at {mountpoint}")
    84→        print(f"Agent: {args.agent_id}")
    85→        print(f"Prompt: {args.prompt}\n")
    86→
    87→        try:
    88→            anyio.run(run_agent, mountpoint, args.prompt, args.agent_id)
    89→        finally:
    90→            ops = fs.drain_ops()
    91→            unmount(mountpoint)
    92→            t.join(timeout=3)
    93→
    94→    print("\n=== FUSE operations captured ===")
    95→    if ops:
    96→        print_ops(ops)
    97→    else:
    98→        print("  (no operations recorded)")
    99→
   100→    print("\n=== Files in workspace (all in-memory) ===")
   101→    try:
   102→        print_workspace(ws)
   103→    except FileNotFoundError:
   104→        print("  (workspace is empty)")
   105→
   106→
   107→if __name__ == "__main__":
   108→    main()
   109→


     1→# mirage-fuse
     2→
     3→Expose any mirage Workspace (memory, S3, R2, HTTP) as a native macOS directory so Claude Code and any CLI tool can read and write remote files transparently.
     4→
     5→## Prerequisites
     6→
     7→```bash
     8→brew install --cask macfuse
     9→uv add mirage-fuse
    10→```
    11→
    12→### macOS Security Setup
    13→
    14→1. Open **System Settings → Privacy & Security**. Scroll to the bottom — you'll see:
    15→
    16→> "System software from developer 'Benjamin Fleischer' was blocked from loading."
    17→> Click **Allow**.
    18→
    19→2. **Restart your Mac.** macOS requires a reboot to load the kernel extension.
    20→1. If you don't see the Allow button (Apple Silicon M1/M2/M3/M4):
    21→
    22→- Shut down completely
    23→- Hold power button until "Loading startup options" appears
    24→- Click **Options → Continue**
    25→- In Recovery: **Utilities → Startup Security Utility**
    26→- Set to **Reduced Security** and check **"Allow user management of kernel extensions"**
    27→- Restart, then retry step 1
    28→
    29→## Usage
    30→
    31→### CLI
    32→
    33→```bash
    34→# Mount an in-memory workspace as a real directory (writable)
    35→mirage mount-fuse /data/:memory:// --mountpoint /tmp/ws/ --mode write --foreground
    36→# (runs in foreground — open a new terminal for the next commands)
    37→
    38→# In another terminal: any OS tool works natively
    39→ls /tmp/ws/data/
    40→echo "hello" > /tmp/ws/data/notes.txt
    41→cat /tmp/ws/data/notes.txt
    42→
    43→# Use Claude Code directly on the mounted path
    44→cd /tmp/ws/data && claude -p "write a hello.py script here"
    45→
    46→# Ctrl+C in the first terminal to unmount
    47→```
    48→
    49→For read-only S3/R2 access:
    50→
    51→```bash
    52→mirage mount-fuse /data/:s3://my-bucket --mountpoint /tmp/s3/ --mode read
    53→claude -p "summarize the files in /tmp/s3/data/reports/"
    54→# Ctrl+C to unmount
    55→```
    56→
    57→### Python API
    58→
    59→```python
    60→from mirage.backend.local.memory import MemoryBackend
    61→from mirage.workspace import Workspace
    62→from mirage_fuse import MirageFS, mount
    63→
    64→ws = Workspace({"/data/": MemoryBackend()}, mode="write")
    65→mount(ws, "/tmp/ws/", foreground=True)
    66→```
    67→
    68→## Supported FUSE Operations
    69→
    70→| Operation  | Description             |
    71→| ---------- | ----------------------- |
    72→| `getattr`  | File/directory metadata |
    73→| `readdir`  | List directory contents |
    74→| `read`     | Read file contents      |
    75→| `write`    | Write file contents     |
    76→| `create`   | Create new files        |
    77→| `mkdir`    | Create directories      |
    78→| `unlink`   | Delete files            |
    79→| `rename`   | Move/rename files       |
    80→| `truncate` | Resize files            |
    81→| `open`     | Open existing files     |
    82→| `release`  | Close file handles      |
    83→


Edit

from mirage.fuse_ops.remote import RemoteFuseOp

from mirage.fuse.remote.remote import RemoteFuseOp


The file /Users/agent/strukto/mirage/tests/fuse/test_remote.py has been updated successfully.


from mirage.fuse_ops.default import DefaultFuseOp
from mirage.fuse_ops.memory import MemoryFuseOp

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp


The file /Users/agent/strukto/mirage/tests/fuse/test_default.py has been updated successfully.


from mirage.fuse_ops.loader import (build_registry_from_config,
                                    load_fuse_op_class)

from mirage.fuse.loader import (build_registry_from_config,
                                load_fuse_op_class)


The file /Users/agent/strukto/mirage/tests/fuse/test_loader.py has been updated successfully.


code = ("from mirage.fuse_ops import FuseOp\n"

code = ("from mirage.fuse import FuseOp\n"


from mirage.fuse_ops.csv_op import CsvFuseOp
from mirage.fuse_ops.parquet import ParquetFuseOp

from mirage.fuse.filetype.csv import CsvFuseOp
from mirage.fuse.filetype.parquet import ParquetFuseOp


The file /Users/agent/strukto/mirage/tests/fuse/test_format_ops.py has been updated successfully.


from mirage.fuse_ops import FUSE_OPS, FuseOp
from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse import FUSE_OPS, FuseOp
from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/tests/fuse/test_registry.py has been updated successfully.


from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS

from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS


The file /Users/agent/strukto/mirage/tests/fuse/test_fs.py has been updated successfully.


from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/tests/fuse/test_fs.py has been updated. All occurrences of '    from mirage.fuse_ops.registry import FuseOpRegistry' were successfully replaced with '    from mirage.fuse.registry import FuseOpRegistry'.


from mirage_fuse import mount_background

from mirage.fuse.fs import mount_background


The file /Users/agent/strukto/mirage/tests/fuse/test_fs.py has been updated. All occurrences of '    from mirage_fuse import mount_background' were successfully replaced with '    from mirage.fuse.fs import mount_background'.


from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/tests/runtime/test_accessors.py has been updated successfully.


from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/mirage/runtime/base.py has been updated successfully.


from mirage.fuse_ops import FUSE_OPS, FuseOp
from mirage.fuse_ops.loader import load_fuse_op_class
from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse import FUSE_OPS, FuseOp
from mirage.fuse.loader import load_fuse_op_class
from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/mirage/runtime/accessors/local.py has been updated successfully.


from mirage.fuse_ops import FUSE_OPS
from mirage.fuse_ops.loader import (build_registry_from_config,
                                    load_fuse_op_class)
from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse import FUSE_OPS
from mirage.fuse.loader import (build_registry_from_config,
                                load_fuse_op_class)
from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/mirage/server/service/local.py has been updated successfully.


from mirage.fuse_ops.loader import build_registry_from_config
from mirage.fuse_ops.registry import FuseOpRegistry

from mirage.fuse.loader import build_registry_from_config
from mirage.fuse.registry import FuseOpRegistry


The file /Users/agent/strukto/mirage/mirage/cli/commands/workspace.py has been updated successfully.


if TYPE_CHECKING:
    from mirage_fuse import MirageFS

if TYPE_CHECKING:
    from mirage.fuse.fs import MirageFS


    from mirage_fuse import MirageFS
    from mirage_fuse import mount as fuse_mount_fn

    from mirage.backend.network.proxy import ProxyBackend
    from mirage.fuse_ops.remote import RemoteFuseOp

    from mirage.fuse.fs import MirageFS
    from mirage.fuse.fs import mount as fuse_mount_fn

    from mirage.backend.network.proxy import ProxyBackend
    from mirage.fuse.remote.remote import RemoteFuseOp


from mirage_fuse import mount_background

from mirage.fuse.fs import mount_background


The file /Users/agent/strukto/mirage/examples/claude-memory.py has been updated successfully.


from mirage_fuse import MirageFS, mount_background

from mirage.fuse.fs import MirageFS, mount_background


The file /Users/agent/strukto/mirage/examples/claude-sdk.py has been updated successfully.


from mirage.fuse.fs import MirageFS, mount


The file /Users/agent/strukto/mirage/mirage/fuse/README.md has been updated successfully.


tests/server/test_app.py:245:def test_list_fuse_ops(client):
tests/cli/test_app.py:49:        with patch("mirage_fuse.mount") as mock_fuse, \
tests/cli/test_app.py:62:        with patch("mirage_fuse.mount") as mock_fuse, \
tests/cli/test_config.py:182:def test_load_workspace_yaml_with_fuse_ops(tmp_path):
tests/cli/test_config.py:186:                        "fuse_ops:\n"
tests/cli/test_config.py:190:    assert "fuse_ops" in cfg
tests/cli/test_config.py:191:    assert cfg["fuse_ops"]["*.parquet"] == "parquet"
tests/cli/test_config.py:192:    assert cfg["fuse_ops"]["*.csv"] == "csv"
tests/cli/test_config.py:195:def test_load_workspace_yaml_no_fuse_ops(tmp_path):
tests/cli/test_config.py:199:    assert "fuse_ops" not in cfg
tests/cli/test_config.py:220:                        "fuse_ops_url: http://ops:8002\n"
tests/cli/test_config.py:226:    assert cfg["fuse_ops_url"] == "http://ops:8002"
tests/runtime/test_remote.py:94:def test_remote_fuse_ops_list(rt):
tests/runtime/test_remote.py:95:    ops = rt.fuse_ops.list()
tests/runtime/test_remote.py:100:def test_remote_fuse_ops_register(rt):
tests/runtime/test_remote.py:101:    rt.fuse_ops.register("*.parquet", "parquet")
tests/runtime/test_remote.py:102:    ops = rt.fuse_ops.list()
tests/server/test_service.py:176:def test_list_fuse_ops(svc):
tests/server/test_service.py:178:    ops = svc.list_fuse_ops(ws_id)
tests/server/test_service.py:186:    ops = svc.list_fuse_ops(ws_id)
tests/observe/test_api.py:243:    fuse_ops = [e for e in history if e["type"] == "fuse_op"]
tests/observe/test_api.py:244:    assert len(fuse_ops) == 2
tests/observe/test_api.py:245:    assert fuse_ops[0]["op"] == "create"
tests/observe/test_api.py:246:    assert fuse_ops[1]["raw"] == "write /data/a.py"
mirage/runtime/base.py:69:        self.fuse_ops: FuseOpsAccessor = LocalFuseOpsAccessor(FuseOpRegistry())
mirage/runtime/remote.py:33:        self.fuse_ops = RemoteFuseOpsAccessor(self._http, ws_id)
mirage/cli/commands/workspace.py:239:        fuse_ops_config = cfg.get("fuse_ops", {}) if cfg else {}
mirage/cli/commands/workspace.py:240:        fuse_registry = build_registry_from_config(fuse_ops_config)
tests/runtime/test_accessors.py:33:def test_local_fuse_ops_list():
tests/runtime/test_accessors.py:40:def test_local_fuse_ops_register():
tests/runtime/test_accessors.py:48:def test_local_fuse_ops_register_unknown():
tests/runtime/test_accessors.py:82:    assert hasattr(rt, "fuse_ops")
tests/runtime/test_accessors.py:85:    assert isinstance(rt.fuse_ops.list(), list)
mirage/server/service/base.py:99:    def list_fuse_ops(self, ws_id: str) -> list[dict]:
fuse/mirage_fuse/__init__.py:1:from mirage_fuse.fs import MirageFS, mount, mount_background
mirage/server/service/local.py:129:        fuse_ops_config = config.get("fuse_ops") or {}
mirage/server/service/local.py:130:        fuse_registry = build_registry_from_config(fuse_ops_config)
mirage/server/service/local.py:362:    def list_fuse_ops(self, ws_id: str) -> list[dict]:
tests/fuse/test_registry.py:9:def test_fuse_ops_dict_has_default():
tests/fuse/test_registry.py:13:def test_fuse_ops_dict_has_all_backends():
tests/fuse/test_registry.py:57:def test_fuse_ops_dict_has_format_ops():
mirage/server/fastapi/config.py:21:    fuse_ops: dict[str, str] | None = None
mirage/cli/config.py:177:    fuse_ops = data.get("fuse_ops", {})
mirage/cli/config.py:178:    if fuse_ops:
mirage/cli/config.py:179:        result["fuse_ops"] = fuse_ops
mirage/cli/config.py:180:    for key in ("runtime", "url", "ws_id", "command_url", "fuse_ops_url"):
mirage/fuse_ops/__init__.py:1:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/__init__.py:2:from mirage.fuse_ops.csv_op import CsvFuseOp
mirage/fuse_ops/__init__.py:3:from mirage.fuse_ops.default import DefaultFuseOp
mirage/fuse_ops/__init__.py:4:from mirage.fuse_ops.local import LocalFuseOp
mirage/fuse_ops/__init__.py:5:from mirage.fuse_ops.memory import MemoryFuseOp
mirage/fuse_ops/__init__.py:6:from mirage.fuse_ops.parquet import ParquetFuseOp
mirage/fuse_ops/__init__.py:7:from mirage.fuse_ops.s3 import S3FuseOp
mirage/server/fastapi/routers/command_router.py:90:def list_fuse_ops(ws_id: str, request: Request) -> list[dict]:
mirage/server/fastapi/routers/command_router.py:92:    return svc.list_fuse_ops(ws_id)
mirage/fuse/loader.py:35:def build_registry_from_config(fuse_ops: dict) -> FuseOpRegistry:
mirage/fuse/loader.py:37:    for pattern, value in fuse_ops.items():


No matches found


46-    """--backend memory with --fuse mounts the backend at / prefix."""
47-    from mirage.cli.commands.workspace import mount
48-    with tempfile.TemporaryDirectory() as mp:
49:        with patch("mirage_fuse.mount") as mock_fuse, \
50-             patch("mirage.cli.commands.workspace._ensure_server"), \
51-             patch("mirage.cli.commands.workspace._client"):
52-            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
--
59-    """--backend combined with positional prefix:uri spec and --fuse."""
60-    from mirage.cli.commands.workspace import mount
61-    with tempfile.TemporaryDirectory() as mp:
62:        with patch("mirage_fuse.mount") as mock_fuse, \
63-             patch("mirage.cli.commands.workspace._ensure_server"), \
64-             patch("mirage.cli.commands.workspace._client"):
65-            mount(["memory:/data/"],


     1→import os
     2→import subprocess
     3→import sys
     4→import tempfile
     5→import time
     6→from unittest.mock import patch
     7→
     8→import pytest
     9→
    10→
    11→def _run(args: list[str],
    12→         env: dict | None = None,
    13→         check: bool = True) -> subprocess.CompletedProcess:
    14→    e = os.environ.copy()
    15→    if env:
    16→        e.update(env)
    17→    return subprocess.run(
    18→        [sys.executable, "-m", "mirage.cli.app"] + args,
    19→        capture_output=True,
    20→        text=True,
    21→        env=e,
    22→        check=check,
    23→    )
    24→
    25→
    26→def test_help():
    27→    result = _run(["--help"], check=False)
    28→    assert result.returncode == 0
    29→
    30→
    31→def _fuse_defaults() -> dict:
    32→    from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    33→    return dict(
    34→        sync_policy="staged",
    35→        mode=MountMode.READ,
    36→        consistency=ConsistencyPolicy.NONE,
    37→        ttl=300,
    38→        config=None,
    39→        runtime=RuntimeType.MEMORY,
    40→        fuse=True,
    41→        agent_id=None,
    42→    )
    43→
    44→
    45→def test_mount_fuse_backend_mounts_at_root():
    46→    """--backend memory with --fuse mounts the backend at / prefix."""
    47→    from mirage.cli.commands.workspace import mount
    48→    with tempfile.TemporaryDirectory() as mp:
    49→        with patch("mirage_fuse.mount") as mock_fuse, \
    50→             patch("mirage.cli.commands.workspace._ensure_server"), \
    51→             patch("mirage.cli.commands.workspace._client"):
    52→            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
    53→            ws = mock_fuse.call_args.kwargs["fs"].ws
    54→            prefixes = [m.prefix for m in ws._registry._mounts]
    55→            assert "/" in prefixes
    56→
    57→
    58→def test_mount_fuse_backend_with_extra_spec():
    59→    """--backend combined with positional prefix:uri spec and --fuse."""
    60→    from mirage.cli.commands.workspace import mount
    61→    with tempfile.TemporaryDirectory() as mp:
    62→        with patch("mirage_fuse.mount") as mock_fuse, \
    63→             patch("mirage.cli.commands.workspace._ensure_server"), \
    64→             patch("mirage.cli.commands.workspace._client"):
    65→            mount(["memory:/data/"],
    66→                  backend="memory",
    67→                  mountpoint=mp,
    68→                  **_fuse_defaults())
    69→            ws = mock_fuse.call_args.kwargs["fs"].ws
    70→            prefixes = [m.prefix for m in ws._registry._mounts]
    71→            assert "/" in prefixes
    72→            assert any(p.rstrip("/") == "/data" for p in prefixes)
    73→
    74→
    75→def test_mount_fuse_no_backend_no_spec_raises():
    76→    """mount --fuse with no --backend and no specs exits with error."""
    77→    result = _run(["mount", "--fuse", "--mountpoint", "/tmp/x"], check=False)
    78→    assert result.returncode != 0
    79→
    80→


with patch("mirage_fuse.mount") as mock_fuse, \

with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \


The file /Users/agent/strukto/mirage/tests/cli/test_app.py has been updated. All occurrences of '        with patch("mirage_fuse.mount") as mock_fuse, \' were successfully replaced with '        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \'.


fuse/mirage_fuse/__init__.py:1:from mirage_fuse.fs import MirageFS, mount, mount_background


mirage/fuse_ops/__init__.py:1:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/__init__.py:2:from mirage.fuse_ops.csv_op import CsvFuseOp
mirage/fuse_ops/__init__.py:3:from mirage.fuse_ops.default import DefaultFuseOp
mirage/fuse_ops/__init__.py:4:from mirage.fuse_ops.local import LocalFuseOp
mirage/fuse_ops/__init__.py:5:from mirage.fuse_ops.memory import MemoryFuseOp
mirage/fuse_ops/__init__.py:6:from mirage.fuse_ops.parquet import ParquetFuseOp
mirage/fuse_ops/__init__.py:7:from mirage.fuse_ops.s3 import S3FuseOp


     1→import os
     2→import tempfile
     3→
     4→import pytest
     5→
     6→from mirage.fuse.loader import (build_registry_from_config,
     7→                                load_fuse_op_class)
     8→
     9→
    10→def test_load_builtin_module():
    11→    cls = load_fuse_op_class("mirage.fuse.backend.default:DefaultFuseOp")
    12→    op = cls()
    13→    assert op.name == "default"
    14→
    15→
    16→def test_load_from_file():
    17→    code = ("from mirage.fuse import FuseOp\n"
    18→            "class CustomOp(FuseOp):\n"
    19→            "    name = 'custom_test'\n")
    20→    with tempfile.NamedTemporaryFile(suffix=".py", mode="w",
    21→                                     delete=False) as f:
    22→        f.write(code)
    23→        f.flush()
    24→        cls = load_fuse_op_class(f"{f.name}:CustomOp")
    25→    op = cls()
    26→    assert op.name == "custom_test"
    27→    os.unlink(f.name)
    28→
    29→
    30→def test_invalid_spec():
    31→    with pytest.raises(ValueError, match="invalid"):
    32→        load_fuse_op_class("no_colon_here")
    33→
    34→
    35→def test_build_registry_builtin():
    36→    registry = build_registry_from_config({"*.parquet": "default"})
    37→    op = registry.resolve("/data/file.parquet", 100)
    38→    assert op.name == "default"
    39→
    40→
    41→def test_build_registry_custom_class():
    42→    registry = build_registry_from_config({
    43→        "*.txt":
    44→        "mirage.fuse.backend.memory:MemoryFuseOp",
    45→    })
    46→    op = registry.resolve("/data/file.txt", 100)
    47→    assert op.name == "memory"
    48→
    49→
    50→def test_build_registry_dict_form():
    51→    registry = build_registry_from_config({
    52→        "*.bin": {
    53→            "handler": "s3"
    54→        },
    55→    })
    56→    op = registry.resolve("/data/file.bin", 100)
    57→    assert op.name == "s3"
    58→
    59→
    60→def test_build_registry_empty():
    61→    registry = build_registry_from_config({})
    62→    op = registry.resolve("/data/file.txt", 100)
    63→    assert op.name == "default"
    64→


     1→import os
     2→import sys
     3→import threading
     4→import time
     5→from pathlib import Path
     6→from typing import TYPE_CHECKING, Annotated
     7→
     8→import typer
     9→
    10→from mirage.backend.loader import load_backend_class
    11→from mirage.cli.client import ClientError
    12→from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
    13→                               parse_mount_uri_full, split_mount_spec)
    14→from mirage.cli.helpers import _client, _ensure_server
    15→from mirage.fuse.loader import build_registry_from_config
    16→from mirage.fuse.registry import FuseOpRegistry
    17→from mirage.observe.inspector import (workspace_status_from_ws,
    18→                                      workspace_transfers_from_ws)
    19→from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    20→from mirage.workspace import Workspace
    21→
    22→if TYPE_CHECKING:
    23→    from mirage.fuse.fs import MirageFS
    24→
    25→workspace_app = typer.Typer(no_args_is_help=True)
    26→
    27→REPORT_INTERVAL = 2
    28→
    29→
    30→def _report_loop(ws: Workspace, mountpoint: str, fs: "MirageFS") -> None:


can we similarly group the backends in fuse to object_storage etc?


The user wants to group the fuse backend ops similarly to how we grouped the backend module. Currently:

```
mirage/fuse/backend/
├── default.py    (DefaultFuseOp - generic)
├── local.py      (LocalFuseOp - local filesystem)
├── memory.py     (MemoryFuseOp - in-memory)
└── s3.py         (S3FuseOp - S3/R2)
```

In the backend module we have:
```
mirage/backend/
├── local/        (local.py, memory.py)
├── object_storage/ (s3.py, r2.py)

├── network/      (http.py, proxy.py)
```

So the user wants:
```
mirage/fuse/backend/
├── local/
│   ├── local.py
│   └── memory.py
├── object_storage/
│   └── s3.py
└── default.py    (stays at top since it's the generic fallback)
```

I'm noticing these are pretty small files with just one class each, so creating subdirectories for 1-2 files feels like it might be over-organizing. But the user explicitly asked for this structure. Also, there's no network equivalent in the fuse backend, so I'll just mirror the local and object_storage grouping.

The structure makes sense: `local/` groups local.py and memory.py, `object_storage/` contains s3.py, and `default.py` stays at the top level since it's the generic fallback that doesn't belong to any specific backend group. This mirrors the backend structure cleanly.


Bash

mkdir -p mirage/fuse/backend/local mirage/fuse/backend/object_storage && git mv mirage/fuse/backend/local.py mirage/fuse/backend/local/local.py && git mv mirage/fuse/backend/memory.py mirage/fuse/backend/local/memory.py && git mv mirage/fuse/backend/s3.py mirage/fuse/backend/object_storage/s3.py

Group fuse backend ops into local/ and object_storage/


File created successfully at: /Users/agent/strukto/mirage/mirage/fuse/backend/local/__init__.py


File created successfully at: /Users/agent/strukto/mirage/mirage/fuse/backend/object_storage/__init__.py


Now update the imports in `mirage/fuse/__init__.py` and any tests that reference these.


     1→from mirage.fuse.backend.default import DefaultFuseOp
     2→from mirage.fuse.backend.local import LocalFuseOp
     3→from mirage.fuse.backend.memory import MemoryFuseOp
     4→from mirage.fuse.backend.s3 import S3FuseOp
     5→from mirage.fuse.base import FuseOp
     6→from mirage.fuse.filetype.csv import CsvFuseOp
     7→from mirage.fuse.filetype.parquet import ParquetFuseOp
     8→
     9→FUSE_OPS: dict[str, FuseOp] = {
    10→    "default": DefaultFuseOp(),
    11→    "s3": S3FuseOp(),
    12→    "memory": MemoryFuseOp(),
    13→    "local": LocalFuseOp(),
    14→    "parquet": ParquetFuseOp(),
    15→    "csv": CsvFuseOp(),
    16→}
    17→
    18→__all__ = [
    19→    "FuseOp",
    20→    "CsvFuseOp",
    21→    "DefaultFuseOp",
    22→    "LocalFuseOp",
    23→    "MemoryFuseOp",
    24→    "ParquetFuseOp",
    25→    "S3FuseOp",
    26→    "FUSE_OPS",
    27→]
    28→

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.local import LocalFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp
from mirage.fuse.backend.s3 import S3FuseOp
from mirage.fuse.base import FuseOp
from mirage.fuse.filetype.csv import CsvFuseOp
from mirage.fuse.filetype.parquet import ParquetFuseOp

FUSE_OPS: dict[str, FuseOp] = {
    "default": DefaultFuseOp(),
    "s3": S3FuseOp(),
    "memory": MemoryFuseOp(),
    "local": LocalFuseOp(),
    "parquet": ParquetFuseOp(),
    "csv": CsvFuseOp(),
}

__all__ = [
    "FuseOp",
    "CsvFuseOp",
    "DefaultFuseOp",
    "LocalFuseOp",
    "MemoryFuseOp",
    "ParquetFuseOp",
    "S3FuseOp",
    "FUSE_OPS",
]


Edit

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.local import LocalFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp
from mirage.fuse.backend.s3 import S3FuseOp

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.local.local import LocalFuseOp
from mirage.fuse.backend.local.memory import MemoryFuseOp
from mirage.fuse.backend.object_storage.s3 import S3FuseOp


The file /Users/agent/strukto/mirage/mirage/fuse/__init__.py has been updated successfully.

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.local.local import LocalFuseOp
from mirage.fuse.backend.local.memory import MemoryFuseOp
from mirage.fuse.backend.object_storage.s3 import S3FuseOp

from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.local import LocalFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp
from mirage.fuse.backend.s3 import S3FuseOp
from mirage.fuse.base import FuseOp
from mirage.fuse.filetype.csv import CsvFuseOp
from mirage.fuse.filetype.parquet import ParquetFuseOp

FUSE_OPS: dict[str, FuseOp] = {
    "default": DefaultFuseOp(),
    "s3": S3FuseOp(),
    "memory": MemoryFuseOp(),
    "local": LocalFuseOp(),
    "parquet": ParquetFuseOp(),
    "csv": CsvFuseOp(),
}

__all__ = [
    "FuseOp",
    "CsvFuseOp",
    "DefaultFuseOp",
    "LocalFuseOp",
    "MemoryFuseOp",
    "ParquetFuseOp",
    "S3FuseOp",
    "FUSE_OPS",
]

-from mirage.fuse.backend.local import LocalFuseOp

-from mirage.fuse.backend.memory import MemoryFuseOp

-from mirage.fuse.backend.s3 import S3FuseOp

+from mirage.fuse.backend.local.local import LocalFuseOp

+from mirage.fuse.backend.local.memory import MemoryFuseOp

+from mirage.fuse.backend.object_storage.s3 import S3FuseOp


Now update the tests that reference these paths directly.


tests/fuse/test_default.py:3:from mirage.fuse.backend.memory import MemoryFuseOp


     1→from mirage.backend.local.memory import MemoryBackend
     2→from mirage.fuse.backend.default import DefaultFuseOp
     3→from mirage.fuse.backend.memory import MemoryFuseOp
     4→from mirage.types import MountMode
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def test_default_eager_small_file():
     9→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    10→    ws.write("/f.txt", b"hello world")
    11→    op = DefaultFuseOp()
    12→    ctx = op.on_open(ws, "/f.txt", 11)
    13→    assert ctx["eager"] is True
    14→    data = op.read(ws, "/f.txt", 0, 5, ctx)
    15→    assert data == b"hello"
    16→
    17→
    18→def test_default_lazy_large_file():
    19→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    20→    big = b"x" * (2 * 1024 * 1024)
    21→    ws.write("/big.bin", big)
    22→    op = DefaultFuseOp()
    23→    ctx = op.on_open(ws, "/big.bin", len(big))
    24→    assert ctx["eager"] is False
    25→    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    26→    assert chunk == b"x" * 100
    27→
    28→
    29→def test_memory_always_eager():
    30→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    31→    big = b"y" * (2 * 1024 * 1024)
    32→    ws.write("/big.bin", big)
    33→    op = MemoryFuseOp()
    34→    ctx = op.on_open(ws, "/big.bin", len(big))
    35→    assert ctx["eager"] is True
    36→
    37→
    38→def test_buffered_write_flush():
    39→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    40→    ws.write("/f.txt", b"hello world")
    41→    op = DefaultFuseOp()
    42→    ctx = op.on_open(ws, "/f.txt", 11)
    43→    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    44→    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    45→    op.on_flush(ws, "/f.txt", ctx)
    46→    assert ws.cat("/f.txt") == b"HELLO WORLD"
    47→
    48→
    49→def test_buffered_write_new_file():
    50→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    51→    op = DefaultFuseOp()
    52→    ctx = op.on_open(ws, "/new.txt", 0)
    53→    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    54→    op.on_flush(ws, "/new.txt", ctx)
    55→    assert ws.cat("/new.txt") == b"brand new"
    56→
    57→
    58→def test_flush_noop_without_writes():
    59→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    60→    ws.write("/f.txt", b"original")
    61→    op = DefaultFuseOp()
    62→    ctx = op.on_open(ws, "/f.txt", 8)
    63→    op.on_flush(ws, "/f.txt", ctx)
    64→    assert ws.cat("/f.txt") == b"original"
    65→

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_default_eager_small_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    assert ctx["eager"] is True
    data = op.read(ws, "/f.txt", 0, 5, ctx)
    assert data == b"hello"

def test_default_lazy_large_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"x" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is False
    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    assert chunk == b"x" * 100

def test_memory_always_eager():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"y" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = MemoryFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is True

def test_buffered_write_flush():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"HELLO WORLD"

def test_buffered_write_new_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/new.txt", 0)
    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    op.on_flush(ws, "/new.txt", ctx)
    assert ws.cat("/new.txt") == b"brand new"

def test_flush_noop_without_writes():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"original")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 8)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"original"


from mirage.fuse.backend.memory import MemoryFuseOp

from mirage.fuse.backend.local.memory import MemoryFuseOp


The file /Users/agent/strukto/mirage/tests/fuse/test_default.py has been updated successfully.

from mirage.fuse.backend.local.memory import MemoryFuseOp

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.default import DefaultFuseOp
from mirage.fuse.backend.memory import MemoryFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_default_eager_small_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    assert ctx["eager"] is True
    data = op.read(ws, "/f.txt", 0, 5, ctx)
    assert data == b"hello"

def test_default_lazy_large_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"x" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is False
    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    assert chunk == b"x" * 100

def test_memory_always_eager():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"y" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = MemoryFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is True

def test_buffered_write_flush():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"HELLO WORLD"

def test_buffered_write_new_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/new.txt", 0)
    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    op.on_flush(ws, "/new.txt", ctx)
    assert ws.cat("/new.txt") == b"brand new"

def test_flush_noop_without_writes():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"original")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 8)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"original"


Also check the test_loader.py for any string literals referencing the old paths.


tests/fuse/test_default.py:3:from mirage.fuse.backend.local.memory import MemoryFuseOp
tests/fuse/test_loader.py:43:        "mirage.fuse.backend.memory:MemoryFuseOp",
mirage/fuse/__init__.py:2:from mirage.fuse.backend.local.local import LocalFuseOp
mirage/fuse/__init__.py:3:from mirage.fuse.backend.local.memory import MemoryFuseOp


    38→
    39→
    40→def test_build_registry_custom_class():
    41→    registry = build_registry_from_config({
    42→        "*.txt":
    43→        "mirage.fuse.backend.memory:MemoryFuseOp",
    44→    })
    45→    op = registry.resolve("/data/file.txt", 100)
    46→    assert op.name == "memory"
    47→
    48→
    49→def test_build_registry_dict_form():
    50→    registry = build_registry_from_config({
    51→        "*.bin": {
    52→            "handler": "s3"

def test_build_registry_custom_class():
    registry = build_registry_from_config({
        "*.txt":
        "mirage.fuse.backend.memory:MemoryFuseOp",
    })
    op = registry.resolve("/data/file.txt", 100)
    assert op.name == "memory"

def test_build_registry_dict_form():
    registry = build_registry_from_config({
        "*.bin": {
            "handler": "s3"


The file /Users/agent/strukto/mirage/tests/fuse/test_loader.py has been updated successfully.

import os
import tempfile

import pytest

from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class

def test_load_builtin_module():
    cls = load_fuse_op_class("mirage.fuse.backend.default:DefaultFuseOp")
    op = cls()
    assert op.name == "default"

def test_load_from_file():
    code = ("from mirage.fuse import FuseOp\n"
            "class CustomOp(FuseOp):\n"
            "    name = 'custom_test'\n")
    with tempfile.NamedTemporaryFile(suffix=".py", mode="w",
                                     delete=False) as f:
        f.write(code)
        f.flush()
        cls = load_fuse_op_class(f"{f.name}:CustomOp")
    op = cls()
    assert op.name == "custom_test"
    os.unlink(f.name)

def test_invalid_spec():
    with pytest.raises(ValueError, match="invalid"):
        load_fuse_op_class("no_colon_here")

def test_build_registry_builtin():
    registry = build_registry_from_config({"*.parquet": "default"})
    op = registry.resolve("/data/file.parquet", 100)
    assert op.name == "default"

def test_build_registry_custom_class():
    registry = build_registry_from_config({
        "*.txt":
        "mirage.fuse.backend.memory:MemoryFuseOp",
    })
    op = registry.resolve("/data/file.txt", 100)
    assert op.name == "memory"

def test_build_registry_dict_form():
    registry = build_registry_from_config({
        "*.bin": {
            "handler": "s3"
        },
    })
    op = registry.resolve("/data/file.bin", 100)
    assert op.name == "s3"

def test_build_registry_empty():
    registry = build_registry_from_config({})
    op = registry.resolve("/data/file.txt", 100)
    assert op.name == "default"

-        "mirage.fuse.backend.memory:MemoryFuseOp",

+        "mirage.fuse.backend.local.memory:MemoryFuseOp",


Bash

Run full test suite


-------------------------------------------------------------------------------------
TOTAL                                                    4704   1193    75%
641 passed, 1 skipped, 2 warnings in 2.75s


641 passed. Final structure:

```
mirage/fuse/backend/
├── __init__.py
├── default.py                ← generic fallback
├── local/
│   ├── local.py              (LocalFuseOp)
│   └── memory.py             (MemoryFuseOp)
└── object_storage/
    └── s3.py                 (S3FuseOp)
```

Mirrors the `mirage/backend/` grouping.


Ok
let's refactor the tests by

1. no class just have def test_xxx
2. each source file should have exactly one test file for example backend/abc/def.py should have tests/backend/abc/test_def.py


Agent

Explore current test structure

Prompt:
Thoroughly explore the tests/ directory structure. For each test file:
1. List the full path
2. Note if it uses classes (class TestXxx) or plain functions (def test_xxx)
3. Note what source file(s) it tests (based on imports and test content)

Also list the full mirage/ source directory structure (all .py files) so we can map source files to test files.

Use `find tests -name '*.py' -type f` and `find mirage -name '*.py' -type f` style exploration. Read each test file to check for class usage.

Be very thorough - I need a complete mapping.