Commit Graph

11 Commits

Author SHA1 Message Date
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
bytecii f328bc0314 fix(parity): truncate/split data-loss flag values, od radix, registry membership, and the gate that missed them (#609 Block A) (#706) 2026-08-08 02:28:52 -07:00
Zecheng Zhang d13ec69ba4 feat(cli): himalaya and gws as builtin CLI packages plus integ cli facet (#699)
* feat(cli): himalaya and gws as builtin CLI packages plus integ cli facet

* fix(cli): flake8 re-exports, gws refreshFn config, docs register_cli, EmailConfig to core
2026-08-03 18:03:12 -07:00
Zecheng Zhang 24fba5d132 Finish the fskit size push-down: github_ci, trello, notion, jaeger, qdrant, lancedb, discord, email, langfuse, chroma (#667)
* feat(sizes): render-at-readdir sizes for github_ci, trello and notion

Batch 2 of the fskit size push-down (plan: ~/Desktop/fskit.md). Each
listing already carries the payload read renders, so readdir seeds a
sized IndexEntry and stat serves it. Zero added API calls.

github_ci workflow/run/job JSON and trello workspace/board/list/card
JSON use the shared node-dir shape: the parent readdir set_dirs each
child with a sized node.json, and the child readdir consults list_dir
before bootstrapping the parent. Notion differs because a database dir
listing is dynamic, so database.json's size travels in
extra[database_json_size].

Artifacts stay class T: size_in_bytes is the zip's exact byte length,
confirmed against two live samples. Flags stay False for all three:
job logs, annotations, comments.jsonl and page.json are class N.

integ gains a github_ci target backed by a fixed Actions dataset in the
fake server, with 24 stat==read pins.

* feat(sizes): render-at-readdir sizes for jaeger, qdrant and lancedb

Batch 3 of the fskit size push-down. qdrant and lancedb flip
SIZES_ALWAYS_KNOWN; jaeger stays False because operations.json is class
C and traces are class P.

jaeger traces are genuinely P: verified live that the /api/traces search
document renders byte-identically to the by-id fetch for all four seeded
traces, including the one listed under two services. operations.json is
sized by one /api/operations call at the service dir readdir.

lancedb needed table_columns/tableColumns to widen the select beyond
ids; it excludes vector and blob columns in schema order so projected
rows render byte-identically. Blob entries stay unsized on purpose,
since sizing a blob at listing time would let one undecodable value fail
the whole directory listing rather than just that file's read.

A previously pinned integ expectation was wrong: jg_read_28 asserted du
-sh printed 0B, which was an artifact of unknown sizes, and now prints
the real total.

* feat(sizes): sizes for discord, email, langfuse and chroma

Batch 4, the last of the fskit size push-down. discord, email and chroma
flip SIZES_ALWAYS_KNOWN; langfuse stays False because traces and prompts
are class N.

chroma's producer-supplied size was measurably wrong for every file in
our own fixture: 180 declared against 166 rendered, 90 against 52, and
150 against 159, which truncated reads. It describes the source document
rather than the chunk join mirage serves, so it can never be the byte
length. It moved to extra.source_size and the real size is measured by
one batched chunk scan per directory, run lazily from stat and cached.
This also corrected provenance byte accounting and du totals.

langfuse items.jsonl follows the class C shape: the dataset dir readdir
makes the one call and seeds a sized entry.

Carries the discord fake-API arc. The new integ/server/discord_server.py
exposed four real backend bugs: message pagination used the oldest id as
the after cursor when discord answers newest-first, attachments were
classified as directories, unknown paths returned an empty listing
instead of ENOENT/ENOTDIR, and a cold ls of a guild dir ENOENTed in
python only.

* fix(sizes): propagate backend failures from the stat parent-listing fallback

Review follow-up on the stat fallback these three backends use to
populate a cold index: the TypeScript side caught every error from the
parent readdir and fell through to ENOENT, so an auth failure, a rate
limit or a transport error read back as "file not found". Python already
caught only FileNotFoundError. Now only isMissingPath errors fall
through, which is the rule that helper's own contract states.

Reachable through the new size lookups, since workspace.json and the
other sized files resolve through this path on a cold index.

Also pins that discord's base_url config key reaches DiscordResource as
baseUrl through the registry: normalizeFields snake-to-camels by default
so no explicit rename is needed, and a silent miss there would send
configured mounts to the public API.
2026-07-31 21:21:35 -07:00
Zecheng Zhang 1c193bc4e8 Optional-typing sweep: path-only generic ops, NULL_INDEX, no bare generics (#541)
* refactor(types): stdin annotations use the ByteSource alias

* refactor(generics): injected ops are path-only, accessor+index bind at the wrapper

Generic commands no longer take accessor (or a dead index) just to
thread them back into injected callables. Builders and bespoke wrappers
bind both via bound_op (None-passthrough) or partial for write-side
ops, mirroring the TS builders' closures. call_*/resolve_pattern/relay
helpers and the cache read-through wrappers gain path-first forms;
CommandIO-level ops keep the raw (accessor, path, index) shape.

* refactor(index): NULL_INDEX everywhere, no index-is-None branches

index is a required IndexCacheStore on every op and wrapper; callers
with no real index pass NULL_INDEX (the null object built for exactly
this). Dead is-None guards drop out of trello/linear/github_ci/google
readdir+stat, CacheManager, and the github wrappers.

* style(types): parameterize every bare generic, gate with disallow_any_generics

dict -> dict[str, Any] (payloads) / dict[str, object] (flag bags),
Callable -> Callable[..., Any], plus list/tuple/Awaitable/Pattern/
Task/Future/Token/AsyncMongoClient. The mypy gate now carries
disallow_any_generics so bare generics cannot come back.

* style: pre-commit formatting + parameterize main's new bare dicts

* fix: post-merge gate fixes + make security barriers analyzer-recognizable

- resolve_within_root: startswith(root + sep) guard instead of
  commonpath (same semantics; the shape CodeQL models as a
  path-injection barrier)
- cli table output: trimEnd() instead of the polynomial /\s+$/ trim
- example proxies: re-encode validated endpoint segments before URL
  construction (recognized SSRF barrier; no-op for the allowed charset)
- parameterize bare generics arriving from #540 (the new mypy gate
  caught them)

* fix(server): plain startswith barrier in resolve_within_root
2026-07-17 20:45:19 -07:00
bytecii cb7dd54d2c refactor: delete per-backend glob boilerplate, open a custom-backend SDK (#540)
Glob dedup (Python + TypeScript):
- Delete all 59 per-backend glob files (30 core/*/glob.py, 29 glob.ts);
  resolvers now bind from one make_resolve_glob/makeResolveGlob in
  utils/glob_walk with DEFAULT_MAX_GLOB_MATCHES = 10000.
- Python gains per-backend commands/builtin/<b>/ops.py CommandIO
  manifests (mirrors TS ops.ts); bespoke commands import RESOLVE_GLOB
  from there, resources bind make_resolve_glob(readdir, cap) directly.
- Unify the SCOPE_ERROR mess: per-backend readdir thresholds stay in
  core/<b>/constants; the glob cap is the shared default; delete the
  unused SCOPE_* from commands/builtin/constants; kill TS cross-backend
  constant imports. TS ssh gets its own constants.ts (cap 5000, Python
  parity, was borrowing disk's 50000).
- Move is_cross_run_root/isCrossRunRoot into core/github_ci/readdir.

Custom-backend SDK:
- mirage/sdk.py: blessed public surface for out-of-tree backends
  (BaseResource, GenericResource, CommandIO, factories, command/op,
  specs, types).
- GenericResource: one CommandIO table -> full generic command set +
  resolve_glob, with overrides/commands/ops/provision escape hatches.
- resource/registry.py: register_resource(), mirage.resources
  entry-point discovery, known_resources(), public resolve_class;
  ResourceEntry accepts classes or loader specs.
- examples/python/other/custom_resource.py: complete one-file backend;
  docs/python/resource/new.mdx split into SDK vs builtin paths.

Dead code: core/gdrive/stream.py -> drive.download_file_stream ->
_client.google_get_stream chain (+tests/mocks), never imported.

Safety nets: golden ops-inventory snapshots (Python 241 ops/31
backends; TS 171 ops/28 backends) as the regression gate for the
upcoming ops-layer factory refactor.

Behavior changes (deliberate):
- Previously uncapped generic-command glob expansion now truncates at
  10000 with a warning.
- TS cap hacks converge: 5000 cross-imports and 1024 inline consts move
  to the 10000 default (matches Python); TS ssh 50000 -> 5000.

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
2026-07-17 20:04:50 -07:00
Zecheng Zhang 5244558e29 Fix latent type-safety and lifecycle bugs (#528)
* Fix latent type and lifecycle bugs

* fix: close ownership for shared resources, gate find -empty, gdrive populate parity, drop mongo DriverInfo

* style: yapf/isort formatting on merged test files

* fix: fan-out grep keeps GNU any-match exit 0, mock github_ci stat populate readdir, current mock s3 mtime

* fix(ts): mirror close ownership, shared stores and copy-shared resources stay open

* refactor: shared op-fn aliases and copy/move strategies in mirage.types, tar package with types and constants

* refactor: single mtime-window primitive, drop msgraph.time re-export, github declares supportsSnapshot

* fix: find -mtime keeps unknown-mtime dirs (s3 integ), mktemp -p PATH-kind spec parity

* fix(find): remote -mtime reverts to TS parity, onedrive root stat carries mtime

- generic_bind find builder: stat filter only for local backends (matches
  main + TS; remote backend find ops ignore mtime like the TS s3 core)
- s3/onedrive core find: drop in-core mtime filtering, document the
  deliberate no-op in the s3 docstring
- onedrive stat: mount root fetches the real Graph root item so
  size/modified are populated (du/find no longer see modified=None)
- integ: fake Graph stamps items at run time like moto; bespoke
  onedrive suite freezes the clock for deterministic ls -l; stat_dir
  case prints stable fields only

* test: drop remote find -mtime tests reverted to TS-parity semantics
2026-07-17 17:58:39 -07:00
Zecheng Zhang ea8096f155 Drive Python mypy to zero and enable it as a hard gate (#522)
* chore(py): shrink mypy baseline 117->84 (resource accessor typing)

Narrow each resource's accessor attribute to its concrete type so the
backend resolve_glob/stat calls type-check; wrap fingerprint stat() paths
with PathSpec.from_str_path (fixes a latent str-to-stat bug); retype the
shared hf_buckets core to _HfAccessor (common base of the hf siblings);
make DevStore a RAMStore subclass (_DevFiles a dict subclass); narrow the
sync redis get_state() reads (redis-py ResponseT is an async/sync union).

Full Python test suite green (9139 passed; fuse excluded, macOS limit).

* chore(py): shrink mypy baseline 84->74 (workspace/executor/shell/resource typing)

* chore(py): clear non-generic mypy errors, shrink baseline 74->56

* chore(py): clear all generic command-layer mypy errors (127->0)

* chore(py): enable mypy as a hard gate (pre-commit + CI), drop ratchet baseline

* fix(py): sed -i guards write availability; dedupe mypy pre-commit hook post-merge

* fix(py): pyproject trailing newline; run mypy on hosted pre-commit too
2026-07-16 12:57:40 -07:00
Zecheng Zhang 97df4db0c8 Merge origin/main; replace is_remote with caches_reads
Resolve cache-consistency conflict: keep the single CacheManager
invalidation path and drop invalidate_index_dirs.

Remove is_remote and cacheable in favor of a single caches_reads flag
declared per resource. Live-insert databases (postgres, mongodb,
chroma) set caches_reads=False so reads always hit the backend;
lancedb caches only for remote-scheme tables. Fix the postgres and
chroma cold grep -e multi-pattern path (route multi-pattern to the
generic grep instead of the single-pattern pushdown).
2026-06-16 21:04:27 -07:00
Zecheng Zhang 3b966d20aa add resource secret redaction (#85)
* add resource secret redaction

* dedupe gmail config
2026-05-22 15:27:48 -07:00
Zecheng Zhang 924ec49796 Initial public release: Mirage v0.0.1-alpha.1
A unified virtual filesystem for AI agents. Mount S3, Google Drive,
Slack, Gmail, GitHub, Linear, Notion, Postgres, MongoDB, SSH, and
more behind one filesystem so agents read, write, and pipe across
services with familiar shell commands.

Ships Python (mirage-ai) and TypeScript (@struktoai/mirage-*) SDKs,
a CLI, FUSE mounts, and adapters for OpenAI Agents SDK, Vercel AI
SDK, LangChain deepagents, Pydantic AI, CAMEL, OpenHands, Mastra,
Pi Coding Agent, plus FUSE-based integration with Claude Code and
Codex.

Apache 2.0 licensed.
2026-05-06 10:03:22 -07:00