Files
strukto-ai--mirage/python/mirage/resource/gridfs/gridfs.py
T
bytecii 3293cb5ff4 feat(py): mirage mcp over stdio, and invert the mypy allowlist
Two cleanup-plan items, both Python-side.

Item 29 -- `mirage mcp`. TypeScript shipped a six-tool stdio MCP server;
Python had none, so a pip-install user could not point Cursor or Claude
Desktop at a workspace and `mirage --help` differed by distribution.
Adding the entry point alone would have duplicated the tools, because
this side kept them private inside the Claude Agent SDK integration, so
the shared layer comes first:

  - agents/tool_descriptions.py -- the six strings, one copy.
  - agents/tool_operations.py -- MirageToolOperations, lifted out of the
    SDK server's private _MirageTools.
  - agents/file_version.py -- stale-write protection, which this side
    lacked entirely. TS stamps stored bytes; here the stamp covers the
    rendered bytes, because this read tool has always rendered and an
    edit must search what the agent was actually shown.
  - agents/mcp/server.py + cli/mcp.py -- the server and `mirage mcp`.
  - server/workspace_config.py -- config discovery (candidates, env
    names, walk up from cwd), which Python had nowhere, so every entry
    point had to be handed an explicit path.

The server is the low-level MCP Server rather than FastMCP: FastMCP does
not forward a version, and TS advertises one. Handlers are bound methods,
not decorated closures, so nothing nests.

Item 28 -- the mypy allowlist. 54 modules opted *in* to annotation
checking against 1826, so the default was unchecked and every new file
joined the unchecked side. The default is now strict, with a list of
what is not yet annotated that only shrinks. 166 annotations cleared
along the way; the remainder is named module by module.

Two real defects surfaced by the annotations, neither of them typing:

  - Workspace._original_open / _original_os were invented by assignment
    in lifecycle.patch_process, so unpatch without a patch raised
    AttributeError. Declared, and the restore is guarded.
  - sed_generic declared a non-optional writer while its own docstring
    and its `write_bytes is None` branch said otherwise; the builder
    passes None whenever the backend cannot write.

Tests keep the PathSpec rule instead of full strict: measured, full
strict on python/tests is 2374 errors, of which 634 are `str` where a
pydantic field declares SecretStr -- which pydantic coerces at runtime --
and most of the rest is the monkeypatched-fake pattern CLAUDE.md
sanctions. The rule that is violated for real is PathSpec, 19 times, and
scripts/check_test_pathspec.py now holds that line. One of the 19 was a
latent AttributeError: tests/e2e passes a str to s3 write_bytes, which
reads .mount_path, and the test skips without a live versioned bucket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 03:33:16 -07:00

114 lines
4.0 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import dataclasses
from typing import Any
from mirage.accessor.gridfs import GridFSAccessor, GridFSConfig
from mirage.commands.builtin.gridfs import COMMANDS as GRIDFS_COMMANDS
from mirage.core.gridfs.constants import SCOPE_ERROR
from mirage.core.gridfs.copy import copy
from mirage.core.gridfs.create import create
from mirage.core.gridfs.du import entries as du_entries
from mirage.core.gridfs.du import size as du_size
from mirage.core.gridfs.exists import exists
from mirage.core.gridfs.find import find
from mirage.core.gridfs.mkdir import mkdir
from mirage.core.gridfs.read import read_bytes
from mirage.core.gridfs.readdir import readdir
from mirage.core.gridfs.rename import rename
from mirage.core.gridfs.rm import rm_r
from mirage.core.gridfs.rmdir import rmdir
from mirage.core.gridfs.stat import stat as gridfs_stat
from mirage.core.gridfs.stream import range_read, read_stream
from mirage.core.gridfs.truncate import truncate
from mirage.core.gridfs.unlink import unlink
from mirage.core.gridfs.watch import build_delta_hook
from mirage.core.gridfs.write import write_bytes
from mirage.ops.gridfs import OPS as GRIDFS_OPS
from mirage.resource.base import BaseResource
from mirage.resource.gridfs.prompt import PROMPT
from mirage.types import PathSpec, ResourceName
from mirage.utils.glob_walk import make_resolve_glob
from mirage.utils.key_prefix import mount_key
from mirage.watch.base import DeltaHook
_resolve_glob = make_resolve_glob(readdir, SCOPE_ERROR)
_GRIDFS_OPS = {
"read_bytes": read_bytes,
"write": write_bytes,
"readdir": readdir,
"stat": gridfs_stat,
"unlink": unlink,
"rmdir": rmdir,
"copy": copy,
"rename": rename,
"mkdir": mkdir,
"read_stream": read_stream,
"range_read": range_read,
"rm_recursive": rm_r,
"du_size": du_size,
"du_entries": du_entries,
"create": create,
"truncate": truncate,
"exists": exists,
"find_flat": find,
}
class GridFSResource(BaseResource):
accessor: GridFSAccessor
name: str = ResourceName.GRIDFS
# byte store: stat() sizes every file from metadata
SIZES_ALWAYS_KNOWN: bool = True
caches_reads: bool = True
_ops: dict[str, Any] = _GRIDFS_OPS
PROMPT: str = PROMPT
SUPPORTS_SNAPSHOT: bool = True
def __init__(self, config: GridFSConfig) -> None:
super().__init__()
self.config = config
self.accessor = GridFSAccessor(self.config)
for fn in GRIDFS_COMMANDS:
self.register(fn)
for op in GRIDFS_OPS:
self.register_op(op)
def delta_hook(self) -> DeltaHook:
return build_delta_hook(self.accessor)
async def resolve_glob(
self,
paths: list[str | PathSpec],
prefix: str = '',
) -> list[PathSpec]:
if prefix:
paths = [
dataclasses.replace(p,
resource_path=mount_key(p.virtual, prefix))
if isinstance(p, PathSpec) else p for p in paths
]
return await _resolve_glob(self.accessor, paths, self._index)
def get_state(self) -> dict[str, Any]:
return self.config_state(self.config)
def load_state(self, state: dict[str, Any]) -> None:
# No-op: GridFSResource holds no local content. Reconstruction
# happens via the resources= override at load time.
pass