     1→import errno
     2→import os
     3→import stat
     4→import subprocess
     5→import sys
     6→import threading
     7→import time
     8→import uuid
     9→from collections import deque
    10→from datetime import datetime, timezone
    11→
    12→import fuse
    13→from mirage.fuse_ops.base import FuseOp
    14→from mirage.fuse_ops.registry import FuseOpRegistry
    15→from mirage.workspace import Workspace
    16→
    17→_ENV_AGENT_ID = "MIRAGE_AGENT_ID"
    18→_MIRAGE_DIR = "/.mirage"
    19→_MIRAGE_WHOAMI = "/.mirage/whoami"
    20→
    21→_MAX_OPS = 200
    22→
    23→
    24→class MirageFS(fuse.Operations):
    25→
    26→    def __init__(self,
    27→                 ws: Workspace,
    28→                 agent_id: str | None = None,
    29→                 fuse_registry: FuseOpRegistry | None = None) -> None:
    30→        self.ws = ws
    31→        self.agent_id = (agent_id or os.environ.get(_ENV_AGENT_ID)
    32→                         or f"agent-{uuid.uuid4().hex[:8]}")
    33→        self._now = time.time()
    34→        self._prefixes = [m.prefix for m in ws._registry._mounts]
    35→        self._ops: deque[dict] = deque(maxlen=_MAX_OPS)
    36→        self._ops_lock = threading.Lock()
    37→        self._read_seen: set[str] = set()
    38→        self._fuse_registry = fuse_registry or FuseOpRegistry()
    39→        self._handles: dict[int, dict] = {}
    40→        self._handle_ops: dict[int, FuseOp] = {}
    41→        self._next_fh = 1
    42→
    43→    def _whoami_content(self) -> bytes:
    44→        mounts = [m.prefix for m in self.ws._registry._mounts]
    45→        lines = [
    46→            f"agent: {self.agent_id}",
    47→            "cwd: /",
    48→            f"mounts: {', '.join(mounts)}",
    49→        ]
    50→        return ("\n".join(lines) + "\n").encode()
    51→
    52→    def _dir_stat(self) -> dict:
    53→        return {
    54→            "st_mode": stat.S_IFDIR | 0o755,
    55→            "st_nlink": 2,
    56→            "st_uid": os.getuid(),
    57→            "st_gid": os.getgid(),
    58→            "st_size": 0,
    59→            "st_atime": self._now,
    60→            "st_mtime": self._now,
    61→            "st_ctime": self._now,
    62→        }
    63→
    64→    def _file_stat(self, size: int) -> dict:
    65→        return {
    66→            "st_mode": stat.S_IFREG | 0o644,
    67→            "st_nlink": 1,
    68→            "st_uid": os.getuid(),
    69→            "st_gid": os.getgid(),
    70→            "st_size": size,
    71→            "st_atime": self._now,
    72→            "st_mtime": self._now,
    73→            "st_ctime": self._now,
    74→        }
    75→
    76→    def _is_virtual_dir(self, path: str) -> bool:
    77→        normalized = path.rstrip("/") + "/"
    78→        for p in self._prefixes:
    79→            if p.startswith(normalized) or p.rstrip("/") == path.rstrip("/"):
    80→                return True
    81→        return False
    82→
    83→    def getattr(self, path: str, fh=None) -> dict:
    84→        if path == "/":
    85→            return self._dir_stat()
    86→        if path == _MIRAGE_DIR:
    87→            return self._dir_stat()
    88→        if path == _MIRAGE_WHOAMI:
    89→            return self._file_stat(len(self._whoami_content()))
    90→        if self._is_virtual_dir(path):
    91→            return self._dir_stat()
    92→        try:
    93→            s = self.ws.stat(path)
    94→            return self._file_stat(s.size or 0)
    95→        except (FileNotFoundError, ValueError):
    96→            pass
    97→        try:
    98→            self.ws.ls(path)
    99→            return self._dir_stat()
   100→        except (FileNotFoundError, ValueError):
   101→            raise fuse.FuseOSError(errno.ENOENT)
   102→
   103→    def _virtual_children(self, path: str) -> list[str]:
   104→        normalized = path.rstrip("/") + "/" if path != "/" else "/"
   105→        children = set()
   106→        for p in self._prefixes:
   107→            if p.startswith(normalized) and p != normalized:
   108→                rest = p[len(normalized):]
   109→                child = rest.split("/")[0]
   110→                if child:
   111→                    children.add(child)
   112→        return sorted(children)
   113→
   114→    def _clean_path(self, path: str) -> str:
   115→        basename = os.path.basename(path)
   116→        if basename.startswith("._"):
   117→            basename = basename[2:]
   118→            return os.path.join(os.path.dirname(path), basename)
   119→        return path
   120→
   121→    def _record_op(self, op: str, path: str, raw: str) -> None:
   122→        path = self._clean_path(path)
   123→        with self._ops_lock:
   124→            self._ops.append({
   125→                "type":
   126→                "fuse_op",
   127→                "op":
   128→                op,
   129→                "path":
   130→                path,
   131→                "timestamp":
   132→                datetime.now(timezone.utc).isoformat(),
   133→                "raw":
   134→                raw,
   135→            })
   136→
   137→    def drain_ops(self) -> list[dict]:
   138→        with self._ops_lock:
   139→            ops = list(self._ops)
   140→            self._ops.clear()
   141→            self._read_seen.clear()
   142→            return ops
   143→
   144→    def readdir(self, path: str, fh) -> list:
   145→        if path == _MIRAGE_DIR:
   146→            return [".", "..", "whoami"]
   147→        names = set(self._virtual_children(path))
   148→        if path == "/":
   149→            names.add(".mirage")
   150→        try:
   151→            entries = self.ws.ls(path)
   152→            for e in entries:
   153→                part = e.rstrip("/").rsplit("/", 1)[-1]
   154→                if part:
   155→                    names.add(part)
   156→        except (FileNotFoundError, ValueError):
   157→            if not names:
   158→                raise fuse.FuseOSError(errno.ENOENT)
   159→        return [".", ".."] + sorted(names)
   160→
   161→    def _record_read(self, path: str) -> None:
   162→        clean = self._clean_path(path)
   163→        with self._ops_lock:
   164→            if clean not in self._read_seen:
   165→                self._read_seen.add(clean)
   166→                self._ops.append({
   167→                    "type":
   168→                    "fuse_op",
   169→                    "op":
   170→                    "read",
   171→                    "path":
   172→                    clean,
   173→                    "timestamp":
   174→                    datetime.now(timezone.utc).isoformat(),
   175→                    "raw":
   176→                    f"read {clean}",
   177→                })
   178→
   179→    def read(self, path: str, size: int, offset: int, fh) -> bytes:
   180→        if path == _MIRAGE_WHOAMI:
   181→            data = self._whoami_content()
   182→            return data[offset:offset + size]
   183→        op = self._handle_ops.get(fh)
   184→        ctx = self._handles.get(fh)
   185→        if op is None or ctx is None:
   186→            try:
   187→                data = self.ws.cat(path)
   188→                self._record_read(path)
   189→                return data[offset:offset + size]
   190→            except (FileNotFoundError, ValueError):
   191→                raise fuse.FuseOSError(errno.ENOENT)
   192→        try:
   193→            data = op.read(self.ws, path, offset, size, ctx)
   194→            self._record_read(path)
   195→            return data
   196→        except (FileNotFoundError, ValueError):
   197→            raise fuse.FuseOSError(errno.ENOENT)
   198→
   199→    def write(self, path: str, data: bytes, offset: int, fh) -> int:
   200→        op = self._handle_ops.get(fh)
   201→        ctx = self._handles.get(fh)
   202→        if op is not None and ctx is not None:
   203→            try:
   204→                result = op.write(self.ws, path, offset, data, ctx)
   205→                self._record_op("write", path, f"write {path}")
   206→                return result
   207→            except PermissionError:
   208→                raise fuse.FuseOSError(errno.EACCES)
   209→        try:
   210→            existing = b""
   211→            try:
   212→                existing = self.ws.cat(path)
   213→            except FileNotFoundError:
   214→                pass
   215→            if offset > len(existing):
   216→                existing = existing + b"\0" * (offset - len(existing))
   217→            new_data = existing[:offset] + data + existing[offset + len(data):]
   218→            self.ws.write(path, new_data)
   219→            self._record_op("write", path, f"write {path}")
   220→            return len(data)
   221→        except PermissionError:
   222→            raise fuse.FuseOSError(errno.EACCES)
   223→        except ValueError:
   224→            raise fuse.FuseOSError(errno.ENOENT)
   225→
   226→    def create(self, path: str, mode, fi=None) -> int:
   227→        try:
   228→            self.ws.write(path, b"")
   229→            self._record_op("create", path, f"create {path}")
   230→            op = self._fuse_registry.resolve(path, 0, self.ws)
   231→            ctx = op.on_open(self.ws, path, 0)
   232→            ctx["path"] = path
   233→            fh = self._next_fh
   234→            self._next_fh += 1
   235→            self._handles[fh] = ctx
   236→            self._handle_ops[fh] = op
   237→            return fh
   238→        except PermissionError:
   239→            raise fuse.FuseOSError(errno.EACCES)
   240→        except ValueError:
   241→            raise fuse.FuseOSError(errno.ENOENT)
   242→
   243→    def mkdir(self, path: str, mode) -> None:
   244→        try:
   245→            self.ws.mkdir(path)
   246→            self._record_op("mkdir", path, f"mkdir {path}")
   247→        except PermissionError:
   248→            raise fuse.FuseOSError(errno.EACCES)
   249→        except ValueError:
   250→            raise fuse.FuseOSError(errno.ENOENT)
   251→
   252→    def unlink(self, path: str) -> None:
   253→        try:
   254→            self.ws.rm(path)
   255→            self._record_op("rm", path, f"rm {path}")
   256→        except PermissionError:
   257→            raise fuse.FuseOSError(errno.EACCES)
   258→        except FileNotFoundError:
   259→            raise fuse.FuseOSError(errno.ENOENT)
   260→
   261→    def rename(self, old: str, new: str, flags: int = 0) -> None:
   262→        try:
   263→            self.ws.mv(old, new)
   264→            self._record_op("mv", old, f"mv {old} → {new}")
   265→        except PermissionError:
   266→            raise fuse.FuseOSError(errno.EACCES)
   267→        except (FileNotFoundError, ValueError):
   268→            raise fuse.FuseOSError(errno.ENOENT)
   269→
   270→    def rmdir(self, path: str) -> None:
   271→        try:
   272→            entries = self.ws.ls(path)
   273→            if entries:
   274→                raise fuse.FuseOSError(errno.ENOTEMPTY)
   275→            self._record_op("rmdir", path, f"rmdir {path}")
   276→        except (FileNotFoundError, ValueError):
   277→            raise fuse.FuseOSError(errno.ENOENT)
   278→
   279→    def statfs(self, path: str) -> dict:
   280→        return {
   281→            "f_bsize": 4096,
   282→            "f_frsize": 4096,
   283→            "f_blocks": 1024 * 1024,
   284→            "f_bfree": 1024 * 1024,
   285→            "f_bavail": 1024 * 1024,
   286→            "f_files": 1000000,
   287→            "f_ffree": 1000000,
   288→            "f_favail": 1000000,
   289→            "f_namemax": 255,
   290→        }
   291→
   292→    def chmod(self, path: str, mode) -> None:
   293→        self.getattr(path)
   294→
   295→    def chown(self, path: str, uid: int, gid: int) -> None:
   296→        self.getattr(path)
   297→
   298→    def utimens(self, path: str, times=None) -> None:
   299→        self.getattr(path)
   300→
   301→    def access(self, path: str, amode: int) -> None:
   302→        self.getattr(path)
   303→
   304→    def flush(self, path: str, fh) -> None:
   305→        op = self._handle_ops.get(fh)
   306→        ctx = self._handles.get(fh)
   307→        if op is not None and ctx is not None:
   308→            try:
   309→                op.on_flush(self.ws, path, ctx)
   310→            except PermissionError:
   311→                raise fuse.FuseOSError(errno.EACCES)
   312→            return
   313→        self.ws.sync(path)
   314→
   315→    def fsync(self, path: str, datasync: int, fh) -> None:
   316→        self.flush(path, fh)
   317→
   318→    def open(self, path: str, flags) -> int:
   319→        if path == _MIRAGE_WHOAMI:
   320→            fh = self._next_fh
   321→            self._next_fh += 1
   322→            self._handles[fh] = {"path": path}
   323→            return fh
   324→        try:
   325→            s = self.ws.stat(path)
   326→            size = s.size or 0
   327→        except (FileNotFoundError, ValueError):
   328→            raise fuse.FuseOSError(errno.ENOENT)
   329→        op = self._fuse_registry.resolve(path, size, self.ws)
   330→        ctx = op.on_open(self.ws, path, size)
   331→        ctx["path"] = path
   332→        fh = self._next_fh
   333→        self._next_fh += 1
   334→        self._handles[fh] = ctx
   335→        self._handle_ops[fh] = op
   336→        return fh
   337→
   338→    def release(self, path: str, fh) -> int:
   339→        self._handles.pop(fh, None)
   340→        self._handle_ops.pop(fh, None)
   341→        return 0
   342→
   343→    def truncate(self, path: str, length: int, fh=None) -> None:
   344→        try:
   345→            data = b""
   346→            try:
   347→                data = self.ws.cat(path)
   348→            except FileNotFoundError:
   349→                pass
   350→            padded = data[:length].ljust(length, b"\0")
   351→            self.ws.write(path, padded)
   352→        except PermissionError:
   353→            raise fuse.FuseOSError(errno.EACCES)
   354→        except ValueError:
   355→            raise fuse.FuseOSError(errno.ENOENT)
   356→
   357→
   358→def _run_fuse(fs: MirageFS, mountpoint: str, foreground: bool) -> None:
   359→    fuse.FUSE(fs, mountpoint, nothreads=True, foreground=foreground)
   360→
   361→
   362→def mount_background(ws: Workspace,
   363→                     mountpoint: str,
   364→                     agent_id: str | None = None) -> threading.Thread:
   365→    """Start FUSE in a daemon thread and return immediately."""
   366→    fs = MirageFS(ws, agent_id=agent_id)
   367→    t = threading.Thread(target=_run_fuse,
   368→                         args=(fs, mountpoint, True),
   369→                         daemon=True)
   370→    t.start()
   371→    time.sleep(0.3)
   372→    return t
   373→
   374→
   375→def mount(ws: Workspace | None = None,
   376→          mountpoint: str = "",
   377→          foreground: bool = True,
   378→          agent_id: str | None = None,
   379→          fs: MirageFS | None = None) -> None:
   380→    if fs is None:
   381→        fs = MirageFS(ws, agent_id=agent_id)
   382→    t = threading.Thread(
   383→        target=_run_fuse,
   384→        args=(fs, mountpoint, foreground),
   385→        daemon=True,
   386→    )
   387→    t.start()
   388→    try:
   389→        while t.is_alive():
   390→            t.join(timeout=0.5)
   391→    except KeyboardInterrupt:
   392→        print("\nUnmounting...", flush=True)
   393→        if sys.platform == "darwin":
   394→            subprocess.run(
   395→                ["diskutil", "unmount", "force", mountpoint],
   396→                capture_output=True,
   397→            )
   398→        else:
   399→            subprocess.run(["fusermount", "-u", mountpoint],
   400→                           capture_output=True)
   401→        t.join(timeout=5)
   402→


     1→from abc import ABC, abstractmethod
     2→
     3→from mirage.types import FileStat
     4→
     5→
     6→class MirageService(ABC):
     7→
     8→    @abstractmethod
     9→    def create_workspace(self, config: dict) -> str:
    10→        ...
    11→
    12→    @abstractmethod
    13→    def delete_workspace(self, ws_id: str) -> None:
    14→        ...
    15→
    16→    @abstractmethod
    17→    def list_workspaces(self) -> list[dict]:
    18→        ...
    19→
    20→    @abstractmethod
    21→    def exec(
    22→        self,
    23→        ws_id: str,
    24→        command: str,
    25→        cwd: str | None = None,
    26→        stdin: bytes | None = None,
    27→        input_files: dict[str, tuple[str, bytes | None]] | None = None,
    28→    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
    29→        ...
    30→
    31→    @abstractmethod
    32→    def list_commands(self, ws_id: str) -> list[str]:
    33→        ...
    34→
    35→    @abstractmethod
    36→    def register_command(self,
    37→                         ws_id: str,
    38→                         name: str,
    39→                         exec_str: str,
    40→                         description: str = "") -> None:
    41→        ...
    42→
    43→    @abstractmethod
    44→    def sync(self, ws_id: str) -> list[dict]:
    45→        ...
    46→
    47→    @abstractmethod
    48→    def status(self, ws_id: str) -> list[dict]:
    49→        ...
    50→
    51→    @abstractmethod
    52→    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    53→        ...
    54→
    55→    @abstractmethod
    56→    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
    57→        ...
    58→
    59→    @abstractmethod
    60→    def io_stat(self, ws_id: str, path: str) -> FileStat:
    61→        ...
    62→
    63→    @abstractmethod
    64→    def io_readdir(self,
    65→                   ws_id: str,
    66→                   path: str,
    67→                   cursor: str | None = None) -> tuple[list[str], str | None]:
    68→        ...
    69→
    70→    @abstractmethod
    71→    def io_create(self, ws_id: str, path: str) -> None:
    72→        ...
    73→
    74→    @abstractmethod
    75→    def io_unlink(self, ws_id: str, path: str) -> None:
    76→        ...
    77→
    78→    @abstractmethod
    79→    def io_mkdir(self, ws_id: str, path: str) -> None:
    80→        ...
    81→
    82→    @abstractmethod
    83→    def io_rmdir(self, ws_id: str, path: str) -> None:
    84→        ...
    85→
    86→    @abstractmethod
    87→    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
    88→        ...
    89→
    90→    @abstractmethod
    91→    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
    92→        ...
    93→
    94→    @abstractmethod
    95→    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
    96→        ...
    97→
    98→    @abstractmethod
    99→    def list_fuse_ops(self, ws_id: str) -> list[dict]:
   100→        ...
   101→
   102→    @abstractmethod
   103→    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
   104→        ...
   105→


     1→import base64
     2→import functools
     3→import hashlib
     4→
     5→from fastapi import APIRouter, HTTPException, Request
     6→
     7→from mirage.server import routes
     8→from mirage.server.fastapi.config import (ExecRequest, RegisterCommandRequest,
     9→                                          RegisterFuseOpRequest)
    10→
    11→command_router = APIRouter(tags=["command"])
    12→
    13→
    14→def _get_service(request: Request):
    15→    return request.app.state.service
    16→
    17→
    18→def _handle_service_errors(fn):
    19→
    20→    @functools.wraps(fn)
    21→    def wrapper(*args, **kwargs):
    22→        try:
    23→            return fn(*args, **kwargs)
    24→        except KeyError:
    25→            raise HTTPException(status_code=404, detail="workspace not found")
    26→        except ValueError as e:
    27→            raise HTTPException(status_code=400, detail=str(e))
    28→
    29→    return wrapper
    30→
    31→
    32→@command_router.post(routes.WORKSPACE_EXEC)
    33→def exec_command(ws_id: str, req: ExecRequest, request: Request) -> dict:
    34→    svc = _get_service(request)
    35→    try:
    36→        stdin_bytes = base64.b64decode(req.stdin) if req.stdin else None
    37→        input_files = None
    38→        if req.input_files:
    39→            input_files = {}
    40→            for path, payload in req.input_files.items():
    41→                data = (base64.b64decode(payload.data)
    42→                        if payload.data else None)
    43→                input_files[path] = (payload.checksum, data)
    44→
    45→        ok, result, out_files, cache_status = svc.exec(
    46→            ws_id,
    47→            req.command,
    48→            cwd=req.cwd,
    49→            stdin=stdin_bytes,
    50→            input_files=input_files,
    51→        )
    52→    except KeyError:
    53→        raise HTTPException(status_code=404, detail="workspace not found")
    54→    except ValueError as e:
    55→        raise HTTPException(status_code=400, detail=str(e))
    56→    if ok:
    57→        result_dict: dict = {"ok": True, "output": result}
    58→        if out_files:
    59→            result_dict["output_files"] = {
    60→                path: {
    61→                    "checksum": hashlib.md5(data).hexdigest(),
    62→                    "data": base64.b64encode(data).decode(),
    63→                }
    64→                for path, data in out_files.items()
    65→            }
    66→        if cache_status:
    67→            result_dict["cache_status"] = cache_status
    68→        return result_dict
    69→    return {"ok": False, "error": result}
    70→
    71→
    72→@command_router.get(routes.WORKSPACE_COMMANDS)
    73→@_handle_service_errors
    74→def list_commands(ws_id: str, request: Request) -> list[str]:
    75→    svc = _get_service(request)
    76→    return svc.list_commands(ws_id)
    77→
    78→
    79→@command_router.post(routes.WORKSPACE_COMMANDS)
    80→@_handle_service_errors
    81→def register_command(ws_id: str, req: RegisterCommandRequest,
    82→                     request: Request) -> dict:
    83→    svc = _get_service(request)
    84→    svc.register_command(ws_id, req.name, req.exec, req.description)
    85→    return {"ok": True}
    86→
    87→
    88→@command_router.get(routes.WORKSPACE_FUSE_OPS)
    89→@_handle_service_errors
    90→def list_fuse_ops(ws_id: str, request: Request) -> list[dict]:
    91→    svc = _get_service(request)
    92→    return svc.list_fuse_ops(ws_id)
    93→
    94→
    95→@command_router.post(routes.WORKSPACE_FUSE_OPS)
    96→@_handle_service_errors
    97→def register_fuse_op(ws_id: str, req: RegisterFuseOpRequest,
    98→                     request: Request) -> dict:
    99→    svc = _get_service(request)
   100→    svc.register_fuse_op(ws_id, req.pattern, req.handler)
   101→    return {"ok": True}
   102→


format of file type?


Right — I'd rename `format/` to `filetype/` since we already have the `FileType` enum and `filetype.py` module. Keeps the naming consistent:

```
fuse_ops/
├── base.py
├── backend/          ← read/write strategy per storage type
│   ├── default.py
│   ├── local.py
│   ├── memory.py
│   └── s3.py
├── filetype/         ← virtual rendering per FileType
│   ├── csv.py
│   └── parquet.py
├── remote/
│   └── ...
├── registry.py
└── loader.py
```

Does this naming work, or do you prefer something else?


also is fuse ops a good folder name or just fuse?


`fuse_ops` is more descriptive — these are FUSE operation handlers, not the FUSE filesystem itself (which lives in `fuse/mirage_fuse/fs.py`). Renaming to just `fuse` could create confusion with the top-level `fuse/` directory.

I'd keep `fuse_ops`. Want me to proceed with the design and implement?


let's rename to fuse and let's also incorporate the top-level fuse to this subfolder


The user wants to:
1. Rename `mirage/fuse_ops/` to `mirage/fuse/`
2. Move the top-level `fuse/` directory contents into `mirage/fuse/` as well
3. Reorganize with the `backend/`, `filetype/`, `remote/` subfolders

Let me first understand the top-level `fuse/` directory structure.


Let me check what's in the top-level `fuse/` directory first.


Bash

List all files in top-level fuse/ directory


fuse/mirage_fuse.egg-info/PKG-INFO
fuse/mirage_fuse.egg-info/SOURCES.txt
fuse/mirage_fuse.egg-info/requires.txt
fuse/mirage_fuse.egg-info/top_level.txt
fuse/mirage_fuse.egg-info/dependency_links.txt
fuse/.pytest_cache/CACHEDIR.TAG
fuse/.pytest_cache/README.md
fuse/.pytest_cache/.gitignore
fuse/.pytest_cache/v/cache/nodeids
fuse/.pytest_cache/v/cache/lastfailed
fuse/.coverage
fuse/pyproject.toml
fuse/tests/__pycache__/test_fs.cpython-312-pytest-9.0.2.pyc
fuse/tests/__pycache__/__init__.cpython-312.pyc
fuse/tests/test_fs.py
fuse/README.md
fuse/mirage_fuse/__init__.py
fuse/mirage_fuse/__pycache__/fs.cpython-312.pyc
fuse/mirage_fuse/__pycache__/__init__.cpython-312.pyc
fuse/mirage_fuse/fs.py


import errno
import os
import stat
import subprocess
import sys
import threading
import time
import uuid
from collections import deque
from datetime import datetime, timezone

import fuse
from mirage.fuse_ops.base import FuseOp
from mirage.fuse_ops.registry import FuseOpRegistry
from mirage.workspace import Workspace

_ENV_AGENT_ID = "MIRAGE_AGENT_ID"
_MIRAGE_DIR = "/.mirage"
_MIRAGE_WHOAMI = "/.mirage/whoami"

_MAX_OPS = 200

class MirageFS(fuse.Operations):

    def __init__(self,
                 ws: Workspace,
                 agent_id: str | None = None,
                 fuse_registry: FuseOpRegistry | None = None) -> None:
        self.ws = ws
        self.agent_id = (agent_id or os.environ.get(_ENV_AGENT_ID)
                         or f"agent-{uuid.uuid4().hex[:8]}")
        self._now = time.time()
        self._prefixes = [m.prefix for m in ws._registry._mounts]
        self._ops: deque[dict] = deque(maxlen=_MAX_OPS)
        self._ops_lock = threading.Lock()
        self._read_seen: set[str] = set()
        self._fuse_registry = fuse_registry or FuseOpRegistry()
        self._handles: dict[int, dict] = {}
        self._handle_ops: dict[int, FuseOp] = {}
        self._next_fh = 1

    def _whoami_content(self) -> bytes:
        mounts = [m.prefix for m in self.ws._registry._mounts]
        lines = [
            f"agent: {self.agent_id}",
            "cwd: /",
            f"mounts: {', '.join(mounts)}",
        ]
        return ("\n".join(lines) + "\n").encode()

    def _dir_stat(self) -> dict:
        return {
            "st_mode": stat.S_IFDIR | 0o755,
            "st_nlink": 2,
            "st_uid": os.getuid(),
            "st_gid": os.getgid(),
            "st_size": 0,
            "st_atime": self._now,
            "st_mtime": self._now,
            "st_ctime": self._now,
        }

    def _file_stat(self, size: int) -> dict:
        return {
            "st_mode": stat.S_IFREG | 0o644,
            "st_nlink": 1,
            "st_uid": os.getuid(),
            "st_gid": os.getgid(),
            "st_size": size,
            "st_atime": self._now,
            "st_mtime": self._now,
            "st_ctime": self._now,
        }

    def _is_virtual_dir(self, path: str) -> bool:
        normalized = path.rstrip("/") + "/"
        for p in self._prefixes:
            if p.startswith(normalized) or p.rstrip("/") == path.rstrip("/"):
                return True
        return False

    def getattr(self, path: str, fh=None) -> dict:
        if path == "/":
            return self._dir_stat()
        if path == _MIRAGE_DIR:
            return self._dir_stat()
        if path == _MIRAGE_WHOAMI:
            return self._file_stat(len(self._whoami_content()))
        if self._is_virtual_dir(path):
            return self._dir_stat()
        try:
            s = self.ws.stat(path)
            return self._file_stat(s.size or 0)
        except (FileNotFoundError, ValueError):
            pass
        try:
            self.ws.ls(path)
            return self._dir_stat()
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def _virtual_children(self, path: str) -> list[str]:
        normalized = path.rstrip("/") + "/" if path != "/" else "/"
        children = set()
        for p in self._prefixes:
            if p.startswith(normalized) and p != normalized:
                rest = p[len(normalized):]
                child = rest.split("/")[0]
                if child:
                    children.add(child)
        return sorted(children)

    def _clean_path(self, path: str) -> str:
        basename = os.path.basename(path)
        if basename.startswith("._"):
            basename = basename[2:]
            return os.path.join(os.path.dirname(path), basename)
        return path

    def _record_op(self, op: str, path: str, raw: str) -> None:
        path = self._clean_path(path)
        with self._ops_lock:
            self._ops.append({
                "type":
                "fuse_op",
                "op":
                op,
                "path":
                path,
                "timestamp":
                datetime.now(timezone.utc).isoformat(),
                "raw":
                raw,
            })

    def drain_ops(self) -> list[dict]:
        with self._ops_lock:
            ops = list(self._ops)
            self._ops.clear()
            self._read_seen.clear()
            return ops

    def readdir(self, path: str, fh) -> list:
        if path == _MIRAGE_DIR:
            return [".", "..", "whoami"]
        names = set(self._virtual_children(path))
        if path == "/":
            names.add(".mirage")
        try:
            entries = self.ws.ls(path)
            for e in entries:
                part = e.rstrip("/").rsplit("/", 1)[-1]
                if part:
                    names.add(part)
        except (FileNotFoundError, ValueError):
            if not names:
                raise fuse.FuseOSError(errno.ENOENT)
        return [".", ".."] + sorted(names)

    def _record_read(self, path: str) -> None:
        clean = self._clean_path(path)
        with self._ops_lock:
            if clean not in self._read_seen:
                self._read_seen.add(clean)
                self._ops.append({
                    "type":
                    "fuse_op",
                    "op":
                    "read",
                    "path":
                    clean,
                    "timestamp":
                    datetime.now(timezone.utc).isoformat(),
                    "raw":
                    f"read {clean}",
                })

    def read(self, path: str, size: int, offset: int, fh) -> bytes:
        if path == _MIRAGE_WHOAMI:
            data = self._whoami_content()
            return data[offset:offset + size]
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is None or ctx is None:
            try:
                data = self.ws.cat(path)
                self._record_read(path)
                return data[offset:offset + size]
            except (FileNotFoundError, ValueError):
                raise fuse.FuseOSError(errno.ENOENT)
        try:
            data = op.read(self.ws, path, offset, size, ctx)
            self._record_read(path)
            return data
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def write(self, path: str, data: bytes, offset: int, fh) -> int:
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is not None and ctx is not None:
            try:
                result = op.write(self.ws, path, offset, data, ctx)
                self._record_op("write", path, f"write {path}")
                return result
            except PermissionError:
                raise fuse.FuseOSError(errno.EACCES)
        try:
            existing = b""
            try:
                existing = self.ws.cat(path)
            except FileNotFoundError:
                pass
            if offset > len(existing):
                existing = existing + b"\0" * (offset - len(existing))
            new_data = existing[:offset] + data + existing[offset + len(data):]
            self.ws.write(path, new_data)
            self._record_op("write", path, f"write {path}")
            return len(data)
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def create(self, path: str, mode, fi=None) -> int:
        try:
            self.ws.write(path, b"")
            self._record_op("create", path, f"create {path}")
            op = self._fuse_registry.resolve(path, 0, self.ws)
            ctx = op.on_open(self.ws, path, 0)
            ctx["path"] = path
            fh = self._next_fh
            self._next_fh += 1
            self._handles[fh] = ctx
            self._handle_ops[fh] = op
            return fh
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def mkdir(self, path: str, mode) -> None:
        try:
            self.ws.mkdir(path)
            self._record_op("mkdir", path, f"mkdir {path}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def unlink(self, path: str) -> None:
        try:
            self.ws.rm(path)
            self._record_op("rm", path, f"rm {path}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except FileNotFoundError:
            raise fuse.FuseOSError(errno.ENOENT)

    def rename(self, old: str, new: str, flags: int = 0) -> None:
        try:
            self.ws.mv(old, new)
            self._record_op("mv", old, f"mv {old} → {new}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def rmdir(self, path: str) -> None:
        try:
            entries = self.ws.ls(path)
            if entries:
                raise fuse.FuseOSError(errno.ENOTEMPTY)
            self._record_op("rmdir", path, f"rmdir {path}")
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def statfs(self, path: str) -> dict:
        return {
            "f_bsize": 4096,
            "f_frsize": 4096,
            "f_blocks": 1024 * 1024,
            "f_bfree": 1024 * 1024,
            "f_bavail": 1024 * 1024,
            "f_files": 1000000,
            "f_ffree": 1000000,
            "f_favail": 1000000,
            "f_namemax": 255,
        }

    def chmod(self, path: str, mode) -> None:
        self.getattr(path)

    def chown(self, path: str, uid: int, gid: int) -> None:
        self.getattr(path)

    def utimens(self, path: str, times=None) -> None:
        self.getattr(path)

    def access(self, path: str, amode: int) -> None:
        self.getattr(path)

    def flush(self, path: str, fh) -> None:
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is not None and ctx is not None:
            try:
                op.on_flush(self.ws, path, ctx)
            except PermissionError:
                raise fuse.FuseOSError(errno.EACCES)
            return
        self.ws.sync(path)

    def fsync(self, path: str, datasync: int, fh) -> None:
        self.flush(path, fh)

    def open(self, path: str, flags) -> int:
        if path == _MIRAGE_WHOAMI:
            fh = self._next_fh
            self._next_fh += 1
            self._handles[fh] = {"path": path}
            return fh
        try:
            s = self.ws.stat(path)
            size = s.size or 0
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)
        op = self._fuse_registry.resolve(path, size, self.ws)
        ctx = op.on_open(self.ws, path, size)
        ctx["path"] = path
        fh = self._next_fh
        self._next_fh += 1
        self._handles[fh] = ctx
        self._handle_ops[fh] = op
        return fh

    def release(self, path: str, fh) -> int:
        self._handles.pop(fh, None)
        self._handle_ops.pop(fh, None)
        return 0

    def truncate(self, path: str, length: int, fh=None) -> None:
        try:
            data = b""
            try:
                data = self.ws.cat(path)
            except FileNotFoundError:
                pass
            padded = data[:length].ljust(length, b"\0")
            self.ws.write(path, padded)
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

def _run_fuse(fs: MirageFS, mountpoint: str, foreground: bool) -> None:
    fuse.FUSE(fs, mountpoint, nothreads=True, foreground=foreground)

def mount_background(ws: Workspace,
                     mountpoint: str,
                     agent_id: str | None = None) -> threading.Thread:
    """Start FUSE in a daemon thread and return immediately."""
    fs = MirageFS(ws, agent_id=agent_id)
    t = threading.Thread(target=_run_fuse,
                         args=(fs, mountpoint, True),
                         daemon=True)
    t.start()
    time.sleep(0.3)
    return t

def mount(ws: Workspace | None = None,
          mountpoint: str = "",
          foreground: bool = True,
          agent_id: str | None = None,
          fs: MirageFS | None = None) -> None:
    if fs is None:
        fs = MirageFS(ws, agent_id=agent_id)
    t = threading.Thread(
        target=_run_fuse,
        args=(fs, mountpoint, foreground),
        daemon=True,
    )
    t.start()
    try:
        while t.is_alive():
            t.join(timeout=0.5)
    except KeyboardInterrupt:
        print("\nUnmounting...", flush=True)
        if sys.platform == "darwin":
            subprocess.run(
                ["diskutil", "unmount", "force", mountpoint],
                capture_output=True,
            )
        else:
            subprocess.run(["fusermount", "-u", mountpoint],
                           capture_output=True)
        t.join(timeout=5)


     1→from mirage_fuse.fs import MirageFS, mount, mount_background
     2→
     3→__all__ = ["MirageFS", "mount", "mount_background"]
     4→

from mirage_fuse.fs import MirageFS, mount, mount_background

__all__ = ["MirageFS", "mount", "mount_background"]


     1→[project]
     2→name = "mirage-fuse"
     3→version = "0.0.1"
     4→description = "FUSE filesystem adapter for mirage workspaces"
     5→requires-python = ">=3.12"
     6→dependencies = [
     7→    "fusepy>=3.0.1",
     8→    "mirage",
     9→]
    10→
    11→[build-system]
    12→requires = ["setuptools>=68"]
    13→build-backend = "setuptools.build_meta"
    14→
    15→[tool.setuptools.packages.find]
    16→include = ["mirage_fuse*"]
    17→
    18→[tool.uv.sources]
    19→mirage = { workspace = true }
    20→

[project]
name = "mirage-fuse"
version = "0.0.1"
description = "FUSE filesystem adapter for mirage workspaces"
requires-python = ">=3.12"
dependencies = [
    "fusepy>=3.0.1",
    "mirage",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["mirage_fuse*"]

[tool.uv.sources]
mirage = { workspace = true }


The top-level `fuse/` is a **separate Python package** (`mirage-fuse`) with its own `pyproject.toml`. It's distributed as a separate installable package. It contains:
- `fuse/mirage_fuse/fs.py` — the actual FUSE filesystem (`MirageFS`, `mount`, `mount_background`)
- `fuse/mirage_fuse/__init__.py` — re-exports
- `fuse/pyproject.toml` — separate package definition
- `fuse/tests/test_fs.py` — tests
- `fuse/README.md`

It's kept separate because `fusepy` is a heavy optional dependency that shouldn't be required for the core `mirage` package, but merging it into `mirage/fuse/` would mean treating `fusepy` as an optional dependency instead. The user wants me to move it anyway, which would mean `fuse/mirage_fuse/fs.py` becomes `mirage/fuse/fs.py`. I should clarify this with the user since merging the packages would be a breaking change that affects how `fusepy` is handled as a dependency.


Important consideration: the top-level `fuse/` is a **separate Python package** (`mirage-fuse`) with its own `pyproject.toml`. It depends on `fusepy`, which is kept out of the core `mirage` package intentionally — not every user needs FUSE.

If we merge it into `mirage/fuse/`, we'd need to make `fusepy` an optional dependency of the main package (like how boto3 is optional for S3).

The merged structure would be:
```
mirage/fuse/
├── __init__.py
├── base.py              ← FuseOp base class
├── fs.py                ← MirageFS (from fuse/mirage_fuse/fs.py)
├── registry.py
├── loader.py
├── backend/
│   ├── default.py
│   ├── local.py
│   ├── memory.py
│   └── s3.py
├── filetype/
│   ├── csv.py
│   └── parquet.py
└── remote/
    └── remote.py
```

And the separate `fuse/` package directory would be removed. Is that what you want, or should we keep `fs.py` in the separate package and only reorganize the ops?


Yea we change the dependency
also remmeber to include the root fuse README.md here also


The user wants to:
1. Merge `fuse/mirage_fuse/` into `mirage/fuse/`
2. Rename `mirage/fuse_ops/` to `mirage/fuse/`
3. Reorganize with `backend/`, `filetype/`, `remote/` subfolders
4. Make `fusepy` an optional dependency
5. Include the `fuse/README.md`
6. Remove the separate `fuse/` package

Let me write a design doc and then implement. Actually, the user said to brainstorm and plan earlier. Let me write the plan and then ask if ready to implement.

I'm going back through the conversation and realizing we've already settled on the design decisions during brainstorming. The user seems ready to move forward, so I'll write out the design doc with the final structure and ask if they want me to proceed with implementation.

Now I'm laying out the complete directory structure for the refactored mirage/fuse module, showing how files will be reorganized across backend, filetype, and remote subdirectories. The main tasks are creating the new directories and migrating the existing files into their new homes.

I need to update all the import statements throughout the codebase to reflect the new module paths, making sure fusepy becomes optional, and then clean up the old directory structure. Rather than writing out a formal design document, I'll move straight into implementation since the design decisions are already locked in.

Before I start, let me identify the key complications: the test files currently import from `mirage_fuse` and will need to move to a new location with updated imports, the separate `fuse/pyproject.toml` needs to be consolidated into the main project config, and I need to carefully handle the transition of the `fuse_ops/` directory structure.

I've already mapped out the scope of this refactoring, so let me move forward with the implementation plan: create the new `mirage/fuse/` directory structure, use git mv to preserve history, write the necessary `__init__.py` files to expose the public API, systematically update all import statements across the codebase, and add fusepy as an optional dependency in the main configuration.

Now I need to handle the remaining steps—moving the test file and README, cleaning up the old directory, and running tests to verify everything works. But there's a naming issue I need to resolve first: renaming `csv_op.py` to `csv.py` would shadow Python's built-in csv module. Let me check whether the file actually imports the stdlib csv module, since that would determine if this is a real problem or not.


Let me first check one naming concern, then implement.


No matches found


Grep

from mirage\.fuse_ops|from mirage_fuse|import mirage\.fuse_ops|import mirage_fuse


examples/claude-memory.py:19:from mirage_fuse import mount_background
examples/claude-sdk.py:22:from mirage_fuse import MirageFS, mount_background
tests/fuse_ops/test_remote.py:4:from mirage.fuse_ops.remote import RemoteFuseOp
tests/fuse_ops/test_default.py:2:from mirage.fuse_ops.default import DefaultFuseOp
tests/fuse_ops/test_default.py:3:from mirage.fuse_ops.memory import MemoryFuseOp
fuse/mirage_fuse/fs.py:13:from mirage.fuse_ops.base import FuseOp
fuse/mirage_fuse/fs.py:14:from mirage.fuse_ops.registry import FuseOpRegistry
tests/fuse_ops/test_loader.py:6:from mirage.fuse_ops.loader import (build_registry_from_config,
tests/fuse_ops/test_loader.py:17:    code = ("from mirage.fuse_ops import FuseOp\n"
tests/fuse_ops/test_format_ops.py:7:from mirage.fuse_ops.csv_op import CsvFuseOp
tests/fuse_ops/test_format_ops.py:8:from mirage.fuse_ops.parquet import ParquetFuseOp
tests/fuse_ops/test_registry.py:1:from mirage.fuse_ops import FUSE_OPS, FuseOp
tests/fuse_ops/test_registry.py:2:from mirage.fuse_ops.registry import FuseOpRegistry
fuse/mirage_fuse/__init__.py:1:from mirage_fuse.fs import MirageFS, mount, mount_background
tests/runtime/test_accessors.py:5:from mirage.fuse_ops.registry import FuseOpRegistry
fuse/README.md:62:from mirage_fuse import MirageFS, mount
fuse/tests/test_fs.py:9:from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS
fuse/tests/test_fs.py:218:    from mirage.fuse_ops.registry import FuseOpRegistry
fuse/tests/test_fs.py:227:    from mirage.fuse_ops.registry import FuseOpRegistry
fuse/tests/test_fs.py:255:    from mirage_fuse import mount_background
fuse/tests/test_fs.py:278:    from mirage_fuse import mount_background
mirage/runtime/base.py:8:from mirage.fuse_ops.registry import FuseOpRegistry
mirage/cli/commands/workspace.py:15:from mirage.fuse_ops.loader import build_registry_from_config
mirage/cli/commands/workspace.py:16:from mirage.fuse_ops.registry import FuseOpRegistry
mirage/cli/commands/workspace.py:23:    from mirage_fuse import MirageFS
mirage/cli/commands/workspace.py:204:    from mirage_fuse import MirageFS
mirage/cli/commands/workspace.py:205:    from mirage_fuse import mount as fuse_mount_fn
mirage/cli/commands/workspace.py:208:    from mirage.fuse_ops.remote import RemoteFuseOp
mirage/runtime/accessors/local.py:2:from mirage.fuse_ops import FUSE_OPS, FuseOp
mirage/runtime/accessors/local.py:3:from mirage.fuse_ops.loader import load_fuse_op_class
mirage/runtime/accessors/local.py:4:from mirage.fuse_ops.registry import FuseOpRegistry
mirage/server/service/local.py:16:from mirage.fuse_ops import FUSE_OPS
mirage/server/service/local.py:17:from mirage.fuse_ops.loader import (build_registry_from_config,
mirage/server/service/local.py:19:from mirage.fuse_ops.registry import FuseOpRegistry
mirage/fuse_ops/local.py:1:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/registry.py:6:from mirage.fuse_ops import FUSE_OPS, FuseOp
mirage/fuse_ops/s3.py:1:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/csv_op.py:5:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/default.py:1:from mirage.fuse_ops.base import FuseOp
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/fuse_ops/remote.py:6:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/loader.py:5:from mirage.fuse_ops import FUSE_OPS
mirage/fuse_ops/loader.py:6:from mirage.fuse_ops.registry import FuseOpRegistry
mirage/fuse_ops/memory.py:1:from mirage.fuse_ops.base import FuseOp
mirage/fuse_ops/parquet.py:6:from mirage.fuse_ops.base import FuseOp


Good - `csv_op.py` doesn't import stdlib `csv`, so we can safely rename it to `csv.py`.

Now let me plan the implementation:

**Target structure:**
```
mirage/fuse/
├── __init__.py
├── base.py
├── fs.py                ← from fuse/mirage_fuse/fs.py
├── registry.py
├── loader.py
├── README.md            ← from fuse/README.md
├── backend/
│   ├── __init__.py
│   ├── default.py
│   ├── local.py

│   ├── memory.py
│   └── s3.py
├── filetype/
│   ├── __init__.py
│   ├── csv.py           ← renamed from csv_op.py
│   └── parquet.py
└── remote/
    ├── __init__.py
    └── remote.py
```

I need to update all the import paths throughout the codebase - replacing `mirage.fuse_ops` with `mirage.fuse` and adjusting the submodule imports accordingly. I'm finishing up the module path mappings, then I need to reorganize the test structure by moving `tests/fuse_ops/` to `tests/fuse/` for consistency, update the main `pyproject.toml` to include `fusepy` as an optional dependency, and clean up the old `fuse/` directory structure.

Now I'm planning out the refactoring steps systematically—creating the new directory layout, moving files with git, writing the necessary `__init__.py` files for the subpackages, then updating all the internal imports within the fuse module and external imports throughout the codebase before moving tests and updating configuration files.

I'm also checking whether `csv.py` in the filetype subpackage would shadow Python's standard library `csv` module—it shouldn't be an issue since the file doesn't import the stdlib csv, but I should verify that consumers won't run into conflicts.


Good — `csv_op.py` doesn't import stdlib `csv`, so renaming to `csv.py` is safe. Let me implement everything now.


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update all imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Move tests and fuse/README.md

Update pyproject.toml (add fusepy optional dep, remove fuse/pyproject.toml)

Clean up old fuse/ and fuse_ops/ directories

Run tests


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update all imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Move tests and fuse/README.md

Update pyproject.toml (add fusepy optional dep, remove fuse/pyproject.toml)

Clean up old fuse/ and fuse_ops/ directories

Run tests


Bash

mkdir -p mirage/fuse/backend mirage/fuse/filetype mirage/fuse/remote && git mv mirage/fuse_ops/base.py mirage/fuse/base.py && git mv mirage/fuse_ops/registry.py mirage/fuse/registry.py && git mv mirage/fuse_ops/loader.py mirage/fuse/loader.py && git mv mirage/fuse_ops/default.py mirage/fuse/backend/default.py && git mv mirage/fuse_ops/local.py mirage/fuse/backend/local.py && git mv mirage/fuse_ops/memory.py mirage/fuse/backend/memory.py && git mv mirage/fuse_ops/s3.py mirage/fuse/backend/s3.py && git mv mirage/fuse_ops/csv_op.py mirage/fuse/filetype/csv.py && git mv mirage/fuse_ops/parquet.py mirage/fuse/filetype/parquet.py && git mv mirage/fuse_ops/remote.py mirage/fuse/remote/remote.py && git mv fuse/mirage_fuse/fs.py mirage/fuse/fs.py && git mv fuse/README.md mirage/fuse/README.md

Create directories and git mv all fuse files


Now move the tests.


git mv tests/fuse_ops tests/fuse && git mv fuse/tests/test_fs.py tests/fuse/test_fs.py

Move fuse_ops tests and fuse/tests/test_fs.py


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update all imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Move tests and fuse/README.md

Update pyproject.toml (add fusepy optional dep, remove fuse/pyproject.toml)

Clean up old fuse/ and fuse_ops/ directories

Run tests


Write sub-package __init__.py files

Move tests and fuse/README.md

Run tests

Write sub-package __init__.py files

Move tests and fuse/README.md

Run tests


Now write the `__init__.py` files and update all imports. Let me read the old `__init__.py` first.


     1→from mirage.fuse_ops.base import FuseOp
     2→from mirage.fuse_ops.csv_op import CsvFuseOp
     3→from mirage.fuse_ops.default import DefaultFuseOp
     4→from mirage.fuse_ops.local import LocalFuseOp
     5→from mirage.fuse_ops.memory import MemoryFuseOp
     6→from mirage.fuse_ops.parquet import ParquetFuseOp
     7→from mirage.fuse_ops.s3 import S3FuseOp
     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_ops.base import FuseOp
from mirage.fuse_ops.csv_op import CsvFuseOp
from mirage.fuse_ops.default import DefaultFuseOp
from mirage.fuse_ops.local import LocalFuseOp
from mirage.fuse_ops.memory import MemoryFuseOp
from mirage.fuse_ops.parquet import ParquetFuseOp
from mirage.fuse_ops.s3 import S3FuseOp

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",
]


Write

from mirage.fuse.backend.csv import CsvFuseOp
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.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",
]


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

from mirage.fuse.backend.csv import CsvFuseOp
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.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",
]


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


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


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


Wait, I made an error in the __init__.py. CsvFuseOp and ParquetFuseOp should be in `filetype/`, not `backend/`. Let me fix that.

Actually, looking at my structure:
- backend/: default, local, memory, s3 (read/write strategies per storage)
- filetype/: csv, parquet (virtual rendering per file type)

So the __init__.py should import:
- CsvFuseOp from mirage.fuse.filetype.csv
- ParquetFuseOp from mirage.fuse.filetype.parquet

- DefaultFuseOp from mirage.fuse.backend.default and the other backend operations

Now I need to update the __init__.py with the correct imports.


Edit

from mirage.fuse.backend.csv import CsvFuseOp
from mirage.fuse.backend.default import DefaultFuseOp

from mirage.fuse.backend.default import DefaultFuseOp


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

from mirage.fuse.backend.csv import CsvFuseOp
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.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.csv import CsvFuseOp


from mirage.fuse.base import FuseOp
from mirage.fuse.filetype.parquet import ParquetFuseOp

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


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

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.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.filetype.csv import CsvFuseOp


The IDE is showing that CsvFuseOp is not defined on line 14. Let me read the current state of the file to see what's going on.


     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",
]


The file looks correct — the IDE diagnostic is likely stale since the moved files still have old import paths. Let me now update all imports across the codebase. First, update the internal fuse files, then external files.


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Write sub-package __init__.py files

Move tests and fuse/README.md

Run tests

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


     1→from mirage.fuse_ops.base import FuseOp
     2→
     3→
     4→class DefaultFuseOp(FuseOp):
     5→
     6→    name = "default"
     7→

from mirage.fuse_ops.base import FuseOp

class DefaultFuseOp(FuseOp):

    name = "default"


     1→from mirage.fuse_ops.base import FuseOp
     2→
     3→
     4→class LocalFuseOp(FuseOp):
     5→
     6→    name = "local"
     7→
     8→    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
     9→        return ws.read(path, offset, size)
    10→

from mirage.fuse_ops.base import FuseOp

class LocalFuseOp(FuseOp):

    name = "local"

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        return ws.read(path, offset, size)


     1→from mirage.fuse_ops.base import FuseOp
     2→
     3→
     4→class MemoryFuseOp(FuseOp):
     5→
     6→    name = "memory"
     7→
     8→    def on_open(self, ws, path: str, size: int) -> dict:
     9→        return {"eager": True}
    10→
    11→    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
    12→        if "data" not in ctx:
    13→            ctx["data"] = ws.cat(path)
    14→        return ctx["data"][offset:offset + size]
    15→

from mirage.fuse_ops.base import FuseOp

class MemoryFuseOp(FuseOp):

    name = "memory"

    def on_open(self, ws, path: str, size: int) -> dict:
        return {"eager": True}

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        if "data" not in ctx:
            ctx["data"] = ws.cat(path)
        return ctx["data"][offset:offset + size]


     1→from mirage.fuse_ops.base import FuseOp
     2→
     3→
     4→class S3FuseOp(FuseOp):
     5→
     6→    name = "s3"
     7→
     8→    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
     9→        return ws.read(path, offset, size)
    10→

from mirage.fuse_ops.base import FuseOp

class S3FuseOp(FuseOp):

    name = "s3"

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        return ws.read(path, offset, size)


     1→from __future__ import annotations
     2→
     3→from typing import TYPE_CHECKING
     4→
     5→from mirage.fuse_ops.base import FuseOp
     6→
     7→if TYPE_CHECKING:
     8→    from mirage.workspace import Workspace
     9→
    10→_LARGE_CSV_THRESHOLD = 50 * 1024  # 50KB
    11→_MAX_PREVIEW_ROWS = 20
    12→
    13→
    14→class CsvFuseOp(FuseOp):
    15→
    16→    name = "csv"
    17→
    18→    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
    19→        return {"eager": True, "virtual": size > _LARGE_CSV_THRESHOLD}
    20→
    21→    def read(self, ws: Workspace, path: str, offset: int, size: int,
    22→             ctx: dict) -> bytes:
    23→        if "data" not in ctx:
    24→            raw = ws.cat(path)
    25→            if ctx.get("virtual"):
    26→                ctx["data"] = self._render_preview(raw, path)
    27→            else:
    28→                ctx["data"] = raw
    29→        return ctx["data"][offset:offset + size]
    30→
    31→    def _render_preview(self, raw: bytes, path: str) -> bytes:
    32→        text = raw.decode(errors="replace")
    33→        lines = text.splitlines()
    34→        total = len(lines)
    35→        header = lines[0] if lines else ""
    36→        preview_lines = lines[:_MAX_PREVIEW_ROWS + 1]
    37→        cols = len(header.split(",")) if header else 0
    38→        remaining = total - _MAX_PREVIEW_ROWS - 1
    39→        result = [
    40→            f"# CSV: {path}",
    41→            f"# {total} rows, {cols} columns",
    42→            "",
    43→            "\n".join(preview_lines),
    44→            "",
    45→            f"# ... ({remaining} more rows)",
    46→            "",
    47→        ]
    48→        return "\n".join(result).encode()
    49→

from __future__ import annotations

from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_LARGE_CSV_THRESHOLD = 50 * 1024  # 50KB
_MAX_PREVIEW_ROWS = 20

class CsvFuseOp(FuseOp):

    name = "csv"

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        return {"eager": True, "virtual": size > _LARGE_CSV_THRESHOLD}

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        if "data" not in ctx:
            raw = ws.cat(path)
            if ctx.get("virtual"):
                ctx["data"] = self._render_preview(raw, path)
            else:
                ctx["data"] = raw
        return ctx["data"][offset:offset + size]

    def _render_preview(self, raw: bytes, path: str) -> bytes:
        text = raw.decode(errors="replace")
        lines = text.splitlines()
        total = len(lines)
        header = lines[0] if lines else ""
        preview_lines = lines[:_MAX_PREVIEW_ROWS + 1]
        cols = len(header.split(",")) if header else 0
        remaining = total - _MAX_PREVIEW_ROWS - 1
        result = [
            f"# CSV: {path}",
            f"# {total} rows, {cols} columns",
            "",
            "\n".join(preview_lines),
            "",
            f"# ... ({remaining} more rows)",
            "",
        ]
        return "\n".join(result).encode()


     1→from __future__ import annotations
     2→
     3→import io
     4→from typing import TYPE_CHECKING
     5→
     6→from mirage.fuse_ops.base import FuseOp
     7→
     8→if TYPE_CHECKING:
     9→    from mirage.workspace import Workspace
    10→
    11→_MAX_PREVIEW_ROWS = 20
    12→
    13→
    14→class ParquetFuseOp(FuseOp):
    15→
    16→    name = "parquet"
    17→
    18→    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
    19→        return {"eager": True, "virtual": True}
    20→
    21→    def read(self, ws: Workspace, path: str, offset: int, size: int,
    22→             ctx: dict) -> bytes:
    23→        if "data" not in ctx:
    24→            ctx["data"] = self._render(ws, path)
    25→        return ctx["data"][offset:offset + size]
    26→
    27→    def _render(self, ws: Workspace, path: str) -> bytes:
    28→        try:
    29→            import pyarrow.parquet as pq
    30→
    31→            raw = ws.cat(path)
    32→            pf = pq.ParquetFile(io.BytesIO(raw))
    33→            schema = pf.schema_arrow
    34→            num_rows = pf.metadata.num_rows
    35→            table = pf.read_row_groups([0]).slice(0, _MAX_PREVIEW_ROWS)
    36→            preview_rows = min(num_rows, _MAX_PREVIEW_ROWS)
    37→            lines = [
    38→                f"# Parquet: {path}",
    39→                f"# Rows: {num_rows}, Columns: {len(schema)}",
    40→                "",
    41→                "## Schema",
    42→            ]
    43→            for field in schema:
    44→                lines.append(f"  {field.name}: {field.type}")
    45→            lines.append("")
    46→            lines.append(f"## Preview (first {preview_rows} rows)")
    47→            lines.append("")
    48→            lines.append(table.to_pandas().to_string(index=False))
    49→            lines.append("")
    50→            return "\n".join(lines).encode()
    51→        except Exception as e:
    52→            return f"# Error reading parquet: {e}\n".encode()
    53→

from __future__ import annotations

import io
from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_MAX_PREVIEW_ROWS = 20

class ParquetFuseOp(FuseOp):

    name = "parquet"

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        return {"eager": True, "virtual": True}

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        if "data" not in ctx:
            ctx["data"] = self._render(ws, path)
        return ctx["data"][offset:offset + size]

    def _render(self, ws: Workspace, path: str) -> bytes:
        try:
            import pyarrow.parquet as pq

            raw = ws.cat(path)
            pf = pq.ParquetFile(io.BytesIO(raw))
            schema = pf.schema_arrow
            num_rows = pf.metadata.num_rows
            table = pf.read_row_groups([0]).slice(0, _MAX_PREVIEW_ROWS)
            preview_rows = min(num_rows, _MAX_PREVIEW_ROWS)
            lines = [
                f"# Parquet: {path}",
                f"# Rows: {num_rows}, Columns: {len(schema)}",
                "",
                "## Schema",
            ]
            for field in schema:
                lines.append(f"  {field.name}: {field.type}")
            lines.append("")
            lines.append(f"## Preview (first {preview_rows} rows)")
            lines.append("")
            lines.append(table.to_pandas().to_string(index=False))
            lines.append("")
            return "\n".join(lines).encode()
        except Exception as e:
            return f"# Error reading parquet: {e}\n".encode()


     1→from __future__ import annotations
     2→
     3→import hashlib
     4→from typing import TYPE_CHECKING
     5→
     6→from mirage.fuse_ops.base import FuseOp
     7→
     8→if TYPE_CHECKING:
     9→    from mirage.workspace import Workspace
    10→
    11→
    12→class RemoteFuseOp(FuseOp):
    13→
    14→    name: str = "remote"
    15→
    16→    def __init__(self, client: object) -> None:
    17→        self._client = client
    18→
    19→    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
    20→        data = ws.cat(path) if size > 0 else b""
    21→        checksum = hashlib.md5(data).hexdigest()
    22→        return self._client.on_open(path=path,
    23→                                    size=size,
    24→                                    data=data,
    25→                                    checksum=checksum)
    26→
    27→    def read(self, ws: Workspace, path: str, offset: int, size: int,
    28→             ctx: dict) -> bytes:
    29→        return self._client.read(path=path, offset=offset, size=size, ctx=ctx)
    30→
    31→    def write(self, ws: Workspace, path: str, offset: int, data: bytes,
    32→              ctx: dict) -> int:
    33→        return self._client.write(path=path, offset=offset, data=data, ctx=ctx)
    34→
    35→    def on_flush(self, ws: Workspace, path: str, ctx: dict) -> None:
    36→        result = self._client.on_flush(path=path, ctx=ctx)
    37→        if result is not None:
    38→            ws.write(path, result)
    39→

from __future__ import annotations

import hashlib
from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

class RemoteFuseOp(FuseOp):

    name: str = "remote"

    def __init__(self, client: object) -> None:
        self._client = client

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        data = ws.cat(path) if size > 0 else b""
        checksum = hashlib.md5(data).hexdigest()
        return self._client.on_open(path=path,
                                    size=size,
                                    data=data,
                                    checksum=checksum)

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        return self._client.read(path=path, offset=offset, size=size, ctx=ctx)

    def write(self, ws: Workspace, path: str, offset: int, data: bytes,
              ctx: dict) -> int:
        return self._client.write(path=path, offset=offset, data=data, ctx=ctx)

    def on_flush(self, ws: Workspace, path: str, ctx: dict) -> None:
        result = self._client.on_flush(path=path, ctx=ctx)
        if result is not None:
            ws.write(path, result)


     1→from __future__ import annotations
     2→
     3→import fnmatch
     4→from typing import TYPE_CHECKING
     5→
     6→from mirage.fuse_ops import FUSE_OPS, FuseOp
     7→
     8→if TYPE_CHECKING:
     9→    from mirage.workspace import Workspace
    10→
    11→_BACKEND_OP_MAP: dict[str, str] = {
    12→    "S3Backend": "s3",
    13→    "R2Backend": "s3",
    14→    "MemoryBackend": "memory",
    15→    "LocalBackend": "local",
    16→}
    17→
    18→
    19→class FuseOpRegistry:
    20→
    21→    def __init__(self) -> None:
    22→        self._ops: list[tuple[str, FuseOp]] = []
    23→        self._default: FuseOp = FUSE_OPS["default"]
    24→
    25→    def register(self, pattern: str, op: FuseOp) -> None:
    26→        self._ops.append((pattern, op))
    27→
    28→    def set_default(self, op: FuseOp) -> None:
    29→        self._default = op
    30→
    31→    def resolve(self,
    32→                path: str,
    33→                size: int,
    34→                ws: Workspace | None = None) -> FuseOp:
    35→        basename = path.rsplit("/", 1)[-1] if "/" in path else path
    36→        for pattern, op in self._ops:
    37→            if fnmatch.fnmatch(basename, pattern):
    38→                return op
    39→        if ws is not None:
    40→            try:
    41→                backend, _, _ = ws._registry.resolve(path)
    42→                backend_name = type(backend).__name__
    43→                op_key = _BACKEND_OP_MAP.get(backend_name)
    44→                if op_key and op_key in FUSE_OPS:
    45→                    return FUSE_OPS[op_key]
    46→            except (ValueError, FileNotFoundError):
    47→                pass
    48→        return self._default
    49→
    50→    def list_ops(self) -> list[dict]:
    51→        ops = [{"pattern": p, "name": o.name} for p, o in self._ops]
    52→        ops.append({"pattern": "*", "name": self._default.name})
    53→        return ops
    54→

from __future__ import annotations

import fnmatch
from typing import TYPE_CHECKING

from mirage.fuse_ops import FUSE_OPS, FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_BACKEND_OP_MAP: dict[str, str] = {
    "S3Backend": "s3",
    "R2Backend": "s3",
    "MemoryBackend": "memory",
    "LocalBackend": "local",
}

class FuseOpRegistry:

    def __init__(self) -> None:
        self._ops: list[tuple[str, FuseOp]] = []
        self._default: FuseOp = FUSE_OPS["default"]

    def register(self, pattern: str, op: FuseOp) -> None:
        self._ops.append((pattern, op))

    def set_default(self, op: FuseOp) -> None:
        self._default = op

    def resolve(self,
                path: str,
                size: int,
                ws: Workspace | None = None) -> FuseOp:
        basename = path.rsplit("/", 1)[-1] if "/" in path else path
        for pattern, op in self._ops:
            if fnmatch.fnmatch(basename, pattern):
                return op
        if ws is not None:
            try:
                backend, _, _ = ws._registry.resolve(path)
                backend_name = type(backend).__name__
                op_key = _BACKEND_OP_MAP.get(backend_name)
                if op_key and op_key in FUSE_OPS:
                    return FUSE_OPS[op_key]
            except (ValueError, FileNotFoundError):
                pass
        return self._default

    def list_ops(self) -> list[dict]:
        ops = [{"pattern": p, "name": o.name} for p, o in self._ops]
        ops.append({"pattern": "*", "name": self._default.name})
        return ops


     1→import importlib
     2→import importlib.util
     3→from typing import Any
     4→
     5→from mirage.fuse_ops import FUSE_OPS
     6→from mirage.fuse_ops.registry import FuseOpRegistry
     7→
     8→
     9→def load_fuse_op_class(spec: str) -> type[Any]:
    10→    if ":" not in spec:
    11→        raise ValueError(
    12→            f"invalid fuse op spec {spec!r}, expected 'source:ClassName'")
    13→
    14→    source, class_name = spec.rsplit(":", 1)
    15→
    16→    if "/" in source or source.endswith(".py"):
    17→        module_spec = importlib.util.spec_from_file_location(
    18→            "_mirage_user_fuse_op", source)
    19→        if module_spec is None or module_spec.loader is None:
    20→            raise ValueError(f"cannot load script {source!r}")
    21→        module = importlib.util.module_from_spec(module_spec)
    22→        try:
    23→            module_spec.loader.exec_module(module)
    24→        except FileNotFoundError:
    25→            raise ValueError(f"cannot load script {source!r}")
    26→    else:
    27→        module = importlib.import_module(source)
    28→
    29→    if not hasattr(module, class_name):
    30→        raise ValueError(f"{class_name!r} not found in {source!r}")
    31→
    32→    return getattr(module, class_name)
    33→
    34→
    35→def build_registry_from_config(fuse_ops: dict) -> FuseOpRegistry:
    36→    registry = FuseOpRegistry()
    37→    for pattern, value in fuse_ops.items():
    38→        if isinstance(value, str):
    39→            if value in FUSE_OPS:
    40→                registry.register(pattern, FUSE_OPS[value])
    41→            else:
    42→                cls = load_fuse_op_class(value)
    43→                registry.register(pattern, cls())
    44→        elif isinstance(value, dict):
    45→            handler = value.get("handler", "")
    46→            if handler in FUSE_OPS:
    47→                registry.register(pattern, FUSE_OPS[handler])
    48→            else:
    49→                cls = load_fuse_op_class(handler)
    50→                args = value.get("args", {})
    51→                registry.register(pattern, cls(**args))
    52→    return registry
    53→

import importlib
import importlib.util
from typing import Any

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

def load_fuse_op_class(spec: str) -> type[Any]:
    if ":" not in spec:
        raise ValueError(
            f"invalid fuse op spec {spec!r}, expected 'source:ClassName'")

    source, class_name = spec.rsplit(":", 1)

    if "/" in source or source.endswith(".py"):
        module_spec = importlib.util.spec_from_file_location(
            "_mirage_user_fuse_op", source)
        if module_spec is None or module_spec.loader is None:
            raise ValueError(f"cannot load script {source!r}")
        module = importlib.util.module_from_spec(module_spec)
        try:
            module_spec.loader.exec_module(module)
        except FileNotFoundError:
            raise ValueError(f"cannot load script {source!r}")
    else:
        module = importlib.import_module(source)

    if not hasattr(module, class_name):
        raise ValueError(f"{class_name!r} not found in {source!r}")

    return getattr(module, class_name)

def build_registry_from_config(fuse_ops: dict) -> FuseOpRegistry:
    registry = FuseOpRegistry()
    for pattern, value in fuse_ops.items():
        if isinstance(value, str):
            if value in FUSE_OPS:
                registry.register(pattern, FUSE_OPS[value])
            else:
                cls = load_fuse_op_class(value)
                registry.register(pattern, cls())
        elif isinstance(value, dict):
            handler = value.get("handler", "")
            if handler in FUSE_OPS:
                registry.register(pattern, FUSE_OPS[handler])
            else:
                cls = load_fuse_op_class(handler)
                args = value.get("args", {})
                registry.register(pattern, cls(**args))
    return registry


     1→import errno
     2→import os
     3→import stat
     4→import subprocess
     5→import sys
     6→import threading
     7→import time
     8→import uuid
     9→from collections import deque
    10→from datetime import datetime, timezone
    11→
    12→import fuse
    13→from mirage.fuse_ops.base import FuseOp
    14→from mirage.fuse_ops.registry import FuseOpRegistry
    15→from mirage.workspace import Workspace
    16→

import errno
import os
import stat
import subprocess
import sys
import threading
import time
import uuid
from collections import deque
from datetime import datetime, timezone

import fuse
from mirage.fuse_ops.base import FuseOp
from mirage.fuse_ops.registry import FuseOpRegistry
from mirage.workspace import Workspace


All internal files use `from mirage.fuse_ops.base import FuseOp`. Let me update them all.


The file /Users/agent/strukto/mirage/mirage/fuse/backend/default.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from mirage.fuse_ops.base import FuseOp

class DefaultFuseOp(FuseOp):

    name = "default"

-from mirage.fuse_ops.base import FuseOp


The file /Users/agent/strukto/mirage/mirage/fuse/backend/local.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from mirage.fuse_ops.base import FuseOp

class LocalFuseOp(FuseOp):

    name = "local"

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        return ws.read(path, offset, size)


The file /Users/agent/strukto/mirage/mirage/fuse/backend/memory.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from mirage.fuse_ops.base import FuseOp

class MemoryFuseOp(FuseOp):

    name = "memory"

    def on_open(self, ws, path: str, size: int) -> dict:
        return {"eager": True}

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        if "data" not in ctx:
            ctx["data"] = ws.cat(path)
        return ctx["data"][offset:offset + size]


The file /Users/agent/strukto/mirage/mirage/fuse/backend/s3.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from mirage.fuse_ops.base import FuseOp

class S3FuseOp(FuseOp):

    name = "s3"

    def read(self, ws, path: str, offset: int, size: int, ctx: dict) -> bytes:
        return ws.read(path, offset, size)


The file /Users/agent/strukto/mirage/mirage/fuse/filetype/csv.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from __future__ import annotations

from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_LARGE_CSV_THRESHOLD = 50 * 1024  # 50KB
_MAX_PREVIEW_ROWS = 20

class CsvFuseOp(FuseOp):

    name = "csv"

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        return {"eager": True, "virtual": size > _LARGE_CSV_THRESHOLD}

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        if "data" not in ctx:
            raw = ws.cat(path)
            if ctx.get("virtual"):
                ctx["data"] = self._render_preview(raw, path)
            else:
                ctx["data"] = raw
        return ctx["data"][offset:offset + size]

    def _render_preview(self, raw: bytes, path: str) -> bytes:
        text = raw.decode(errors="replace")
        lines = text.splitlines()
        total = len(lines)
        header = lines[0] if lines else ""
        preview_lines = lines[:_MAX_PREVIEW_ROWS + 1]
        cols = len(header.split(",")) if header else 0
        remaining = total - _MAX_PREVIEW_ROWS - 1
        result = [
            f"# CSV: {path}",
            f"# {total} rows, {cols} columns",
            "",
            "\n".join(preview_lines),
            "",
            f"# ... ({remaining} more rows)",
            "",
        ]
        return "\n".join(result).encode()


The file /Users/agent/strukto/mirage/mirage/fuse/filetype/parquet.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from __future__ import annotations

import io
from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_MAX_PREVIEW_ROWS = 20

class ParquetFuseOp(FuseOp):

    name = "parquet"

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        return {"eager": True, "virtual": True}

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        if "data" not in ctx:
            ctx["data"] = self._render(ws, path)
        return ctx["data"][offset:offset + size]

    def _render(self, ws: Workspace, path: str) -> bytes:
        try:
            import pyarrow.parquet as pq

            raw = ws.cat(path)
            pf = pq.ParquetFile(io.BytesIO(raw))
            schema = pf.schema_arrow
            num_rows = pf.metadata.num_rows
            table = pf.read_row_groups([0]).slice(0, _MAX_PREVIEW_ROWS)
            preview_rows = min(num_rows, _MAX_PREVIEW_ROWS)
            lines = [
                f"# Parquet: {path}",
                f"# Rows: {num_rows}, Columns: {len(schema)}",
                "",
                "## Schema",
            ]
            for field in schema:
                lines.append(f"  {field.name}: {field.type}")
            lines.append("")
            lines.append(f"## Preview (first {preview_rows} rows)")
            lines.append("")
            lines.append(table.to_pandas().to_string(index=False))
            lines.append("")
            return "\n".join(lines).encode()
        except Exception as e:
            return f"# Error reading parquet: {e}\n".encode()


The file /Users/agent/strukto/mirage/mirage/fuse/remote/remote.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

from __future__ import annotations

import hashlib
from typing import TYPE_CHECKING

from mirage.fuse_ops.base import FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

class RemoteFuseOp(FuseOp):

    name: str = "remote"

    def __init__(self, client: object) -> None:
        self._client = client

    def on_open(self, ws: Workspace, path: str, size: int) -> dict:
        data = ws.cat(path) if size > 0 else b""
        checksum = hashlib.md5(data).hexdigest()
        return self._client.on_open(path=path,
                                    size=size,
                                    data=data,
                                    checksum=checksum)

    def read(self, ws: Workspace, path: str, offset: int, size: int,
             ctx: dict) -> bytes:
        return self._client.read(path=path, offset=offset, size=size, ctx=ctx)

    def write(self, ws: Workspace, path: str, offset: int, data: bytes,
              ctx: dict) -> int:
        return self._client.write(path=path, offset=offset, data=data, ctx=ctx)

    def on_flush(self, ws: Workspace, path: str, ctx: dict) -> None:
        result = self._client.on_flush(path=path, ctx=ctx)
        if result is not None:
            ws.write(path, result)


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

from __future__ import annotations

import fnmatch
from typing import TYPE_CHECKING

from mirage.fuse_ops import FUSE_OPS, FuseOp

if TYPE_CHECKING:
    from mirage.workspace import Workspace

_BACKEND_OP_MAP: dict[str, str] = {
    "S3Backend": "s3",
    "R2Backend": "s3",
    "MemoryBackend": "memory",
    "LocalBackend": "local",
}

class FuseOpRegistry:

    def __init__(self) -> None:
        self._ops: list[tuple[str, FuseOp]] = []
        self._default: FuseOp = FUSE_OPS["default"]

    def register(self, pattern: str, op: FuseOp) -> None:
        self._ops.append((pattern, op))

    def set_default(self, op: FuseOp) -> None:
        self._default = op

    def resolve(self,
                path: str,
                size: int,
                ws: Workspace | None = None) -> FuseOp:
        basename = path.rsplit("/", 1)[-1] if "/" in path else path
        for pattern, op in self._ops:
            if fnmatch.fnmatch(basename, pattern):
                return op
        if ws is not None:
            try:
                backend, _, _ = ws._registry.resolve(path)
                backend_name = type(backend).__name__
                op_key = _BACKEND_OP_MAP.get(backend_name)
                if op_key and op_key in FUSE_OPS:
                    return FUSE_OPS[op_key]
            except (ValueError, FileNotFoundError):
                pass
        return self._default

    def list_ops(self) -> list[dict]:
        ops = [{"pattern": p, "name": o.name} for p, o in self._ops]
        ops.append({"pattern": "*", "name": self._default.name})
        return ops

-from mirage.fuse_ops import FUSE_OPS, FuseOp

+from mirage.fuse import FUSE_OPS, FuseOp


The file /Users/agent/strukto/mirage/mirage/fuse/loader.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

import importlib
import importlib.util
from typing import Any

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

def load_fuse_op_class(spec: str) -> type[Any]:
    if ":" not in spec:
        raise ValueError(
            f"invalid fuse op spec {spec!r}, expected 'source:ClassName'")

    source, class_name = spec.rsplit(":", 1)

    if "/" in source or source.endswith(".py"):
        module_spec = importlib.util.spec_from_file_location(
            "_mirage_user_fuse_op", source)
        if module_spec is None or module_spec.loader is None:
            raise ValueError(f"cannot load script {source!r}")
        module = importlib.util.module_from_spec(module_spec)
        try:
            module_spec.loader.exec_module(module)
        except FileNotFoundError:
            raise ValueError(f"cannot load script {source!r}")
    else:
        module = importlib.import_module(source)

    if not hasattr(module, class_name):
        raise ValueError(f"{class_name!r} not found in {source!r}")

    return getattr(module, class_name)

def build_registry_from_config(fuse_ops: dict) -> FuseOpRegistry:
    registry = FuseOpRegistry()
    for pattern, value in fuse_ops.items():
        if isinstance(value, str):
            if value in FUSE_OPS:
                registry.register(pattern, FUSE_OPS[value])
            else:
                cls = load_fuse_op_class(value)
                registry.register(pattern, cls())
        elif isinstance(value, dict):
            handler = value.get("handler", "")
            if handler in FUSE_OPS:
                registry.register(pattern, FUSE_OPS[handler])
            else:
                cls = load_fuse_op_class(handler)
                args = value.get("args", {})
                registry.register(pattern, cls(**args))
    return registry

-from mirage.fuse_ops.registry import FuseOpRegistry

+from mirage.fuse.registry import FuseOpRegistry