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

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     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:
    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 #cached|#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.CACHED),
   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 = ("#cached", "#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.backend.network.proxy import ProxyBackend
   205→    from mirage.fuse.fs import MirageFS
   206→    from mirage.fuse.fs import mount as fuse_mount_fn
   207→    from mirage.fuse.remote.remote import RemoteFuseOp
   208→
   209→    if not mountpoint:
   210→        raise typer.BadParameter("--mountpoint is required when using --fuse")
   211→    if not specs and not config:
   212→        raise typer.BadParameter(
   213→            "No backends specified. Use --backend URI or provide mount specs.")
   214→    for spec in specs:
   215→        if ":" not in spec:
   216→            raise typer.BadParameter(
   217→                f"Invalid mount spec: {spec!r}. Use backend:/path")
   218→
   219→    Path(mountpoint).mkdir(parents=True, exist_ok=True)
   220→    _ensure_server()
   221→    cfg = load_workspace_yaml(config) if config else {}
   222→
   223→    if runtime == RuntimeType.REMOTE:
   224→        url = cfg.get("url", "")
   225→        ws_id_remote = cfg.get("ws_id", "")
   226→        proxy = ProxyBackend(url, ws_id_remote)
   227→        ws = Workspace({"/": proxy}, mode=MountMode.WRITE)
   228→        fuse_registry = FuseOpRegistry()
   229→        fuse_registry.set_default(RemoteFuseOp(proxy))
   230→        fs = MirageFS(ws, agent_id=agent_id, fuse_registry=fuse_registry)
   231→    else:
   232→        mounts, mode, consistency, ttl, agent_id = _parse_mounts_for_fuse(
   233→            specs, cfg, mode, consistency, ttl, agent_id)
   234→        ws = Workspace(mounts,
   235→                       mode=mode,
   236→                       consistency=consistency,
   237→                       ttl_seconds=ttl)
   238→        fuse_ops_config = cfg.get("fuse_ops", {}) if cfg else {}
   239→        fuse_registry = build_registry_from_config(fuse_ops_config)
   240→        fs = MirageFS(ws, agent_id=agent_id, fuse_registry=fuse_registry)
   241→
   242→    pid = os.getpid()
   243→    _client().register_fuse(mountpoint, pid, specs, runtime=runtime.value)
   244→
   245→    thread = threading.Thread(target=_report_loop,
   246→                              args=(ws, mountpoint, fs),
   247→                              daemon=True)
   248→    thread.start()
   249→
   250→    fuse_mount_fn(mountpoint=mountpoint, foreground=True, fs=fs)
   251→
   252→
   253→def exec_cmd(
   254→        ws_id: str = typer.Argument(),
   255→        command: list[str] = typer.Argument(),
   256→) -> None:
   257→    """Execute a command in a running workspace."""
   258→    cmd_str = " ".join(command)
   259→    try:
   260→        output = _client().exec(ws_id, cmd_str)
   261→        if output:
   262→            print(output)
   263→    except ClientError as e:
   264→        print(f"Error: {e}", file=sys.stderr)
   265→        raise typer.Exit(1)
   266→
   267→
   268→def kill(ws_id: str = typer.Argument()) -> None:
   269→    """Stop a workspace daemon."""
   270→    _client().delete_workspace(ws_id)
   271→    print(f"Killed workspace {ws_id}")
   272→
   273→
   274→def sync(ws_id: str = typer.Argument()) -> None:
   275→    """Flush dirty inodes to backends for a workspace."""
   276→    try:
   277→        results = _client().sync(ws_id)
   278→        if results:
   279→            for r in results:
   280→                print(f"{r['path']}: {r['status']}")
   281→        else:
   282→            print("All clean.")
   283→    except ClientError as e:
   284→        print(f"Error: {e}", file=sys.stderr)
   285→        raise typer.Exit(1)
   286→
   287→
   288→def status(ws_id: str = typer.Argument()) -> None:
   289→    """Show dirty/clean inode status for a workspace."""
   290→    try:
   291→        inodes = _client().status(ws_id)
   292→        if inodes:
   293→            for i in inodes:
   294→                marker = "D" if i["dirty"] else " "
   295→                print(f"{marker} {i['path']}")
   296→        else:
   297→            print("No tracked files.")
   298→    except ClientError as e:
   299→        print(f"Error: {e}", file=sys.stderr)
   300→        raise typer.Exit(1)
   301→
   302→
   303→@workspace_app.command(name="ls")
   304→def workspace_ls() -> None:
   305→    """List active workspaces."""
   306→    _ensure_server()
   307→    workspaces = _client().list_workspaces()
   308→    if not workspaces:
   309→        print("No active workspaces.")
   310→        return
   311→    for w in workspaces:
   312→        parts = [w["id"], w.get("type", "workspace")]
   313→        if w.get("mountpoint"):
   314→            parts.append(w["mountpoint"])
   315→        print("\t".join(parts))
   316→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import os
     2→
     3→import yaml
     4→
     5→from mirage.backend.base import BaseBackend
     6→from mirage.backend.local.local import LocalBackend
     7→from mirage.backend.local.memory import MemoryBackend
     8→from mirage.types import BackendType, ConsistencyPolicy, MountMode
     9→
    10→
    11→def _build_backend(type_name: str, connection: str) -> BaseBackend:
    12→    if type_name == BackendType.MEMORY:
    13→        return MemoryBackend()
    14→    if type_name == BackendType.LOCAL:
    15→        return LocalBackend(root=os.path.abspath(connection))
    16→    if type_name == BackendType.S3:
    17→        from mirage.backend.object_storage.s3 import S3Backend, S3Config
    18→        rest = connection[len("s3://"):]
    19→        bucket = rest.split("/", 1)[0]
    20→        return S3Backend(S3Config(bucket=bucket))
    21→    if type_name == BackendType.R2:
    22→        from mirage.backend.object_storage.r2 import R2Backend, R2Config
    23→        rest = connection[len("r2://"):]
    24→        parts = rest.split("/", 1)
    25→        return R2Backend(
    26→            R2Config(
    27→                account_id=parts[0],
    28→                bucket=parts[1] if len(parts) > 1 else "",
    29→                access_key_id=os.environ.get("R2_ACCESS_KEY_ID", ""),
    30→                secret_access_key=os.environ.get("R2_SECRET_ACCESS_KEY", ""),
    31→            ))
    32→    if type_name in (BackendType.HTTP, BackendType.HTTPS):
    33→        from mirage.backend.network.http import HTTPBackend, HTTPConfig
    34→        return HTTPBackend(HTTPConfig(base_url=connection))
    35→    raise ValueError(f"Unknown backend type: {type_name!r}. "
    36→                     "Use built-ins: memory, local, s3, r2, http")
    37→
    38→
    39→def parse_mount_uri(uri: str) -> BaseBackend:
    40→    if uri == BackendType.MEMORY:
    41→        return MemoryBackend()
    42→    if uri == BackendType.LOCAL:
    43→        return LocalBackend(root=os.path.abspath("."))
    44→
    45→    colon = uri.find(":")
    46→    if colon > 0 and "//" not in uri[:colon + 3]:
    47→        return _build_backend(uri[:colon], uri[colon + 1:])
    48→
    49→    if uri.startswith("s3://"):
    50→        return _build_backend("s3", uri)
    51→    if uri.startswith("r2://"):
    52→        return _build_backend("r2", uri)
    53→    if uri.startswith("http://") or uri.startswith("https://"):
    54→        return _build_backend("http", uri)
    55→
    56→    if "://" in uri:
    57→        scheme = uri.split("://")[0]
    58→        raise ValueError(
    59→            f"Unknown URI scheme {scheme!r}. "
    60→            "Accepted: s3://, r2://, https://. "
    61→            "For simple backends use: memory, local, local:./path")
    62→    return LocalBackend(root=os.path.abspath(uri))
    63→
    64→
    65→def split_mount_spec(spec: str) -> tuple[str, str]:
    66→    """Split 'backend:/path' into (backend, path).
    67→
    68→    Args:
    69→        spec (str): Mount spec like 'memory:/data/' or 's3://bucket:/data/'.
    70→
    71→    Returns:
    72→        tuple[str, str]: (backend, mount_path) pair.
    73→    """
    74→    i = len(spec) - 1
    75→    while i >= 0:
    76→        if spec[i] == ":" and i + 1 < len(spec) and spec[i + 1] == "/":
    77→            if i >= 2 and spec[i - 1:i + 2] == "://":
    78→                i -= 1
    79→                continue
    80→            return spec[:i], spec[i + 1:]
    81→        i -= 1
    82→    raise ValueError(
    83→        f"Invalid mount spec: {spec!r}. "
    84→        "Use format: backend:/path (e.g. memory:/data/, s3://bucket:/)")
    85→
    86→
    87→_MODE_SUFFIXES = ("@read", "@write", "@exec")
    88→
    89→
    90→def parse_mount_uri_with_mode(
    91→    uri: str,
    92→    default_mode: MountMode = MountMode.READ,
    93→) -> tuple[BaseBackend, MountMode]:
    94→    for suffix in _MODE_SUFFIXES:
    95→        if uri.endswith(suffix):
    96→            return parse_mount_uri(uri[:-len(suffix)]), MountMode(suffix[1:])
    97→    return parse_mount_uri(uri), default_mode
    98→
    99→
   100→_CONSISTENCY_SUFFIXES = ("#cached", "#lazy", "#ttl", "#always")
   101→
   102→
   103→def parse_mount_uri_full(
   104→    uri: str,
   105→    default_mode: MountMode = MountMode.READ,
   106→    default_consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,
   107→) -> tuple[BaseBackend, MountMode, ConsistencyPolicy]:
   108→    consistency = default_consistency
   109→    for suffix in _CONSISTENCY_SUFFIXES:
   110→        if uri.endswith(suffix):
   111→            consistency = ConsistencyPolicy(suffix[1:])
   112→            uri = uri[:-len(suffix)]
   113→            break
   114→    backend, mode = parse_mount_uri_with_mode(uri, default_mode)
   115→    return backend, mode, consistency
   116→
   117→
   118→def parse_mounts_from_cli(
   119→    mount_specs: list[str],
   120→    default_mode: MountMode = MountMode.READ,
   121→) -> dict[str, BaseBackend | tuple[BaseBackend, MountMode]]:
   122→    mounts: dict[str, BaseBackend | tuple[BaseBackend, MountMode]] = {}
   123→    for spec in mount_specs:
   124→        uri, prefix = split_mount_spec(spec)
   125→        backend, mode = parse_mount_uri_with_mode(uri, default_mode)
   126→        if mode != default_mode:
   127→            mounts[prefix] = (backend, mode)
   128→        else:
   129→            mounts[prefix] = backend
   130→    return mounts
   131→
   132→
   133→def load_yaml_config(path: str) -> dict:
   134→    with open(path) as f:
   135→        return yaml.safe_load(f)
   136→
   137→
   138→def load_workspace_yaml(path: str) -> dict:
   139→    with open(path) as f:
   140→        data = yaml.safe_load(f)
   141→    mounts_raw = data.get("mounts", {})
   142→    mounts: dict[str, str | dict] = {}
   143→    for prefix, value in mounts_raw.items():
   144→        if isinstance(value, str):
   145→            mounts[prefix] = value
   146→        elif isinstance(value, dict):
   147→            backend_spec = value.get("backend")
   148→            if backend_spec:
   149→                entry: dict[str, str | dict] = {"backend": backend_spec}
   150→                if value.get("args"):
   151→                    entry["args"] = value["args"]
   152→                if value.get("mode"):
   153→                    entry["mode"] = value["mode"]
   154→                if value.get("consistency"):
   155→                    entry["consistency"] = value["consistency"]
   156→                mounts[prefix] = entry
   157→            else:
   158→                uri = value.get("uri", "")
   159→                mount_mode = value.get("mode")
   160→                mount_consistency = value.get("consistency")
   161→                entry_uri: dict[str, str] = {"uri": uri}
   162→                if mount_mode:
   163→                    entry_uri["mode"] = mount_mode
   164→                if mount_consistency:
   165→                    entry_uri["consistency"] = mount_consistency
   166→                if len(entry_uri) == 1:
   167→                    mounts[prefix] = uri
   168→                else:
   169→                    mounts[prefix] = entry_uri
   170→    result: dict = {"mounts": mounts, "commands": data.get("commands", {})}
   171→    if "consistency" in data:
   172→        result["consistency"] = data["consistency"]
   173→    if "ttl" in data:
   174→        result["ttl"] = data["ttl"]
   175→    if "agent_id" in data:
   176→        result["agent_id"] = data["agent_id"]
   177→    fuse_ops = data.get("fuse_ops", {})
   178→    if fuse_ops:
   179→        result["fuse_ops"] = fuse_ops
   180→    for key in ("runtime", "url", "ws_id", "command_url", "fuse_ops_url"):
   181→        if key in data:
   182→            result[key] = data[key]
   183→    return result
   184→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import typer
     2→
     3→from mirage.cli.config import split_mount_spec
     4→from mirage.cli.helpers import _client
     5→from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
     6→
     7→command_app = typer.Typer(no_args_is_help=True)
     8→command_server_app = typer.Typer(no_args_is_help=True)
     9→
    10→
    11→@command_app.command(name="add")
    12→def command_add(
    13→        ws_id: str = typer.Argument(),
    14→        name: str = typer.Argument(),
    15→        exec_str: str = typer.Argument(),
    16→        description: str = typer.Option(""),
    17→) -> None:
    18→    """Register a user CLI command in a workspace."""
    19→    _client().register_command(ws_id, name, exec_str, description)
    20→    print(f"registered: {name}")
    21→
    22→
    23→@command_server_app.command(name="serve")
    24→def command_server_serve(
    25→    mount_specs: list[str] | None = typer.Argument(default=None),
    26→    host: str = typer.Option("0.0.0.0"),
    27→    port: int = typer.Option(8765),
    28→    transport: str = typer.Option("http", help="http or grpc"),
    29→    auth: str = typer.Option("none", help="none, bearer, basic, or apikey"),
    30→    token: str | None = typer.Option(None,
    31→                                     help="Bearer token (--auth bearer)"),
    32→    api_key: str | None = typer.Option(None, help="API key (--auth apikey)"),
    33→    api_key_header: str = typer.Option("X-API-Key"),
    34→    username: str | None = typer.Option(None, help="Username (--auth basic)"),
    35→    password: str | None = typer.Option(None, help="Password (--auth basic)"),
    36→    mode: MountMode = typer.Option(MountMode.READ),
    37→    config: str | None = typer.Option(None),
    38→    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
    39→                                        help="Runtime: memory or local"),
    40→) -> None:
    41→    """Start a standalone command server backed by the given mounts."""
    42→    from mirage.backend.loader import load_backend_class
    43→    from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
    44→                                   parse_mount_uri_full)
    45→    from mirage.commands.remote.auth import (APIKeyAuth, BasicAuth,
    46→                                             BearerTokenAuth, NoAuth)
    47→    from mirage.workspace import Workspace
    48→
    49→    default_mode = mode
    50→    mounts: dict = {}
    51→    if config:
    52→        cfg = load_workspace_yaml(config)
    53→        if "mounts" in cfg:
    54→            for prefix, value in cfg["mounts"].items():
    55→                if isinstance(value, str):
    56→                    backend_obj, m, _ = parse_mount_uri_full(
    57→                        value, default_mode, ConsistencyPolicy.CACHED)
    58→                elif isinstance(value, dict) and "backend" in value:
    59→                    cls = load_backend_class(value["backend"])
    60→                    backend_obj = cls(**(value.get("args") or {}))
    61→                    m = MountMode(
    62→                        value["mode"]) if "mode" in value else default_mode
    63→                elif isinstance(value, dict):
    64→                    backend_obj = parse_mount_uri(value.get("uri", ""))
    65→                    m = MountMode(
    66→                        value["mode"]) if "mode" in value else default_mode
    67→                else:
    68→                    continue
    69→                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.CACHED)
    70→
    71→    for spec in (mount_specs or []):
    72→        try:
    73→            uri, prefix = split_mount_spec(spec)
    74→        except ValueError as e:
    75→            raise typer.BadParameter(str(e))
    76→        backend_obj, m, _ = parse_mount_uri_full(uri, default_mode,
    77→                                                 ConsistencyPolicy.CACHED)
    78→        mounts[prefix] = (backend_obj, m, ConsistencyPolicy.CACHED)
    79→
    80→    if not mounts:
    81→        raise typer.BadParameter("No mounts specified.")
    82→
    83→    ws = Workspace(mounts, mode=default_mode)
    84→
    85→    if auth == "bearer":
    86→        if not token:
    87→            raise typer.BadParameter("--token required for bearer auth")
    88→        server_auth = BearerTokenAuth(token)
    89→    elif auth == "basic":
    90→        if not username or not password:
    91→            raise typer.BadParameter(
    92→                "--username and --password required for basic auth")
    93→        server_auth = BasicAuth(username, password)
    94→    elif auth == "apikey":
    95→        if not api_key:
    96→            raise typer.BadParameter("--api-key required for apikey auth")
    97→        server_auth = APIKeyAuth(api_key, api_key_header)
    98→    else:
    99→        server_auth = NoAuth()
   100→
   101→    if transport == "grpc":
   102→        from mirage.commands.remote.grpc.server import serve
   103→        serve(ws, server_auth, host=host, port=port)
   104→    else:
   105→        import uvicorn
   106→
   107→        from mirage.commands.remote.fastapi.app import \
   108→            command_server_app as cs_app
   109→        from mirage.commands.remote.fastapi.app import configure
   110→        configure(ws, server_auth)
   111→        uvicorn.run(cs_app, host=host, port=port, log_level="warning")
   112→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→from datetime import datetime, timezone
     2→
     3→import pytest
     4→
     5→from mirage.backend.local.memory import MemoryBackend
     6→from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy
     7→from mirage.workspace import Workspace
     8→
     9→
    10→def _make_consistency_ws(consistency=ConsistencyPolicy.CACHED,
    11→                         ttl_seconds=300):
    12→    backend = MemoryBackend()
    13→    backend.put("/file.txt", b"original")
    14→    ws = Workspace(
    15→        {"/data/": backend},
    16→        mode=MountMode.WRITE,
    17→        consistency=consistency,
    18→        ttl_seconds=ttl_seconds,
    19→    )
    20→    return ws, backend
    21→
    22→
    23→def _make_ws(mode=MountMode.WRITE, sync_policy=SyncPolicy.STAGED):
    24→    backend = MemoryBackend()
    25→    return Workspace({"/data/": backend}, mode=mode,
    26→                     sync_policy=sync_policy), backend
    27→
    28→
    29→def test_consistency_none_caches_forever():
    30→    ws, backend = _make_consistency_ws(ConsistencyPolicy.CACHED)
    31→    assert ws.cat("/data/file.txt") == b"original"
    32→    backend._files["/file.txt"] = b"updated"
    33→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    34→    assert ws.cat("/data/file.txt") == b"original"
    35→
    36→
    37→def test_consistency_always_refetches():
    38→    ws, backend = _make_consistency_ws(ConsistencyPolicy.ALWAYS)
    39→    assert ws.cat("/data/file.txt") == b"original"
    40→    backend._files["/file.txt"] = b"updated"
    41→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    42→    assert ws.cat("/data/file.txt") == b"updated"
    43→
    44→
    45→def test_consistency_ttl_fresh_within_window():
    46→    ws, backend = _make_consistency_ws(ConsistencyPolicy.TTL, ttl_seconds=300)
    47→    assert ws.cat("/data/file.txt") == b"original"
    48→    backend._files["/file.txt"] = b"updated"
    49→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    50→    assert ws.cat("/data/file.txt") == b"original"
    51→
    52→
    53→def test_consistency_ttl_stale_after_expiry():
    54→    ws, backend = _make_consistency_ws(ConsistencyPolicy.TTL, ttl_seconds=10)
    55→    assert ws.cat("/data/file.txt") == b"original"
    56→    backend._files["/file.txt"] = b"updated"
    57→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    58→    entry = ws._cache.get("/data/file.txt")
    59→    entry.cached_at -= 20
    60→    assert ws.cat("/data/file.txt") == b"updated"
    61→
    62→
    63→def test_consistency_lazy_unchanged():
    64→    ws, backend = _make_consistency_ws(ConsistencyPolicy.LAZY)
    65→    assert ws.cat("/data/file.txt") == b"original"
    66→    assert ws.cat("/data/file.txt") == b"original"
    67→
    68→
    69→def test_consistency_lazy_changed():
    70→    ws, backend = _make_consistency_ws(ConsistencyPolicy.LAZY)
    71→    assert ws.cat("/data/file.txt") == b"original"
    72→    backend._files["/file.txt"] = b"updated-content"
    73→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    74→    assert ws.cat("/data/file.txt") == b"updated-content"
    75→
    76→
    77→def test_dirty_file_skips_revalidation():
    78→    ws, backend = _make_consistency_ws(ConsistencyPolicy.ALWAYS)
    79→    ws.write("/data/file.txt", b"local-edit")
    80→    backend._files["/file.txt"] = b"remote-change"
    81→    assert ws.cat("/data/file.txt") == b"local-edit"
    82→
    83→
    84→def test_per_mount_consistency():
    85→    be_none = MemoryBackend()
    86→    be_none.put("/f.txt", b"v1")
    87→    be_always = MemoryBackend()
    88→    be_always.put("/f.txt", b"v1")
    89→    ws = Workspace(
    90→        {
    91→            "/a/": be_none,
    92→            "/b/": (be_always, MountMode.READ, ConsistencyPolicy.ALWAYS),
    93→        },
    94→        mode=MountMode.READ,
    95→        consistency=ConsistencyPolicy.CACHED,
    96→    )
    97→    assert ws.cat("/a/f.txt") == b"v1"
    98→    assert ws.cat("/b/f.txt") == b"v1"
    99→    be_none._files["/f.txt"] = b"v2"
   100→    be_none._modified["/f.txt"] = datetime.now(timezone.utc).isoformat()
   101→    be_always._files["/f.txt"] = b"v2"
   102→    be_always._modified["/f.txt"] = datetime.now(timezone.utc).isoformat()
   103→    assert ws.cat("/a/f.txt") == b"v1"
   104→    assert ws.cat("/b/f.txt") == b"v2"
   105→
   106→
   107→def test_write_and_cat():
   108→    ws, _ = _make_ws()
   109→    ws.write("/data/hello.txt", b"hello world")
   110→    assert ws.cat("/data/hello.txt") == b"hello world"
   111→
   112→
   113→def test_write_marks_dirty():
   114→    ws, _ = _make_ws()
   115→    ws.write("/data/file.txt", b"x")
   116→    assert any(i.virtual_path == "/data/file.txt" and i.dirty
   117→               for i in ws.status())
   118→
   119→
   120→def test_rm_staged_only():
   121→    ws, _ = _make_ws()
   122→    ws.write("/data/new.txt", b"staged")
   123→    ws.rm("/data/new.txt")
   124→    assert not ws.exists("/data/new.txt")
   125→
   126→
   127→def test_rm_persisted():
   128→    ws, backend = _make_ws()
   129→    backend.put("/remote.txt", b"remote")
   130→    ws.rm("/data/remote.txt")
   131→    assert not ws.exists("/data/remote.txt")
   132→
   133→
   134→def test_mv_staged_only():
   135→    ws, _ = _make_ws()
   136→    ws.write("/data/a.txt", b"content")
   137→    ws.mv("/data/a.txt", "/data/b.txt")
   138→    assert ws.cat("/data/b.txt") == b"content"
   139→    assert not ws.exists("/data/a.txt")
   140→
   141→
   142→def test_cp_creates_copy():
   143→    ws, backend = _make_ws()
   144→    backend.put("/src.txt", b"original")
   145→    ws.cp("/data/src.txt", "/data/copy.txt")
   146→    assert ws.cat("/data/copy.txt") == b"original"
   147→
   148→
   149→def test_write_read_only_raises():
   150→    ws, _ = _make_ws(mode=MountMode.READ)
   151→    with pytest.raises(PermissionError):
   152→        ws.write("/data/x.txt", b"x")
   153→
   154→
   155→def test_sync_staged_writes_to_backend():
   156→    ws, backend = _make_ws(sync_policy=SyncPolicy.STAGED)
   157→    ws.write("/data/out.txt", b"synced")
   158→    results = ws.sync()
   159→    assert any(r.path == "/data/out.txt" and r.status == "ok" for r in results)
   160→    assert backend.cat("/out.txt") == b"synced"
   161→
   162→
   163→def test_sync_immediate_writes_through():
   164→    ws, backend = _make_ws(sync_policy=SyncPolicy.IMMEDIATE)
   165→    ws.write("/data/live.txt", b"live data")
   166→    assert backend.cat("/live.txt") == b"live data"
   167→    assert not any(i.dirty for i in ws.status())
   168→
   169→
   170→def test_reset_clears_dirty():
   171→    ws, _ = _make_ws()
   172→    ws.write("/data/x.txt", b"x")
   173→    ws.reset()
   174→    assert ws.status() == []
   175→
   176→
   177→def test_stat_staged_file():
   178→    ws, _ = _make_ws()
   179→    ws.write("/data/stat.txt", b"hello")
   180→    stat = ws.stat("/data/stat.txt")
   181→    assert stat.size == 5
   182→
   183→
   184→def test_exists_staged_file():
   185→    ws, _ = _make_ws()
   186→    ws.write("/data/exists.txt", b"y")
   187→    assert ws.exists("/data/exists.txt")
   188→
   189→
   190→def test_lock_paths_blocks_write():
   191→    ws = Workspace({"/data/": MemoryBackend()},
   192→                   mode=MountMode.WRITE,
   193→                   sync_policy=SyncPolicy.IMMEDIATE)
   194→    ws.write("/data/file.txt", b"hello")
   195→    with ws.lock_paths(["/data/file.txt"]):
   196→        with pytest.raises(PermissionError, match="locked"):
   197→            ws.write("/data/file.txt", b"world")
   198→
   199→
   200→def test_lock_paths_unlocks_on_exit():
   201→    ws = Workspace({"/data/": MemoryBackend()},
   202→                   mode=MountMode.WRITE,
   203→                   sync_policy=SyncPolicy.IMMEDIATE)
   204→    ws.write("/data/file.txt", b"hello")
   205→    with ws.lock_paths(["/data/file.txt"]):
   206→        pass
   207→    ws.write("/data/file.txt", b"world")
   208→    assert ws.cat("/data/file.txt") == b"world"
   209→
   210→
   211→def test_lock_paths_unlocks_on_exception():
   212→    ws = Workspace({"/data/": MemoryBackend()},
   213→                   mode=MountMode.WRITE,
   214→                   sync_policy=SyncPolicy.IMMEDIATE)
   215→    ws.write("/data/file.txt", b"hello")
   216→    with pytest.raises(RuntimeError):
   217→        with ws.lock_paths(["/data/file.txt"]):
   218→            raise RuntimeError("boom")
   219→    ws.write("/data/file.txt", b"world")
   220→    assert ws.cat("/data/file.txt") == b"world"
   221→
   222→
   223→def test_lock_allows_read():
   224→    ws = Workspace({"/data/": MemoryBackend()},
   225→                   mode=MountMode.WRITE,
   226→                   sync_policy=SyncPolicy.IMMEDIATE)
   227→    ws.write("/data/file.txt", b"hello")
   228→    with ws.lock_paths(["/data/file.txt"]):
   229→        assert ws.cat("/data/file.txt") == b"hello"
   230→
   231→
   232→def test_lock_multiple_paths():
   233→    ws = Workspace({"/data/": MemoryBackend()},
   234→                   mode=MountMode.WRITE,
   235→                   sync_policy=SyncPolicy.IMMEDIATE)
   236→    ws.write("/data/a.txt", b"a")
   237→    ws.write("/data/b.txt", b"b")
   238→    with ws.lock_paths(["/data/a.txt", "/data/b.txt"]):
   239→        with pytest.raises(PermissionError, match="locked"):
   240→            ws.write("/data/a.txt", b"x")
   241→        with pytest.raises(PermissionError, match="locked"):
   242→            ws.write("/data/b.txt", b"x")
   243→
   244→
   245→def test_unlock_paths_force():
   246→    ws = Workspace({"/data/": MemoryBackend()},
   247→                   mode=MountMode.WRITE,
   248→                   sync_policy=SyncPolicy.IMMEDIATE)
   249→    ws.write("/data/file.txt", b"hello")
   250→    ws._lock_path("/data/file.txt")
   251→    with pytest.raises(PermissionError, match="locked"):
   252→        ws.write("/data/file.txt", b"world")
   253→    ws.unlock_paths(["/data/file.txt"])
   254→    ws.write("/data/file.txt", b"world")
   255→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     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.CACHED,
    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.fs.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.fs.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→
    81→@pytest.mark.skip(reason="requires live server")
    82→def test_mount_exec_kill():
    83→    tmpdir = tempfile.mkdtemp(dir="/tmp")
    84→    env = {"MIRAGE_HOME": tmpdir}
    85→
    86→    result = _run(["mount", "memory:/data/"], env=env)
    87→    ws_id = result.stdout.strip()
    88→    assert len(ws_id) == 8
    89→
    90→    time.sleep(0.3)
    91→
    92→    result = _run(["exec", ws_id, "ls", "/data"], env=env)
    93→    assert result.returncode == 0
    94→
    95→    result = _run(["workspace", "ls"], env=env)
    96→    assert ws_id in result.stdout
    97→
    98→    _run(["kill", ws_id], env=env)
    99→    time.sleep(0.3)
   100→
   101→    result = _run(["workspace", "ls"], env=env, check=False)
   102→    assert ws_id not in result.stdout
   103→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import pytest
     2→
     3→from mirage.backend.local.local import LocalBackend
     4→from mirage.backend.local.memory import MemoryBackend
     5→from mirage.cli.config import (load_workspace_yaml, load_yaml_config,
     6→                               parse_mount_uri, parse_mount_uri_full,
     7→                               parse_mount_uri_with_mode,
     8→                               parse_mounts_from_cli, split_mount_spec)
     9→from mirage.types import ConsistencyPolicy, MountMode
    10→
    11→
    12→def test_parse_memory_uri():
    13→    backend = parse_mount_uri("memory")
    14→    assert isinstance(backend, MemoryBackend)
    15→
    16→
    17→def test_parse_local_uri():
    18→    backend = parse_mount_uri("local:./scratch")
    19→    assert isinstance(backend, LocalBackend)
    20→
    21→
    22→def test_parse_local_uri_relative_path():
    23→    backend = parse_mount_uri("./scratch")
    24→    assert isinstance(backend, LocalBackend)
    25→
    26→
    27→def test_parse_local_uri_absolute_path():
    28→    backend = parse_mount_uri("/tmp/mydir")
    29→    assert isinstance(backend, LocalBackend)
    30→
    31→
    32→def test_parse_unknown_scheme_raises():
    33→    with pytest.raises(ValueError, match="Unknown URI scheme"):
    34→        parse_mount_uri("memory://")
    35→
    36→
    37→def test_parse_mount_cli_args():
    38→    mounts = parse_mounts_from_cli(
    39→        ["memory:/data/", "local:./scratch:/local/"])
    40→    assert "/data/" in mounts
    41→    assert "/local/" in mounts
    42→    assert isinstance(mounts["/data/"], MemoryBackend)
    43→    assert isinstance(mounts["/local/"], LocalBackend)
    44→
    45→
    46→def test_load_yaml_config(tmp_path):
    47→    cfg_file = tmp_path / "ws.yaml"
    48→    cfg_file.write_text(
    49→        "mounts:\n  /data/: memory\n  /local/: local:./scratch\n"
    50→        "sync_policy: immediate\nmode: write\n")
    51→    cfg = load_yaml_config(str(cfg_file))
    52→    assert "/data/" in cfg["mounts"]
    53→    assert cfg["sync_policy"] == "immediate"
    54→    assert cfg["mode"] == "write"
    55→
    56→
    57→def test_parse_mount_uri_with_mode_read():
    58→    backend, mode = parse_mount_uri_with_mode("memory@read")
    59→    assert isinstance(backend, MemoryBackend)
    60→    assert mode == MountMode.READ
    61→
    62→
    63→def test_parse_mount_uri_with_mode_write():
    64→    backend, mode = parse_mount_uri_with_mode("memory@write")
    65→    assert isinstance(backend, MemoryBackend)
    66→    assert mode == MountMode.WRITE
    67→
    68→
    69→def test_parse_mount_uri_with_mode_default():
    70→    backend, mode = parse_mount_uri_with_mode("memory")
    71→    assert isinstance(backend, MemoryBackend)
    72→    assert mode == MountMode.READ
    73→
    74→
    75→def test_parse_mounts_from_cli_per_mount_mode():
    76→    mounts = parse_mounts_from_cli(
    77→        ["memory@write:/a/", "memory:/b/"],
    78→        default_mode=MountMode.READ,
    79→    )
    80→    assert isinstance(mounts["/a/"], tuple)
    81→    backend, mode = mounts["/a/"]
    82→    assert isinstance(backend, MemoryBackend)
    83→    assert mode == MountMode.WRITE
    84→    assert isinstance(mounts["/b/"], MemoryBackend)
    85→
    86→
    87→def test_split_mount_spec_memory():
    88→    backend, path = split_mount_spec("memory:/data/")
    89→    assert backend == "memory"
    90→    assert path == "/data/"
    91→
    92→
    93→def test_split_mount_spec_s3():
    94→    backend, path = split_mount_spec("s3://bucket:/")
    95→    assert backend == "s3://bucket"
    96→    assert path == "/"
    97→
    98→
    99→def test_split_mount_spec_with_mode():
   100→    backend, path = split_mount_spec("memory@write:/a/")
   101→    assert backend == "memory@write"
   102→    assert path == "/a/"
   103→
   104→
   105→def test_split_mount_spec_invalid():
   106→    with pytest.raises(ValueError, match="Invalid mount spec"):
   107→        split_mount_spec("nocolon")
   108→
   109→
   110→def test_load_workspace_yaml_per_mount_mode(tmp_path):
   111→    cfg_file = tmp_path / "ws.yaml"
   112→    cfg_file.write_text("mounts:\n"
   113→                        "  /data/: memory\n"
   114→                        "  /scratch/:\n"
   115→                        "    uri: memory\n"
   116→                        "    mode: write\n")
   117→    cfg = load_workspace_yaml(str(cfg_file))
   118→    assert cfg["mounts"]["/data/"] == "memory"
   119→    assert cfg["mounts"]["/scratch/"] == {"uri": "memory", "mode": "write"}
   120→
   121→
   122→def test_parse_mount_uri_full_lazy():
   123→    backend, mode, consistency = parse_mount_uri_full("memory@write#lazy")
   124→    assert isinstance(backend, MemoryBackend)
   125→    assert mode == MountMode.WRITE
   126→    assert consistency == ConsistencyPolicy.LAZY
   127→
   128→
   129→def test_parse_mount_uri_full_default():
   130→    backend, mode, consistency = parse_mount_uri_full("memory")
   131→    assert isinstance(backend, MemoryBackend)
   132→    assert mode == MountMode.READ
   133→    assert consistency == ConsistencyPolicy.CACHED
   134→
   135→
   136→def test_load_workspace_yaml_consistency(tmp_path):
   137→    cfg_file = tmp_path / "ws.yaml"
   138→    cfg_file.write_text("consistency: lazy\n"
   139→                        "ttl: 60\n"
   140→                        "mounts:\n"
   141→                        "  /data/: memory\n"
   142→                        "  /scratch/:\n"
   143→                        "    uri: memory\n"
   144→                        "    consistency: always\n")
   145→    cfg = load_workspace_yaml(str(cfg_file))
   146→    assert cfg["consistency"] == "lazy"
   147→    assert cfg["ttl"] == 60
   148→    assert cfg["mounts"]["/data/"] == "memory"
   149→    assert cfg["mounts"]["/scratch/"] == {
   150→        "uri": "memory",
   151→        "consistency": "always",
   152→    }
   153→
   154→
   155→def test_load_workspace_yaml_custom_backend(tmp_path):
   156→    cfg_file = tmp_path / "ws.yaml"
   157→    cfg_file.write_text("mounts:\n"
   158→                        "  /data/:\n"
   159→                        "    backend: './my_backend.py:MyBackend'\n"
   160→                        "    args:\n"
   161→                        "      bucket: my-bucket\n"
   162→                        "    mode: write\n"
   163→                        "    consistency: lazy\n")
   164→    cfg = load_workspace_yaml(str(cfg_file))
   165→    mount = cfg["mounts"]["/data/"]
   166→    assert mount["backend"] == "./my_backend.py:MyBackend"
   167→    assert mount["args"] == {"bucket": "my-bucket"}
   168→    assert mount["mode"] == "write"
   169→    assert mount["consistency"] == "lazy"
   170→
   171→
   172→def test_load_workspace_yaml_custom_backend_minimal(tmp_path):
   173→    cfg_file = tmp_path / "ws.yaml"
   174→    cfg_file.write_text("mounts:\n"
   175→                        "  /data/:\n"
   176→                        "    backend: 'mypackage:MyBackend'\n")
   177→    cfg = load_workspace_yaml(str(cfg_file))
   178→    mount = cfg["mounts"]["/data/"]
   179→    assert mount == {"backend": "mypackage:MyBackend"}
   180→
   181→
   182→def test_load_workspace_yaml_with_fuse_ops(tmp_path):
   183→    cfg_file = tmp_path / "ws.yaml"
   184→    cfg_file.write_text("mounts:\n"
   185→                        "  /data/: memory\n"
   186→                        "fuse_ops:\n"
   187→                        "  '*.parquet': parquet\n"
   188→                        "  '*.csv': csv\n")
   189→    cfg = load_workspace_yaml(str(cfg_file))
   190→    assert "fuse_ops" in cfg
   191→    assert cfg["fuse_ops"]["*.parquet"] == "parquet"
   192→    assert cfg["fuse_ops"]["*.csv"] == "csv"
   193→
   194→
   195→def test_load_workspace_yaml_no_fuse_ops(tmp_path):
   196→    cfg_file = tmp_path / "ws.yaml"
   197→    cfg_file.write_text("mounts:\n  /data/: memory\n")
   198→    cfg = load_workspace_yaml(str(cfg_file))
   199→    assert "fuse_ops" not in cfg
   200→
   201→
   202→def test_load_workspace_yaml_remote(tmp_path):
   203→    cfg_file = tmp_path / "ws.yaml"
   204→    cfg_file.write_text("runtime: remote\n"
   205→                        "url: http://server:8000\n"
   206→                        "ws_id: abc12345\n"
   207→                        "mounts: {}\n")
   208→    cfg = load_workspace_yaml(str(cfg_file))
   209→    assert cfg["runtime"] == "remote"
   210→    assert cfg["url"] == "http://server:8000"
   211→    assert cfg["ws_id"] == "abc12345"
   212→
   213→
   214→def test_load_workspace_yaml_remote_with_command_url(tmp_path):
   215→    cfg_file = tmp_path / "ws.yaml"
   216→    cfg_file.write_text("runtime: remote\n"
   217→                        "url: http://data:8000\n"
   218→                        "ws_id: abc12345\n"
   219→                        "command_url: http://cmd:8001\n"
   220→                        "fuse_ops_url: http://ops:8002\n"
   221→                        "mounts: {}\n")
   222→    cfg = load_workspace_yaml(str(cfg_file))
   223→    assert cfg["runtime"] == "remote"
   224→    assert cfg["url"] == "http://data:8000"
   225→    assert cfg["command_url"] == "http://cmd:8001"
   226→    assert cfg["fuse_ops_url"] == "http://ops:8002"
   227→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→from mirage.types import FileStat
     2→from mirage.workspace.cache.entry import CacheEntry
     3→from mirage.workspace.cache.policy import metadata_matches
     4→
     5→
     6→def test_metadata_matches_etag_priority():
     7→    entry = CacheEntry(data=b"x",
     8→                       cached_at=0,
     9→                       etag="abc",
    10→                       modified="old",
    11→                       size=1)
    12→    remote = FileStat(name="f",
    13→                      size=999,
    14→                      modified="new",
    15→                      extra={"etag": "abc"})
    16→    assert metadata_matches(entry, remote) is True
    17→
    18→
    19→def test_metadata_matches_fallback_to_modified():
    20→    entry = CacheEntry(data=b"x", cached_at=0, modified="2024-01-01", size=1)
    21→    remote = FileStat(name="f", size=999, modified="2024-01-01")
    22→    assert metadata_matches(entry, remote) is True
    23→    remote2 = FileStat(name="f", size=999, modified="2024-02-01")
    24→    assert metadata_matches(entry, remote2) is False
    25→
    26→
    27→def test_metadata_matches_fallback_to_size():
    28→    entry = CacheEntry(data=b"x", cached_at=0, size=100)
    29→    remote = FileStat(name="f", size=100)
    30→    assert metadata_matches(entry, remote) is True
    31→    remote2 = FileStat(name="f", size=200)
    32→    assert metadata_matches(entry, remote2) is False
    33→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


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


Write

import time
from enum import Enum

from mirage.backend.base import BaseBackend
from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry

class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.CACHED:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False


File created successfully at: /Users/agent/strukto/mirage/mirage/workspace/consistency.py


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


Edit

class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

class RuntimeType(str, Enum):


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


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


Edit

from mirage.backend.local import LocalBackend, MemoryBackend
from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult)
from mirage.workspace import Workspace

from mirage.backend.local import LocalBackend, MemoryBackend
from mirage.types import FileStat, Inode, MountMode, SyncPolicy, SyncResult
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy


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


from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult, TransferStats)
from mirage.workspace.cache import LRUCache
from mirage.workspace.cache.policy import is_fresh
from mirage.workspace.mount import MountRegistry

from mirage.types import (FileStat, Inode, MountMode, SyncPolicy, SyncResult,
                          TransferStats)
from mirage.workspace.cache import LRUCache
from mirage.workspace.consistency import ConsistencyPolicy, is_fresh
from mirage.workspace.mount import MountRegistry


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


from mirage.backend.base import BaseBackend
from mirage.types import ConsistencyPolicy, MountMode

from mirage.backend.base import BaseBackend
from mirage.types import MountMode
from mirage.workspace.consistency import ConsistencyPolicy


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


from pydantic import BaseModel

from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy

from pydantic import BaseModel

from mirage.types import MountMode, SyncPolicy
from mirage.workspace.consistency import ConsistencyPolicy


The file /Users/agent/strukto/mirage/mirage/server/fastapi/config.py has been updated successfully.


from mirage.types import (CommandStats, ConsistencyPolicy, FileStat, MountMode,
                          RuntimeType, SyncPolicy)

from mirage.types import (CommandStats, FileStat, MountMode, RuntimeType,
                          SyncPolicy)
from mirage.workspace.consistency import ConsistencyPolicy


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


from mirage.types import ConsistencyPolicy, MountMode, RuntimeType

from mirage.types import MountMode, RuntimeType
from mirage.workspace.consistency import ConsistencyPolicy


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


from mirage.types import BackendType, ConsistencyPolicy, MountMode

from mirage.types import BackendType, MountMode
from mirage.workspace.consistency import ConsistencyPolicy


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


from mirage.types import ConsistencyPolicy, MountMode, RuntimeType

from mirage.types import MountMode, RuntimeType
from mirage.workspace.consistency import ConsistencyPolicy


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


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


Edit

from mirage.backend.local.memory import MemoryBackend
from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy
from mirage.workspace import Workspace

from mirage.backend.local.memory import MemoryBackend
from mirage.types import MountMode, SyncPolicy
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy


The file /Users/agent/strukto/mirage/tests/workspace/test_workspace.py has been updated successfully.


from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry
from mirage.workspace.cache.policy import metadata_matches

from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry
from mirage.workspace.consistency import metadata_matches


The file /Users/agent/strukto/mirage/tests/workspace/cache/test_policy.py has been updated successfully.


from mirage.types import ConsistencyPolicy, MountMode, RuntimeType

    from mirage.types import MountMode, RuntimeType
    from mirage.workspace.consistency import ConsistencyPolicy


The file /Users/agent/strukto/mirage/tests/cli/test_app.py has been updated successfully.


from mirage.types import ConsistencyPolicy, MountMode

from mirage.types import MountMode
from mirage.workspace.consistency import ConsistencyPolicy


The file /Users/agent/strukto/mirage/tests/cli/test_config.py has been updated successfully.


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


Bash

Remove old policy.py file via git rm


Error: Exit code 1
error: the following file has local modifications:
    mirage/workspace/cache/policy.py
(use --cached to keep the file, or -f to force removal)

error: the following file has local modifications:
    mirage/workspace/cache/policy.py
(use --cached to keep the file, or -f to force removal)


git rm -f mirage/workspace/cache/policy.py

Force remove old policy.py file via git rm


rm 'mirage/workspace/cache/policy.py'


TodoWrite

Read existing files to understand current state

Reading existing files to understand current state

Create mirage/workspace/consistency.py

Remove ConsistencyPolicy from mirage/types.py

Removing ConsistencyPolicy from mirage/types.py

Update source file imports

Update test imports

Delete old policy file

Run tests and verify all pass

Commit changes


Bash

uv run pytest tests/workspace/ tests/cli/ -q 2>&1

Run workspace and CLI tests


Error: Exit code 1
.......................................................FF.s............. [ 84%]
.............                                                            [100%]
=================================== FAILURES ===================================
____________________ test_mount_fuse_backend_mounts_at_root ____________________

    def test_mount_fuse_backend_mounts_at_root():
        """--backend memory with --fuse mounts the backend at / prefix."""
        from mirage.cli.commands.workspace import mount
        with tempfile.TemporaryDirectory() as mp:
>           with patch("mirage.fuse.fs.mount") as mock_fuse, \
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                 patch("mirage.cli.commands.workspace._ensure_server"), \
                 patch("mirage.cli.commands.workspace._client"):

tests/cli/test_app.py:50:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/unittest/mock.py:1467: in __enter__
    original, local = self.get_original()
                      ^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <unittest.mock._patch object at 0x108abd1f0>

    def get_original(self):
        target = self.getter()
        name = self.attribute

        original = DEFAULT
        local = False

        try:
            original = target.__dict__[name]
        except (AttributeError, KeyError):
            original = getattr(target, name, DEFAULT)
        else:
            local = True

        if name in _builtins and isinstance(target, ModuleType):
            self.create = True

        if not self.create and original is DEFAULT:
>           raise AttributeError(
                "%s does not have the attribute %r" % (target, name)
            )
E           AttributeError: <module 'mirage.fuse.fs' from '/Users/agent/strukto/mirage/mirage/fuse/fs.py'> does not have the attribute 'mount'

../../.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/unittest/mock.py:1437: AttributeError
___________________ test_mount_fuse_backend_with_extra_spec ____________________

    def test_mount_fuse_backend_with_extra_spec():
        """--backend combined with positional prefix:uri spec and --fuse."""
        from mirage.cli.commands.workspace import mount
        with tempfile.TemporaryDirectory() as mp:
>           with patch("mirage.fuse.fs.mount") as mock_fuse, \
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                 patch("mirage.cli.commands.workspace._ensure_server"), \
                 patch("mirage.cli.commands.workspace._client"):

tests/cli/test_app.py:63:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/unittest/mock.py:1467: in __enter__
    original, local = self.get_original()
                      ^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <unittest.mock._patch object at 0x108afa510>

    def get_original(self):
        target = self.getter()
        name = self.attribute

        original = DEFAULT
        local = False

        try:
            original = target.__dict__[name]
        except (AttributeError, KeyError):
            original = getattr(target, name, DEFAULT)
        else:
            local = True

        if name in _builtins and isinstance(target, ModuleType):
            self.create = True

        if not self.create and original is DEFAULT:
>           raise AttributeError(
                "%s does not have the attribute %r" % (target, name)
            )
E           AttributeError: <module 'mirage.fuse.fs' from '/Users/agent/strukto/mirage/mirage/fuse/fs.py'> does not have the attribute 'mount'

../../.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/unittest/mock.py:1437: AttributeError
================================ tests coverage ================================
______________ coverage: platform darwin, python 3.12.10-final-0 _______________

Name                                                    Stmts   Miss  Cover   Missing
-------------------------------------------------------------------------------------
mirage/__init__.py                                          5      0   100%
mirage/backend/__init__.py                                  4      0   100%
mirage/backend/base.py                                     81     43    47%   16, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 66, 70-77, 81, 87-89, 96-103, 107-115, 118
mirage/backend/filetype.py                                  6      0   100%
mirage/backend/loader.py                                   20     16    20%   20-42
mirage/backend/local/__init__.py                            3      0   100%
mirage/backend/local/local.py                              69     46    33%   26-3

... [19470 characters truncated] ...

t_storage/s3.py                    5      1    80%   9
mirage/fuse/base.py                                        34     25    26%   16, 20-24, 28-30, 33-48
mirage/fuse/filetype/__init__.py                            0      0   100%
mirage/fuse/filetype/csv.py                                26     16    38%   19, 23-29, 32-48
mirage/fuse/filetype/parquet.py                            33     23    30%   19, 23-25, 28-52
mirage/fuse/fs.py                                         237    196    17%   27-36, 39-45, 48, 60, 72-76, 79-87, 90, 93-110, 113-127, 130-147, 150-174, 177-191, 194-200, 203-209, 212-218, 221-227, 230, 243, 246, 249, 252, 255-263, 266, 269-286, 289-291, 294-305
mirage/fuse/loader.py                                      38     31    18%   10-32, 36-52
mirage/fuse/mount.py                                       29     29     0%   1-54
mirage/fuse/ops_log.py                                     32     21    34%   10-14, 20-22, 25-27, 36-40, 49-53
mirage/fuse/registry.py                                    32     21    34%   22-23, 26, 29, 35-48, 51-53
mirage/fuse/remote/__init__.py                              0      0   100%
mirage/fuse/remote/remote.py                               20     20     0%   1-38
mirage/observe/__init__.py                                  3      0   100%
mirage/observe/entries.py                                  56     10    82%   23-26, 69-74
mirage/observe/fastapi/__init__.py                          0      0   100%
mirage/observe/fastapi/router.py                           59     59     0%   1-98
mirage/observe/inspector.py                                78     60    23%   17-21, 28, 32-42, 51-65, 69-76, 88-104, 108-122, 138-139, 143-149, 161-177
mirage/observe/session_log.py                              46     29    37%   14-21, 25, 28, 31-34, 37-43, 46-50, 57-59
mirage/runtime/__init__.py                                  5      5     0%   1-6
mirage/runtime/accessors/__init__.py                        0      0   100%
mirage/runtime/accessors/base.py                            6      6     0%   1-29
mirage/runtime/accessors/local.py                          36     36     0%   1-59
mirage/runtime/accessors/remote.py                         40     40     0%   1-77
mirage/runtime/base.py                                     51     51     0%   1-102
mirage/runtime/local.py                                    11     11     0%   1-21
mirage/runtime/memory.py                                    7      7     0%   1-14
mirage/runtime/remote.py                                   47     47     0%   1-68
mirage/server/__init__.py                                   0      0   100%
mirage/server/constants.py                                  9      5    44%   9-14
mirage/server/fastapi/__init__.py                           0      0   100%
mirage/server/fastapi/app.py                               14     14     0%   1-18
mirage/server/fastapi/config.py                            61     61     0%   1-90
mirage/server/fastapi/daemon.py                            22     22     0%   1-31
mirage/server/fastapi/routers/__init__.py                   0      0   100%
mirage/server/fastapi/routers/command_router.py            65     65     0%   1-101
mirage/server/fastapi/routers/io_router.py                110    110     0%   1-139
mirage/server/fastapi/routers/workspace_router.py          60     60     0%   1-95
mirage/server/routes.py                                    27      0   100%
mirage/server/service/__init__.py                           0      0   100%
mirage/server/service/base.py                               7      7     0%   1-64
mirage/server/service/cache.py                             24     24     0%   1-33
mirage/server/service/local.py                            258    258     0%   1-427
mirage/types.py                                            86      0   100%
mirage/workspace/__init__.py                                2      0   100%
mirage/workspace/cache/__init__.py                          3      0   100%
mirage/workspace/cache/entry.py                             7      0   100%
mirage/workspace/cache/store.py                            45      3    93%   14, 60, 64
mirage/workspace/consistency.py                            32      3    91%   32-33, 45
mirage/workspace/mount.py                                  35      2    94%   59, 66
mirage/workspace/sync.py                                   56      0   100%
mirage/workspace/workspace.py                             235     59    75%   87, 90-91, 143-176, 179-180, 203-204, 207-208, 211-217, 220-221, 237-239, 265-272, 330
-------------------------------------------------------------------------------------
TOTAL                                                    4718   3619    23%
=========================== short test summary info ============================
FAILED tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root - Attrib...
FAILED tests/cli/test_app.py::test_mount_fuse_backend_with_extra_spec - Attri...


git stash && uv run pytest tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root tests/cli/test_app.py::test_mount_fuse_backend_with_extra_spec -q 2>&1 | tail -20

Check if these tests were already failing before our changes


Saved working directory and index state WIP on main: 38b0734 Update
mirage/server/routes.py                                    27      0   100%
mirage/server/service/__init__.py                           0      0   100%
mirage/server/service/base.py                               7      7     0%   1-64
mirage/server/service/cache.py                             24     24     0%   1-33
mirage/server/service/local.py                            260    260     0%   1-428
mirage/types.py                                            91      0   100%
mirage/workspace/__init__.py                              241    199    17%   18-19, 22, 25, 58-78, 88, 91-92, 97-141, 144-177, 180-181, 186-196, 199-203, 206-207, 210-211, 214-220, 223-224, 229-237, 240-242, 245-252, 255-275, 278-281, 286-298, 301, 304-305, 310, 313-314, 317-319, 322-323, 329-333, 336-339, 342-344, 352-367
mirage/workspace/cache/__init__.py                          3      0   100%
mirage/workspace/cache/entry.py                             7      0   100%
mirage/workspace/cache/policy.py                           26     20    23%   15-26, 30-37
mirage/workspace/cache/store.py                            45     30    33%   8-14, 20-22, 25-28, 36-47, 50-52, 55-56, 60, 64, 67-69
mirage/workspace/consistency.py                            32     32     0%   1-45
mirage/workspace/mount.py                                  34     24    29%   12-16, 29, 36-41, 50-56, 59-63, 66
mirage/workspace/sync.py                                   56     41    27%   20, 25-33, 43-50, 60-63, 72-93, 102, 105, 108, 111, 115
mirage/workspace/workspace.py                             235    235     0%   1-358
-------------------------------------------------------------------------------------
TOTAL                                                    5032   4365    13%
=========================== short test summary info ============================
FAILED tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root - Attrib...
FAILED tests/cli/test_app.py::test_mount_fuse_backend_with_extra_spec - Attri...


Restore our changes