     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, MountMode
     9→from mirage.workspace.consistency import ConsistencyPolicy
    10→
    11→
    12→def _build_backend(type_name: str, connection: str) -> BaseBackend:
    13→    if type_name == BackendType.MEMORY:
    14→        return MemoryBackend()
    15→    if type_name == BackendType.LOCAL:
    16→        return LocalBackend(root=os.path.abspath(connection))
    17→    if type_name == BackendType.S3:
    18→        from mirage.backend.object_storage.s3 import S3Backend, S3Config
    19→        rest = connection[len("s3://"):]
    20→        bucket = rest.split("/", 1)[0]
    21→        return S3Backend(S3Config(bucket=bucket))
    22→    if type_name == BackendType.R2:
    23→        from mirage.backend.object_storage.r2 import R2Backend, R2Config
    24→        rest = connection[len("r2://"):]
    25→        parts = rest.split("/", 1)
    26→        return R2Backend(
    27→            R2Config(
    28→                account_id=parts[0],
    29→                bucket=parts[1] if len(parts) > 1 else "",
    30→                access_key_id=os.environ.get("R2_ACCESS_KEY_ID", ""),
    31→                secret_access_key=os.environ.get("R2_SECRET_ACCESS_KEY", ""),
    32→            ))
    33→    if type_name in (BackendType.HTTP, BackendType.HTTPS):
    34→        from mirage.backend.network.http import HTTPBackend, HTTPConfig
    35→        return HTTPBackend(HTTPConfig(base_url=connection))
    36→    raise ValueError(f"Unknown backend type: {type_name!r}. "
    37→                     "Use built-ins: memory, local, s3, r2, http")
    38→
    39→
    40→def parse_mount_uri(uri: str) -> BaseBackend:
    41→    if uri == BackendType.MEMORY:
    42→        return MemoryBackend()
    43→    if uri == BackendType.LOCAL:
    44→        return LocalBackend(root=os.path.abspath("."))
    45→
    46→    colon = uri.find(":")
    47→    if colon > 0 and "//" not in uri[:colon + 3]:
    48→        return _build_backend(uri[:colon], uri[colon + 1:])
    49→
    50→    if uri.startswith("s3://"):
    51→        return _build_backend("s3", uri)
    52→    if uri.startswith("r2://"):
    53→        return _build_backend("r2", uri)
    54→    if uri.startswith("http://") or uri.startswith("https://"):
    55→        return _build_backend("http", uri)
    56→
    57→    if "://" in uri:
    58→        scheme = uri.split("://")[0]
    59→        raise ValueError(
    60→            f"Unknown URI scheme {scheme!r}. "
    61→            "Accepted: s3://, r2://, https://. "
    62→            "For simple backends use: memory, local, local:./path")
    63→    return LocalBackend(root=os.path.abspath(uri))
    64→
    65→
    66→def split_mount_spec(spec: str) -> tuple[str, str]:
    67→    """Split 'backend:/path' into (backend, path).
    68→
    69→    Args:
    70→        spec (str): Mount spec like 'memory:/data/' or 's3://bucket:/data/'.
    71→
    72→    Returns:
    73→        tuple[str, str]: (backend, mount_path) pair.
    74→    """
    75→    i = len(spec) - 1
    76→    while i >= 0:
    77→        if spec[i] == ":" and i + 1 < len(spec) and spec[i + 1] == "/":
    78→            if i >= 2 and spec[i - 1:i + 2] == "://":
    79→                i -= 1
    80→                continue
    81→            return spec[:i], spec[i + 1:]
    82→        i -= 1
    83→    raise ValueError(
    84→        f"Invalid mount spec: {spec!r}. "
    85→        "Use format: backend:/path (e.g. memory:/data/, s3://bucket:/)")
    86→
    87→
    88→_MODE_SUFFIXES = ("@read", "@write", "@exec")
    89→
    90→
    91→def parse_mount_uri_with_mode(
    92→    uri: str,
    93→    default_mode: MountMode = MountMode.READ,
    94→) -> tuple[BaseBackend, MountMode]:
    95→    for suffix in _MODE_SUFFIXES:
    96→        if uri.endswith(suffix):
    97→            return parse_mount_uri(uri[:-len(suffix)]), MountMode(suffix[1:])
    98→    return parse_mount_uri(uri), default_mode
    99→
   100→

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


mirage/commands/remote/fastapi/app.py:4:from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
mirage/commands/remote/fastapi/app.py:5:from mirage.commands.remote.auth import AuthError, NoAuth, ServerAuth
mirage/commands/remote/fastapi/app.py:6:from mirage.workspace import Workspace
mirage/commands/remote/client.py:7:from mirage.commands.remote.auth import ClientAuth, NoAuth
mirage/commands/remote/client.py:8:from mirage.commands.remote.grpc.proto import command_pb2, command_pb2_grpc
mirage/commands/remote/client.py:9:from mirage.types import CommandResult, FilePayload
mirage/commands/remote/grpc/proto/command_pb2_grpc.py:5:from mirage.commands.remote.grpc.proto import command_pb2 as command__pb2
mirage/commands/remote/grpc/server.py:5:from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
mirage/commands/remote/grpc/server.py:6:from mirage.commands.remote.auth import AuthError, NoAuth, ServerAuth
mirage/commands/remote/grpc/server.py:7:from mirage.commands.remote.grpc.proto import command_pb2, command_pb2_grpc
mirage/commands/remote/grpc/server.py:8:from mirage.workspace import Workspace
mirage/observe/entries.py:5:from mirage.types import RuntimeType
mirage/__init__.py:1:from mirage.backend.local import LocalBackend, MemoryBackend
mirage/__init__.py:2:from mirage.types import FileStat, Inode, MountMode, SyncPolicy, SyncResult
mirage/__init__.py:3:from mirage.workspace import Workspace
mirage/__init__.py:4:from mirage.workspace.consistency import ConsistencyPolicy
mirage/commands/builtin/read.py:6:from mirage.types import FileStat, FileType
mirage/commands/builtin/read.py:7:from mirage.workspace import Workspace
mirage/backend/base.py:3:from mirage.types import FileStat
mirage/observe/inspector.py:6:from mirage.backend.local.memory import MemoryBackend
mirage/observe/inspector.py:7:from mirage.commands import COMMAND_COST, COMMANDS
mirage/observe/inspector.py:8:from mirage.types import CostLevel
mirage/backend/__init__.py:1:from mirage.backend.base import BaseBackend
mirage/backend/__init__.py:2:from mirage.backend.local import LocalBackend, MemoryBackend
mirage/backend/__init__.py:3:from mirage.backend.network import ProxyBackend
mirage/observe/session_log.py:8:from mirage.observe.entries import CommandEntry, SessionEntry
mirage/backend/filetype.py:1:from mirage.types import FileType
mirage/workspace/workspace.py:5:from mirage.backend.base import BaseBackend
mirage/workspace/workspace.py:6:from mirage.backend.filetype import guess_type
mirage/workspace/workspace.py:7:from mirage.types import (FileStat, Inode, MountMode, SyncPolicy, SyncResult,
mirage/workspace/workspace.py:9:from mirage.workspace.cache import LRUCache
mirage/workspace/workspace.py:10:from mirage.workspace.consistency import ConsistencyPolicy, is_fresh
mirage/workspace/workspace.py:11:from mirage.workspace.mount import MountRegistry
mirage/workspace/workspace.py:12:from mirage.workspace.sync import DirtyTracker
mirage/cli/helpers.py:7:from mirage.cli.client import MirageClient
mirage/cli/helpers.py:8:from mirage.server.constants import DEFAULT_SOCKET
mirage/commands/builtin/search.py:6:from mirage.types import FileType
mirage/commands/builtin/search.py:7:from mirage.workspace import Workspace
mirage/backend/object_storage/r2.py:3:from mirage.backend.object_storage.s3 import S3Backend, S3Config
mirage/backend/object_storage/s3.py:6:from mirage.backend.base import BaseBackend
mirage/backend/object_storage/s3.py:7:from mirage.backend.filetype import guess_type
mirage/backend/object_storage/s3.py:8:from mirage.types import FileStat, FileType
mirage/observe/__init__.py:1:from mirage.observe.inspector import set_server_start_time
mirage/observe/__init__.py:2:from mirage.observe.session_log import SessionLog
mirage/workspace/mount.py:1:from mirage.backend.base import BaseBackend
mirage/workspace/mount.py:2:from mirage.types import MountMode
mirage/workspace/mount.py:3:from mirage.workspace.consistency import ConsistencyPolicy
mirage/commands/builtin/write.py:5:from mirage.types import FileType
mirage/commands/builtin/write.py:6:from mirage.workspace import Workspace
mirage/observe/fastapi/router.py:3:from mirage.observe import inspector
mirage/observe/fastapi/router.py:4:from mirage.observe.session_log import _session_logs
mirage/observe/fastapi/router.py:5:from mirage.server import routes
mirage/commands/builtin/nav.py:5:from mirage.types import FileStat, FileType
mirage/commands/builtin/nav.py:6:from mirage.workspace import Workspace
mirage/runtime/base.py:5:from mirage.commands import _resolve
mirage/runtime/base.py:6:from mirage.commands.builtin import nav
mirage/runtime/base.py:7:from mirage.commands.custom.registry import CommandRegistry
mirage/runtime/base.py:8:from mirage.fuse.registry import FuseOpRegistry
mirage/runtime/base.py:9:from mirage.runtime.accessors.base import (CommandsAccessor, FuseOpsAccessor,
mirage/runtime/base.py:11:from mirage.runtime.accessors.local import (LocalCommandsAccessor,
mirage/runtime/base.py:14:from mirage.workspace import Workspace
mirage/commands/__init__.py:1:from mirage.commands.builtin.handlers import COMMAND_COST, COMMANDS, _resolve
mirage/backend/network/http.py:3:from mirage.backend.base import BaseBackend
mirage/backend/network/http.py:4:from mirage.backend.filetype import guess_type
mirage/backend/network/http.py:5:from mirage.types import FileStat
mirage/server/fastapi/daemon.py:8:from mirage.observe import set_server_start_time
mirage/server/fastapi/daemon.py:9:from mirage.server.constants import DEFAULT_SOCKET
mirage/server/fastapi/daemon.py:10:from mirage.server.fastapi.app import app
mirage/server/fastapi/app.py:3:from mirage.observe.fastapi.router import observe_router
mirage/server/fastapi/app.py:4:from mirage.server.fastapi.routers.command_router import command_router
mirage/server/fastapi/app.py:5:from mirage.server.fastapi.routers.io_router import io_router
mirage/server/fastapi/app.py:6:from mirage.server.fastapi.routers.workspace_router import workspace_router
mirage/server/fastapi/app.py:7:from mirage.server.service.local import LocalService
mirage/workspace/cache/__init__.py:1:from mirage.workspace.cache.entry import CacheEntry
mirage/workspace/cache/__init__.py:2:from mirage.workspace.cache.store import LRUCache
mirage/workspace/__init__.py:1:from mirage.workspace.workspace import Workspace
mirage/workspace/cache/store.py:4:from mirage.workspace.cache.entry import CacheEntry
mirage/commands/builtin/handlers.py:4:from mirage.types import CostLevel
mirage/commands/builtin/handlers.py:5:from mirage.workspace import Workspace
mirage/backend/network/__init__.py:1:from mirage.backend.network.proxy import ProxyBackend
mirage/server/fastapi/config.py:3:from mirage.types import MountMode, SyncPolicy
mirage/server/fastapi/config.py:4:from mirage.workspace.consistency import ConsistencyPolicy
mirage/runtime/__init__.py:1:from mirage.runtime.base import Runtime
mirage/runtime/__init__.py:2:from mirage.runtime.local import LocalRuntime
mirage/runtime/__init__.py:3:from mirage.runtime.memory import MemoryRuntime
mirage/runtime/__init__.py:4:from mirage.runtime.remote import RemoteRuntime
mirage/workspace/sync.py:4:from mirage.backend.base import BaseBackend
mirage/workspace/sync.py:5:from mirage.types import Inode, SyncResult
mirage/commands/custom/registry.py:7:from mirage.commands import COMMANDS
mirage/commands/custom/registry.py:8:from mirage.commands.custom.arg_resolver import classify_args
mirage/commands/custom/registry.py:9:from mirage.commands.custom.executor import run_backend_method, run_external
mirage/commands/custom/registry.py:10:from mirage.server.constants import _parse_size
mirage/commands/custom/registry.py:11:from mirage.types import FilePayload, RemoteCommandOptions
mirage/commands/custom/registry.py:12:from mirage.workspace import Workspace
mirage/server/fastapi/routers/command_router.py:7:from mirage.server import routes
mirage/server/fastapi/routers/command_router.py:8:from mirage.server.fastapi.config import (ExecRequest, RegisterCommandRequest,
mirage/cli/commands/workspace.py:10:from mirage.backend.loader import load_backend_class
mirage/cli/commands/workspace.py:11:from mirage.backend.network.proxy import ProxyBackend
mirage/cli/commands/workspace.py:12:from mirage.cli.client import ClientError, MirageClient
mirage/cli/commands/workspace.py:13:from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
mirage/cli/commands/workspace.py:15:from mirage.cli.helpers import _client, _ensure_server
mirage/cli/commands/workspace.py:16:from mirage.fuse.fs import MirageFS
mirage/cli/commands/workspace.py:17:from mirage.fuse.loader import build_registry_from_config
mirage/cli/commands/workspace.py:18:from mirage.fuse.mount import mount as fuse_mount_fn
mirage/cli/commands/workspace.py:19:from mirage.fuse.registry import FuseOpRegistry
mirage/cli/commands/workspace.py:20:from mirage.fuse.remote.remote import RemoteFuseOp
mirage/cli/commands/workspace.py:21:from mirage.observe.inspector import (workspace_status_from_ws,
mirage/cli/commands/workspace.py:23:from mirage.types import MountMode, RuntimeType
mirage/cli/commands/workspace.py:24:from mirage.workspace import Workspace
mirage/cli/commands/workspace.py:25:from mirage.workspace.consistency import ConsistencyPolicy
mirage/server/fastapi/routers/workspace_router.py:5:from mirage.server import routes
mirage/server/fastapi/routers/workspace_router.py:6:from mirage.server.fastapi.config import (CreateWorkspaceRequest,
mirage/server/service/base.py:3:from mirage.types import FileStat
mirage/fuse/registry.py:6:from mirage.fuse import FUSE_OPS, FuseOp
mirage/workspace/consistency.py:4:from mirage.backend.base import BaseBackend
mirage/workspace/consistency.py:5:from mirage.types import FileStat
mirage/workspace/consistency.py:6:from mirage.workspace.cache.entry import CacheEntry
mirage/commands/custom/executor.py:9:from mirage.commands.custom.arg_resolver import classify_args
mirage/commands/custom/executor.py:10:from mirage.workspace import Workspace
mirage/fuse/remote/remote.py:6:from mirage.fuse.base import FuseOp
mirage/backend/network/proxy.py:5:from mirage.backend.base import BaseBackend
mirage/backend/network/proxy.py:6:from mirage.types import FileStat
mirage/commands/custom/arg_resolver.py:4:from mirage.types import FileType
mirage/commands/custom/arg_resolver.py:5:from mirage.workspace import Workspace
mirage/fuse/__init__.py:1:from mirage.fuse.backend.default import DefaultFuseOp
mirage/fuse/__init__.py:2:from mirage.fuse.backend.local.local import LocalFuseOp
mirage/fuse/__init__.py:3:from mirage.fuse.backend.local.memory import MemoryFuseOp
mirage/fuse/__init__.py:4:from mirage.fuse.backend.object_storage.s3 import S3FuseOp
mirage/fuse/__init__.py:5:from mirage.fuse.base import FuseOp
mirage/fuse/__init__.py:6:from mirage.fuse.filetype.csv import CsvFuseOp
mirage/fuse/__init__.py:7:from mirage.fuse.filetype.parquet import ParquetFuseOp
mirage/server/fastapi/routers/io_router.py:5:from mirage.server import routes
mirage/server/fastapi/routers/io_router.py:6:from mirage.server.fastapi.config import (PathRequest, ReaddirRequest,
mirage/cli/commands/observe.py:3:from mirage.cli.helpers import _client, _ensure_server
mirage/backend/local/__init__.py:1:from mirage.backend.local.local import LocalBackend
mirage/backend/local/__init__.py:2:from mirage.backend.local.memory import MemoryBackend
mirage/fuse/fs.py:9:from mirage.fuse.base import FuseOp
mirage/fuse/fs.py:10:from mirage.fuse.ops_log import OpsLog
mirage/fuse/fs.py:11:from mirage.fuse.registry import FuseOpRegistry
mirage/fuse/fs.py:12:from mirage.workspace import Workspace
mirage/backend/local/memory.py:4:from mirage.backend.base import BaseBackend
mirage/backend/local/memory.py:5:from mirage.backend.filetype import guess_type
mirage/backend/local/memory.py:6:from mirage.types import FileStat, FileType
mirage/backend/local/local.py:5:from mirage.backend.base import BaseBackend
mirage/backend/local/local.py:6:from mirage.backend.filetype import guess_type
mirage/backend/local/local.py:7:from mirage.types import FileStat, FileType
mirage/fuse/loader.py:5:from mirage.fuse import FUSE_OPS
mirage/fuse/loader.py:6:from mirage.fuse.registry import FuseOpRegistry
mirage/fuse/filetype/csv.py:5:from mirage.fuse.base import FuseOp
mirage/server/service/local.py:11:from mirage.backend.loader import load_backend_class
mirage/server/service/local.py:12:from mirage.cli.config import parse_mount_uri, split_mount_spec
mirage/server/service/local.py:13:from mirage.commands import COMMANDS, _resolve
mirage/server/service/local.py:14:from mirage.commands.builtin import nav
mirage/server/service/local.py:15:from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
mirage/server/service/local.py:16:from mirage.fuse import FUSE_OPS
mirage/server/service/local.py:17:from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class
mirage/server/service/local.py:18:from mirage.fuse.registry import FuseOpRegistry
mirage/server/service/local.py:19:from mirage.observe.entries import (CommandEntry, CommandResult, DispatchInfo,
mirage/server/service/local.py:21:from mirage.observe.session_log import get_or_create_log
mirage/server/service/local.py:22:from mirage.runtime.base import _BLOCKED, CommandNotAllowedError
mirage/server/service/local.py:23:from mirage.server.service.base import MirageService
mirage/server/service/local.py:24:from mirage.server.service.cache import FileCache
mirage/server/service/local.py:25:from mirage.types import (CommandStats, FileStat, MountMode, RuntimeType,
mirage/server/service/local.py:27:from mirage.workspace import Workspace
mirage/server/service/local.py:28:from mirage.workspace.consistency import ConsistencyPolicy
mirage/fuse/backend/default.py:1:from mirage.fuse.base import FuseOp
mirage/fuse/README.md:60:from mirage.backend.local.memory import MemoryBackend
mirage/fuse/README.md:61:from mirage.workspace import Workspace
mirage/fuse/README.md:62:from mirage.fuse.fs import MirageFS, mount
mirage/fuse/mount.py:8:from mirage.fuse.fs import MirageFS
mirage/fuse/mount.py:9:from mirage.workspace import Workspace
mirage/cli/config.py:5:from mirage.backend.base import BaseBackend
mirage/cli/config.py:6:from mirage.backend.local.local import LocalBackend
mirage/cli/config.py:7:from mirage.backend.local.memory import MemoryBackend
mirage/cli/config.py:8:from mirage.types import BackendType, MountMode
mirage/cli/config.py:9:from mirage.workspace.consistency import ConsistencyPolicy
mirage/fuse/filetype/parquet.py:6:from mirage.fuse.base import FuseOp
mirage/runtime/accessors/local.py:1:from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
mirage/runtime/accessors/local.py:2:from mirage.fuse import FUSE_OPS, FuseOp
mirage/runtime/accessors/local.py:3:from mirage.fuse.loader import load_fuse_op_class
mirage/runtime/accessors/local.py:4:from mirage.fuse.registry import FuseOpRegistry
mirage/runtime/accessors/local.py:5:from mirage.runtime.accessors.base import (CommandsAccessor, FuseOpsAccessor,
mirage/runtime/accessors/local.py:7:from mirage.workspace import Workspace
mirage/fuse/backend/object_storage/s3.py:1:from mirage.fuse.base import FuseOp
mirage/cli/app.py:3:from mirage.cli.commands import server, workspace
mirage/cli/app.py:4:from mirage.cli.commands.command import command_app, command_server_app
mirage/cli/app.py:5:from mirage.cli.commands.observe import observe_app
mirage/cli/commands/command.py:4:from mirage.backend.loader import load_backend_class
mirage/cli/commands/command.py:5:from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
mirage/cli/commands/command.py:7:from mirage.cli.helpers import _client
mirage/cli/commands/command.py:8:from mirage.commands.remote.auth import (APIKeyAuth, BasicAuth,
mirage/cli/commands/command.py:10:from mirage.commands.remote.fastapi.app import (
mirage/cli/commands/command.py:12:from mirage.commands.remote.grpc.server import serve as grpc_serve
mirage/cli/commands/command.py:13:from mirage.types import MountMode, RuntimeType
mirage/cli/commands/command.py:14:from mirage.workspace import Workspace
mirage/cli/commands/command.py:15:from mirage.workspace.consistency import ConsistencyPolicy
mirage/cli/client.py:3:from mirage.server import routes
mirage/cli/client.py:4:from mirage.server.constants import DEFAULT_SOCKET
mirage/cli/README.md:248:from mirage.commands.client import HTTPCommandClient
mirage/cli/README.md:249:from mirage.commands.auth import BearerTokenAuth
mirage/cli/README.md:264:from mirage.commands.client import GRPCCommandClient
mirage/cli/README.md:265:from mirage.commands.auth import BearerTokenAuth
mirage/cli/README.md:287:from mirage.server.registry import CommandRegistry, ExternalCommand
mirage/fuse/backend/local/local.py:1:from mirage.fuse.base import FuseOp
mirage/runtime/memory.py:1:from mirage.commands.custom.registry import CommandRegistry
mirage/runtime/memory.py:2:from mirage.runtime.base import CommandNotAllowedError, Runtime
mirage/runtime/memory.py:3:from mirage.workspace import Workspace
mirage/runtime/remote.py:5:from mirage.backend.network.proxy import ProxyBackend
mirage/runtime/remote.py:6:from mirage.runtime.accessors.remote import (RemoteCommandsAccessor,
mirage/runtime/remote.py:9:from mirage.runtime.base import _BLOCKED, CommandNotAllowedError, Runtime
mirage/runtime/remote.py:10:from mirage.server import routes
mirage/runtime/remote.py:11:from mirage.types import MountMode, SyncPolicy
mirage/runtime/remote.py:12:from mirage.workspace import Workspace
mirage/runtime/accessors/remote.py:3:from mirage.runtime.accessors.base import (CommandsAccessor, FuseOpsAccessor,
mirage/runtime/accessors/remote.py:5:from mirage.server import routes
mirage/fuse/backend/local/memory.py:1:from mirage.fuse.base import FuseOp
mirage/runtime/local.py:1:from mirage.backend.local.local import LocalBackend
mirage/runtime/local.py:2:from mirage.commands.custom.registry import CommandRegistry
mirage/runtime/local.py:3:from mirage.runtime.base import CommandNotAllowedError, Runtime
mirage/runtime/local.py:4:from mirage.types import MountMode, SyncPolicy
mirage/runtime/local.py:5:from mirage.workspace import Workspace
mirage/cli/commands/server.py:4:from mirage.server.constants import DEFAULT_SOCKET


let's have a plan to refactor this


The user wants to plan the refactoring of `server/service/local.py` (split into multiple modules). I'm in plan mode. Let me explore the codebase first to understand what's in local.py and the related files.

Let me launch explore agents to understand:
1. The current state of `local.py` and what methods it has
2. The test files for it
3. The `base.py` service interface


Agent

Explore server/service/local.py structure

Prompt:
Read these files in full and report their complete contents:
1. `/Users/agent/strukto/mirage/mirage/server/service/local.py`
2. `/Users/agent/strukto/mirage/mirage/server/service/base.py`
3. `/Users/agent/strukto/mirage/tests/server/service/test_local.py`

Also list all files in:
- `/Users/agent/strukto/mirage/mirage/server/service/`
- `/Users/agent/strukto/mirage/tests/server/service/`

Report the full content of each file verbatim with line numbers.


Explore who imports LocalService

Prompt:
In `/Users/agent/strukto/mirage`, find all files that import from `mirage.server.service.local` or reference `LocalService`. Also find all files that import from `mirage.server.service.base` or reference `MirageService`.

Report: file path, the exact import line, and the context (what they use from it).


## Summary

Here is the complete content of all requested files with line numbers:

### 1. `/Users/agent/strukto/mirage/mirage/server/service/local.py` (426 lines)
This file contains the `LocalService` class which implements the `MirageService` abstract base class. Key components include:

- **WorkspaceSession**: A Pydantic model that stores session state including workspace ID, mount information, FUSE status, and command statistics
- **_unmount_fuse()**: Helper function to unmount FUSE filesystems (supports macOS and Linux)
- **LocalService**: Main service class with methods for:
  - Workspace lifecycle: `create_workspace()`, `delete_workspace()`, `list_workspaces()`
  - Command execution: `exec()`, `list_commands()`, `register_command()`
  - Workspace operations: `sync()`, `status()`
  - File I/O operations: `io_read()`, `io_write()`, `io_stat()`, `io_readdir()`, `io_create()`, `io_unlink()`, `io_mkdir()`, `io_rmdir()`, `io_rename()`, `io_truncate()`, `io_fuse_cat()`
  - FUSE operation management: `list_fuse_ops()`, `register_fuse_op()`, `register_fuse()`, `report_fuse_stats()`

### 2. `/Users/agent/strukto/mirage/mirage/server/service/base.py` (105 lines)
This is the abstract base class `MirageService` that defines the interface for service implementations. It declares abstract methods for all workspace, command, I/O, and FUSE operations.

### 3. `/Users/agent/strukto/mirage/tests/server/service/test_local.py` (214 lines)
Comprehensive test suite for `LocalService` covering:
- Workspace creation and listing
- Command execution (touch, ls)
- Runtime restrictions (blocked commands)
- Workspace deletion
- File I/O operations (read, write, stat, readdir, mkdir, unlink, rename, truncate)
- FUSE operations (cat, list ops, register ops)
- File cache usage
- Error cases (missing workspaces, FUSE workspace restrictions)

### 4. `/Users/agent/strukto/mirage/mirage/server/service/cache.py` (34 lines)
Simple file caching implementation with:
- `_CacheEntry`: Dataclass storing checksum and data
- `FileCache`: Cache manager with `get()`, `put()`, and `clear()` methods supporting per-workspace isolation

### 5. `/Users/agent/strukto/mirage/tests/server/service/test_cache.py` (55 lines)
Tests for the `FileCache` including hit/miss scenarios, checksum validation, workspace isolation, and cache clearing.

### Directory Contents:

**`/Users/agent/strukto/mirage/mirage/server/service/`:**
- `__init__.py` (empty)
- `base.py` (2,508 bytes)
- `cache.py` (915 bytes)
- `local.py` (17,108 bytes)

**`/Users/agent/strukto/mirage/tests/server/service/`:**
- `test_cache.py` (1,602 bytes)
- `test_local.py` (6,209 bytes)

agentId: a37da4a896739caa6 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 26909
tool_uses: 7
duration_ms: 12726</usage>

Prompt:
Read these files in full and report their complete contents:
1. `/Users/agent/strukto/mirage/mirage/server/service/local.py`
2. `/Users/agent/strukto/mirage/mirage/server/service/base.py`
3. `/Users/agent/strukto/mirage/tests/server/service/test_local.py`

Also list all files in:
- `/Users/agent/strukto/mirage/mirage/server/service/`
- `/Users/agent/strukto/mirage/tests/server/service/`

Report the full content of each file verbatim with line numbers.


Perfect! I now have all the information needed. Let me compile the comprehensive report:

## Report: Files Importing from `mirage.server.service.local` and `mirage.server.service.base`

### 1. **`/Users/agent/strukto/mirage/mirage/server/service/local.py`**
   - **Import Line (line 23):** `from mirage.server.service.base import MirageService`
   - **Context:** `LocalService` class inherits from `MirageService` (line 64). The class implements all abstract methods defined in the `MirageService` ABC, including workspace management, command execution, file I/O operations, and FUSE operations.

### 2. **`/Users/agent/strukto/mirage/mirage/server/fastapi/app.py`**
   - **Import Line (line 7):** `from mirage.server.service.local import LocalService`
   - **Context:** Creates a singleton instance of `LocalService` (line 10: `service = LocalService()`) and assigns it to the FastAPI app state (line 11: `app.state.service = service`). Also accesses the private `_sessions` dictionary directly (line 18: `_sessions = service._sessions`).

### 3. **`/Users/agent/strukto/mirage/tests/server/service/test_local.py`**
   - **Import Line (line 4):** `from mirage.server.service.local import LocalService`
   - **Context:** Uses `LocalService` directly in a pytest fixture (line 9: `s = LocalService()`) to create service instances for testing. Tests all public methods of `LocalService` including workspace lifecycle, command execution, file I/O, and FUSE operations. Also accesses private `_sessions` dictionary in assertions (lines 201, 210).

### Summary of Usage:

- **`MirageService` (base class)** is used as the abstract base class that `LocalService` implements
- **`LocalService`** is instantiated in the FastAPI application and tests, and serves as the concrete implementation of the service protocol for workspace management and file operations

agentId: ad1948246a2877b09 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 27845
tool_uses: 7
duration_ms: 11510</usage>

Prompt:
In `/Users/agent/strukto/mirage`, find all files that import from `mirage.server.service.local` or reference `LocalService`. Also find all files that import from `mirage.server.service.base` or reference `MirageService`.

Report: file path, the exact import line, and the context (what they use from it).


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

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

import os
import signal
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone

from pydantic import BaseModel, ConfigDict, Field

from mirage.backend.loader import load_backend_class
from mirage.cli.config import parse_mount_uri, split_mount_spec
from mirage.commands import COMMANDS, _resolve
from mirage.commands.builtin import nav
from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
from mirage.fuse import FUSE_OPS
from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class
from mirage.fuse.registry import FuseOpRegistry
from mirage.observe.entries import (CommandEntry, CommandResult, DispatchInfo,
                                    ParsedCommand, SessionEntry, WorkspaceInfo)
from mirage.observe.session_log import get_or_create_log
from mirage.runtime.base import _BLOCKED, CommandNotAllowedError
from mirage.server.service.base import MirageService
from mirage.server.service.cache import FileCache
from mirage.types import (CommandStats, FileStat, MountMode, RuntimeType,
                          SyncPolicy)
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy

class WorkspaceSession(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    id: str
    ws: Workspace | None
    registry: CommandRegistry | None
    cwd: str = "/"
    fuse_mounted: bool = False
    mode: MountMode = MountMode.WRITE
    mountpoint: str | None = None
    pid: int | None = None
    mounts: list[dict] = Field(default_factory=list)
    runtime_type: RuntimeType | None = None
    fuse_registry: FuseOpRegistry = Field(default_factory=FuseOpRegistry)
    command_stats: CommandStats = Field(default_factory=CommandStats)
    created_at: float = Field(default_factory=time.monotonic)
    reported_status: dict | None = None
    reported_transfers: dict | None = None

def _unmount_fuse(mountpoint: str, pid: int | None) -> None:
    if sys.platform == "darwin":
        subprocess.run(["diskutil", "unmount", "force", mountpoint],
                       capture_output=True)
    else:
        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    if pid:
        try:
            os.kill(pid, signal.SIGTERM)
        except ProcessLookupError:
            pass

class LocalService(MirageService):

    def __init__(self) -> None:
        self._sessions: dict[str, WorkspaceSession] = {}
        self._file_cache = FileCache()

    def _get_session(self, ws_id: str) -> WorkspaceSession:
        session = self._sessions.get(ws_id)
        if session is None:
            raise KeyError(f"workspace {ws_id!r} not found")
        if session.fuse_mounted:
            raise ValueError("not supported for fuse workspaces")
        return session

    def create_workspace(self, config: dict) -> str:
        mode = config.get("mode", "write")
        consistency = config.get("consistency", "cached")
        default_mode = MountMode(mode)
        default_consistency = ConsistencyPolicy(consistency)
        mounts = config.get("mounts", {})

        backends: dict = {}
        for prefix, value in mounts.items():
            if isinstance(value, str):
                backends[prefix] = parse_mount_uri(value)
            else:
                backend_spec = value.get("backend")
                if backend_spec:
                    cls = load_backend_class(backend_spec)
                    backend = cls(**(value.get("args") or {}))
                else:
                    backend = parse_mount_uri(value.get("uri") or "")
                mount_mode = (MountMode(value["mode"])
                              if value.get("mode") else default_mode)
                mount_consistency = (ConsistencyPolicy(value["consistency"])
                                     if value.get("consistency") else
                                     default_consistency)
                if (mount_mode != default_mode
                        or mount_consistency != default_consistency):
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                else:
                    backends[prefix] = backend

        ws = Workspace(
            backends,
            sync_policy=SyncPolicy(config.get("sync_policy", "staged")),
            mode=default_mode,
            consistency=default_consistency,
            ttl_seconds=config.get("ttl_seconds", 300),
        )

        mounts_info: list[dict] = []
        for prefix, value in mounts.items():
            if isinstance(value, str):
                uri = value
            else:
                uri = value.get("uri") or ""
                if not uri and value.get("backend"):
                    uri = f"{value['backend']}://"
            mounts_info.append({"prefix": prefix, "uri": uri})

        ws_id = str(uuid.uuid4())[:8]
        registry = CommandRegistry()
        fuse_ops_config = config.get("fuse_ops") or {}
        fuse_registry = build_registry_from_config(fuse_ops_config)
        runtime = config.get("runtime")
        rt_type = RuntimeType(runtime) if runtime else None

        self._sessions[ws_id] = WorkspaceSession(
            id=ws_id,
            ws=ws,
            registry=registry,
            mode=mode,
            mounts=mounts_info,
            runtime_type=rt_type,
            fuse_registry=fuse_registry,
        )

        log = get_or_create_log(ws_id)
        mount_info = {}
        for m in ws._registry.mounts():
            mount_info[m.prefix] = {
                "type": type(m.backend).__name__,
                "mode": m.mode.value,
            }
        session_entry = SessionEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            workspace=WorkspaceInfo(
                mounts=mount_info,
                cache_limit=str(ws._cache.limit),
                runtime_type=rt_type.value if rt_type else None,
            ),
            commands=registry.list_commands(),
        )
        log.write_session(session_entry)
        return ws_id

    def delete_workspace(self, ws_id: str) -> None:
        session = self._sessions.get(ws_id)
        if session is None:
            raise KeyError(f"workspace {ws_id!r} not found")
        if session.fuse_mounted and session.mountpoint:
            _unmount_fuse(session.mountpoint, session.pid)
        self._file_cache.clear(ws_id)
        del self._sessions[ws_id]

    def list_workspaces(self) -> list[dict]:
        return [{
            "id": s.id,
            "fuse_mounted": s.fuse_mounted,
            "mode": s.mode,
            "mountpoint": s.mountpoint,
            "mounts": s.mounts,
            "runtime": s.runtime_type.value if s.runtime_type else None,
        } for s in self._sessions.values()]

    def exec(
        self,
        ws_id: str,
        command: str,
        cwd: str | None = None,
        stdin: bytes | None = None,
        input_files: dict[str, tuple[str, bytes | None]] | None = None,
    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
        session = self._get_session(ws_id)
        if cwd is not None:
            session.cwd = cwd

        cache_status: dict[str, str] = {}
        if input_files:
            for path, (checksum, data) in input_files.items():
                hit, _ = self._file_cache.get(ws_id, path, checksum)
                if hit:
                    cache_status[path] = "hit"
                elif data is not None:
                    self._file_cache.put(ws_id, path, checksum, data)
                    session.ws.write(path, data)
                    cache_status[path] = "miss"
                else:
                    return (False, f"cache miss: {path}", None, None)

        cmd_name = command.strip().split()[0] if command.strip() else ""
        session.command_stats.total_commands += 1
        session.command_stats.command_counts[cmd_name] = (
            session.command_stats.command_counts.get(cmd_name, 0) + 1)
        session.command_stats.last_command = command
        session.command_stats.last_timestamp = datetime.now(
            timezone.utc).isoformat()
        t0 = time.monotonic()
        try:
            if session.runtime_type is not None and cmd_name in _BLOCKED:
                raise CommandNotAllowedError(
                    f"{cmd_name!r} is not allowed in this runtime")
            output = session.registry.run(session.ws,
                                          command,
                                          cwd=session.cwd,
                                          stdin=stdin)
            duration_ms = int((time.monotonic() - t0) * 1000)
            if command.strip().startswith("cd "):
                parts = command.strip().split(None, 1)
                if len(parts) > 1:
                    session.cwd = nav.cd(session.ws, session.cwd,
                                         _resolve(session.cwd, parts[1]))
            args = command.strip().split()[1:] if command.strip() else []
            source = "builtin" if cmd_name in COMMANDS else "external"
            log = get_or_create_log(ws_id)
            entry = CommandEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                raw=command,
                parsed=ParsedCommand(
                    pipeline=[command],
                    args={cmd_name: args},
                ),
                dispatch=[
                    DispatchInfo(
                        command=cmd_name,
                        handler=f"_{cmd_name}"
                        if source == "builtin" else cmd_name,
                        source=source,
                    )
                ],
                result=CommandResult(
                    exit_code=0,
                    stdout_bytes=len(output.encode()) if output else 0,
                    duration_ms=duration_ms,
                ),
            )
            log.write_command(entry)

            return (True, output, None, cache_status or None)
        except Exception as e:
            duration_ms = int((time.monotonic() - t0) * 1000)
            session.command_stats.total_errors += 1
            log = get_or_create_log(ws_id)
            entry = CommandEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                raw=command,
                result=CommandResult(exit_code=1,
                                     stdout_bytes=0,
                                     duration_ms=duration_ms),
            )
            log.write_command(entry)
            return (False, str(e), None, None)

    def list_commands(self, ws_id: str) -> list[str]:
        session = self._get_session(ws_id)
        return session.registry.list_commands()

    def register_command(self,
                         ws_id: str,
                         name: str,
                         exec_str: str,
                         description: str = "") -> None:
        session = self._get_session(ws_id)
        session.registry.register(
            ExternalCommand(name=name, exec=exec_str, description=description))

    def sync(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        results = session.ws.sync()
        return [{"path": r.path, "status": r.status} for r in results]

    def status(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        inodes = session.ws.status()
        return [{
            "path": i.virtual_path,
            "dirty": i.dirty,
            "size": i.size
        } for i in inodes]

    # ── File I/O (backend-level) ─────────────────────────────────────────

    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
        session = self._get_session(ws_id)
        return session.ws.read(path, offset, size)

    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.write(backend_path, offset, data)

    def io_stat(self, ws_id: str, path: str) -> FileStat:
        session = self._get_session(ws_id)
        return session.ws.stat(path)

    def io_readdir(self,
                   ws_id: str,
                   path: str,
                   cursor: str | None = None) -> tuple[list[str], str | None]:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.readdir(backend_path, cursor)

    def io_create(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.create(backend_path)

    def io_unlink(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.unlink(backend_path)

    def io_mkdir(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.mkdir(backend_path)

    def io_rmdir(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.rmdir(backend_path)

    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
        session = self._get_session(ws_id)
        backend, src_path, _ = session.ws._registry.resolve(src)
        _, dst_path, _ = session.ws._registry.resolve(dst)
        backend.rename(src_path, dst_path)

    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.truncate(backend_path, size)

    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
        session = self._get_session(ws_id)
        st = session.ws.stat(path)
        op = session.fuse_registry.resolve(path, st.size or 0, session.ws)
        ctx = op.on_open(session.ws, path, st.size or 0)
        return op.read(session.ws, path, 0, st.size or 0, ctx)

    # ── FuseOp management ────────────────────────────────────────────────

    def list_fuse_ops(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        return session.fuse_registry.list_ops()

    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
        session = self._get_session(ws_id)
        op = FUSE_OPS.get(handler)
        if op is None:
            try:
                cls = load_fuse_op_class(handler)
                op = cls()
            except (ValueError, Exception) as e:
                raise ValueError(f"unknown handler: {handler} ({e})")
        session.fuse_registry.register(pattern, op)

    # ── FUSE-specific (not part of MirageService ABC) ────────────────────

    def register_fuse(self,
                      mountpoint: str,
                      pid: int,
                      mount_specs: list[str] | None = None,
                      runtime: str | None = None) -> str:
        fuse_mounts: list[dict] = []
        for spec in (mount_specs or []):
            try:
                uri, prefix = split_mount_spec(spec)
                fuse_mounts.append({"prefix": prefix, "uri": uri})
            except ValueError:
                pass
        ws_id = str(uuid.uuid4())[:8]
        rt_type = RuntimeType(runtime) if runtime else None
        self._sessions[ws_id] = WorkspaceSession(
            id=ws_id,
            ws=None,
            registry=None,
            fuse_mounted=True,
            mountpoint=mountpoint,
            pid=pid,
            mounts=fuse_mounts,
            runtime_type=rt_type,
        )
        mount_info = {m["prefix"]: {"type": m["uri"]} for m in fuse_mounts}
        log = get_or_create_log(ws_id)
        session_entry = SessionEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            workspace=WorkspaceInfo(
                mounts=mount_info,
                runtime_type=rt_type.value if rt_type else None,
            ),
        )
        log.write_session(session_entry)
        return ws_id

    def report_fuse_stats(self,
                          mountpoint: str,
                          status: dict,
                          transfers: dict | None = None,
                          ops: list[dict] | None = None) -> bool:
        for s in self._sessions.values():
            if s.fuse_mounted and s.mountpoint == mountpoint:
                s.reported_status = status
                s.reported_transfers = transfers or {}
                if ops:
                    log = get_or_create_log(s.id)
                    for op in ops:
                        log._append(op)
                return True
        return False


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

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

from abc import ABC, abstractmethod

from mirage.types import FileStat

class MirageService(ABC):

    @abstractmethod
    def create_workspace(self, config: dict) -> str:
        ...

    @abstractmethod
    def delete_workspace(self, ws_id: str) -> None:
        ...

    @abstractmethod
    def list_workspaces(self) -> list[dict]:
        ...

    @abstractmethod
    def exec(
        self,
        ws_id: str,
        command: str,
        cwd: str | None = None,
        stdin: bytes | None = None,
        input_files: dict[str, tuple[str, bytes | None]] | None = None,
    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
        ...

    @abstractmethod
    def list_commands(self, ws_id: str) -> list[str]:
        ...

    @abstractmethod
    def register_command(self,
                         ws_id: str,
                         name: str,
                         exec_str: str,
                         description: str = "") -> None:
        ...

    @abstractmethod
    def sync(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def status(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
        ...

    @abstractmethod
    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
        ...

    @abstractmethod
    def io_stat(self, ws_id: str, path: str) -> FileStat:
        ...

    @abstractmethod
    def io_readdir(self,
                   ws_id: str,
                   path: str,
                   cursor: str | None = None) -> tuple[list[str], str | None]:
        ...

    @abstractmethod
    def io_create(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_unlink(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_mkdir(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_rmdir(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
        ...

    @abstractmethod
    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
        ...

    @abstractmethod
    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
        ...

    @abstractmethod
    def list_fuse_ops(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
        ...


Now I have a clear picture. Let me think about the refactoring plan.

`local.py` has 426 lines and handles 5 distinct concerns:

1. **Workspace lifecycle** (lines 78-178): `create_workspace`, `delete_workspace`, `list_workspaces`
   - Depends on: `parse_mount_uri`, `load_backend_class`, `ConsistencyPolicy`, `MountMode`, `SyncPolicy`, `Workspace`, `RuntimeType`, `CommandRegistry`, `build_registry_from_config`, `get_or_create_log`

2. **Command execution** (lines 180-279): `exec`, `list_commands`, `register_command`, `sync`, `status`
   - Depends on: `COMMANDS`, `_resolve`, `nav`, `_BLOCKED`, `CommandNotAllowedError`, `FileCache`, `get_or_create_log`

3. **File I/O** (lines 295-354): `io_read`, `io_write`, `io_stat`, `io_readdir`, `io_create`, `io_unlink`, `io_mkdir`, `io_rmdir`, `io_rename`, `io_truncate`, `io_fuse_cat`
   - Depends on: `workspace._registry.resolve`, `workspace.stat`, `workspace.read`

4. **FUSE operations** and **Utility functions** round out the remaining concerns.

The core challenge is that all these methods rely on `self._sessions: dict[str, WorkspaceSession]` as shared state, which makes refactoring into separate classes tricky since they'd all need access to the same sessions dictionary. I'm weighing whether to extract the session model and helpers into their own module while keeping the main class intact and better organized, or pursue a more aggressive split that would require careful state sharing. into multiple files — `workspace_ops.py`, `command_ops.py`, `io_ops.py`, `fuse_ops.py` — each containing module-level functions that operate on `WorkspaceSession`, with `LocalService` becoming a thin delegation layer. But looking back at the CLAUDE.md constraint about avoiding nested functions, the current structure already respects that. What the user is really asking is which pieces can be extracted into separate modules to improve organization.

The cleanest approach would be extracting `WorkspaceSession` and its related unmount logic into a dedicated `session.py` module, leaving `LocalService` as a much leaner wrapper. Alternatively, the FUSE-specific methods that handle externally-registered mounts stand apart from workspace operations and could be isolated separately. The broader question seems to be about which modules in the codebase itself are candidates for refactoring or simplification, with `local.py` being the primary target for splitting. The real constraint is that `LocalService` methods all depend on shared state like `_sessions` and `_file_cache`, so I can't cleanly split them into independent classes without threading these dependencies through. The simplest approach is extracting just `WorkspaceSession` and the `_unmount_fuse` helper to a separate `session.py` file, leaving `LocalService` in `local.py` but significantly lighter without the data model cluttering it.

Looking at the broader codebase, there are a few other candidates—`cache.py` is only 34 lines and could merge into `local.py`, and `ops_log.py` is a small utility only used by `fuse/fs.py`—but the main refactoring opportunity is really this `local.py` split. Let me map out the concrete plan: move `WorkspaceSession` and `_unmount_fuse` to `session.py`, then update the imports in `local.py`.

I could push further and extract FUSE-specific methods like `register_fuse` and `report_fuse_stats` into their own module, but they're tightly coupled to `_sessions` and `_file_cache`, so that would require more careful dependency management. The session extraction is the cleanest win here. local.py` keeps just `LocalService` with the refactored dependencies. This splits the concerns cleanly — session management lives in its own module, and `local.py` becomes much leaner by importing what it needs. I'm laying out the refactoring plan: extracting `WorkspaceSession` and `_unmount_fuse` from `local.py` into a new `session.py` file, which will require updates to `local.py` and potentially `app.py` depending on their import patterns. But I should clarify with the user first what exactly they want refactored—whether it's just extracting these components or a broader restructuring of the service module. splitting `local.py` (extract `WorkspaceSession` + `_unmount_fuse`)
2. A broader refactor of the service layer

Let me just go with the most reasonable interpretation: split `local.py` into focused modules.

The cleanest split that makes sense:
1. `session.py`: `WorkspaceSession` + `_unmount_fuse`
2. `local.py`: Just `LocalService` class

This reduces `local.py` from 426 lines to ~380 lines (not that much reduction).

Alternatively, a bigger refactor:
1. `session.py`: `WorkspaceSession`, `_unmount_fuse`
2. `workspace_factory.py`: `_build_workspace_from_config()` (the 80-line workspace creation logic)
3. `local.py`: `LocalService` (thin dispatch layer)

Looking at the actual method groupings, I can see natural boundaries: workspace lifecycle operations, command execution, workspace state tracking, file I/O, FUSE operation management, and FUSE process handling. The simplest approach is extracting the session and cleanup logic into its own module while keeping `LocalService` as a thin dispatcher in the original file.


Now I have the full picture. Let me write the plan.


     1→# Refactor workspace/__init__.py + ConsistencyPolicy cleanup
     2→
     3→## Context
     4→
     5→`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
     6→1. Moves the `Workspace` class to its own file
     7→2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
     8→3. Makes `Mount.consistency` always explicit (no more `None` = inherit)
     9→
    10→---
    11→
    12→## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`
    13→
    14→**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`
    15→
    16→Update all references (~14 occurrences):
    17→- `mirage/workspace/__init__.py` (line 55)
    18→- `mirage/workspace/cache/policy.py` (line 15)
    19→- `mirage/cli/commands/workspace.py` (line 107)
    20→- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
    21→- `mirage/cli/config.py` (line 106)
    22→- `mirage/server/fastapi/config.py` (line 18)
    23→- `tests/workspace/test_init.py` (lines 10, 29, 94)
    24→- `tests/cli/test_app.py` (line 36)
    25→- `tests/cli/test_config.py` (line 133)
    26→
    27→## Step 2: Make `Mount.consistency` always explicit
    28→
    29→**`mirage/workspace/mount.py`**:
    30→- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
    31→- `MountRegistry.mount()`: same signature change
    32→
    33→**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
    34→- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
    35→- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
    36→- Remove `_effective_consistency()` method entirely
    37→- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`
    38→
    39→**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`
    40→
    41→**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)
    42→
    43→**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults
    44→
    45→## Step 3: Move Workspace to `workspace/workspace.py`
    46→
    47→**Create: `mirage/workspace/workspace.py`**
    48→- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)
    49→
    50→**Rewrite: `mirage/workspace/__init__.py`**
    51→```python
    52→from mirage.workspace.workspace import Workspace
    53→
    54→__all__ = ["Workspace"]
    55→```
    56→
    57→No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.
    58→
    59→## Step 4: Rename test file
    60→
    61→- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
    62→- No content changes (imports stay `from mirage.workspace import Workspace`)
    63→
    64→## Key Files Modified
    65→
    66→- `mirage/types.py`
    67→- `mirage/workspace/workspace.py` (new)
    68→- `mirage/workspace/__init__.py` (rewritten to re-exports)
    69→- `mirage/workspace/mount.py`
    70→- `mirage/workspace/cache/policy.py`
    71→- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
    72→- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
    73→- `tests/workspace/test_workspace.py` (renamed)
    74→- `tests/cli/test_app.py`, `tests/cli/test_config.py`
    75→
    76→## Verification
    77→
    78→1. `uv run pytest` — all 641 tests pass
    79→2. `pre-commit run --all-files` — lint/format clean
    80→

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

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


Conversation compacted