Closes the two remaining small items of Block E. Both were largely recon: much of what they filed had already been fixed, and the part that had not turned up a real GNU divergence. Item 31 (T3-7), rule/doc reconciliation --------------------------------------- The `object`-parameter half is already done: #825's mypy inversion took the package from the filed 28 down to 7, of which 3 are docstring prose and the remaining 4 are protocol methods (`__contains__`, `pop`) whose signatures typeshed dictates, all already listed in the no-object gate. `utils/errors.py` reads `str | PathSpec` today, and `builders/sed.py` no longer holds flag values at all -- item 22 moved that to the generic. That left the nested-function rule, which the plan asked to decide before gating. Measured: 39 nested defs, and **every one of them closes over its enclosing scope** -- 38 by free variable, and `sed_helper._repl` by binding through parameter defaults, the loop-variable idiom, with a comment saying so. A flat "do not nest" is a rule the architecture cannot keep (op factories, provision builders, the read-through cache, every decorator's wrapper), so gating it as written would have meant 39 standing violations. So the rule now says what it was reaching for: a nested def must capture the scope around it, or it belongs at module level. That is mechanical and enforceable -- `tests/test_nested_functions_are_closures.py` reads free variables from `symtable` and parameter defaults from the AST. It passes on the tree as-is and fires on a helper that reads only its own arguments. Layout: the eight test-only TS directories are flattened (`ram/{cat,cut,grep,head,ls,tail,wc}/`, `ssh/ls/`), `awk_helper.ts` moves up beside its nine siblings, and the `provision.ts` / `_provision.ts` split is settled on Python's convention -- `_provision` for a backend's own, bare `provision` for the shared ones (cli, generic_bind). That last one was not cosmetic: Python already had `_provision.py` for github, gmail, redis and email, so renaming **closed 4 real parity divergences** (layout baseline 296 -> 292). The `findEval`/`findParse` line was already done in #827. Item 32 (T3-8), test/CI tidiness -------------------------------- The two asymmetric `ls` conformance matrices are raised to symmetry, and they pass: python `[ram,disk,redis]` vs typescript `[ram]` was stale caution, not a divergence. Both runners now reject an asymmetric matrix at load time unless the case carries a `divergence` key explaining why -- a case is a parity claim, and narrowing one side reads as coverage while the side that still lists the backend goes green. Loading the corpus under the new assertion proves no other case was asymmetric. The README documented the override as "not yet needed"; it exists now. Adds the `ts-audit` job, mirroring `test_python.yml`'s exactly (same `continue-on-error`, same out-of-gate placement). Nothing ran `pnpm audit` for the TS tree while `typescript/package.json` carried 27 hand-written CVE overrides that only a person remembering to check kept current. Integ facets for `ls`: `-A`, `-d`, `-r`, `-S`, `-h`, all pinned against GNU coreutils 9.7 in docker. `-S` uses regular files only, because GNU sorts a directory by its inode size while mirage counts it as 0 -- a divergence CLAUDE.md documents deliberately. The `-h` facet found a real bug ------------------------------- `ls -h` disagreed with GNU three ways, and because the flag had zero cross-backend coverage nothing caught it. GNU prints a count below one unit with no suffix (`24`); mirage printed `24B`. GNU rounds *up* to the precision shown (1025 bytes is `1.1K`); mirage gave `1.0K`. GNU drops the decimal once the value reaches ten (`10K`); mirage gave `10.0K`. One shared engine replaces both formatters in both languages -- GNU runs `-h` and `-H` through one `human_readable`, and so do we now. Rounding up can carry past the base (1048575 ceils to 1024K, which GNU shows as `1.0M`), so the unit is re-chosen after rounding rather than once up front. 21 GNU-read points are pinned as a table in each language. Blast radius, all re-pinned: five integ cases that had recorded the `B` suffix (du, discord, langfuse, email, gmail), and the du fan-out tests. Those last ones needed care rather than a new number: they exist to prove the total is humanized once instead of twice, and their 1500+1500 stops discriminating under correct rounding, since 3000 bytes and two `1.5K` readings both render `3.0K`. They now use 1025+1025, where single-rounding gives `2.1K` and double-rounding would give `2.2K`. Verified: pre-commit clean, integ 8054/0 on ram+disk+redis in both languages, conformance green in both, and the layout, spec, barrel, docs, case-target and PathSpec gates all pass. Not covered, still open on item 32: integ facets for `ls -t` (mtime order is not stable across backends without a seeded fixture), mktemp, gzip, the checksum `--check` companions, `df -H/-k/-a`, and `cmp -n/-b/-i`; and the T2-7 helper unit-suite mirroring. Item 31 leaves the provision *presence* sets unreconciled (Python has lancedb/qdrant, TS has trello) -- that is a question about which backends should provision at all, not about naming. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirage is a Unified Virtual File System for AI Agents: it mounts services and data sources like S3, Google Drive, Slack, Gmail, and Redis side-by-side as one filesystem. Any LLM that already knows bash can read, grep, and pipe across every backend out of the box, with zero new vocabulary.
ws = Workspace(
{
"/tmp": (RAMResource(), MountMode.EXEC),
"/redis": (RedisResource(url=redis_url), MountMode.WRITE),
"/slack": (SlackResource(SlackConfig(token=slack_bot_token)), MountMode.EXEC),
},
# monty captures python, so scripts run sandboxed inside the workspace
runtimes=[MontyRuntime(captures=["python", "python3"]), "vfs"],
)
# one grep sweeps every source
await ws.execute("grep -rln session /redis /tmp")
# run a script that lives in Slack, file the report into Redis
await ws.execute(
"python3 /slack/channels/general__C0.../files/example__F0....py > /redis/report.txt"
)
# install a typed CLI under a head word: dispatched by name, not by path,
# and discoverable through `man`, `type` and `which` like any other program
ws.register_cli("slack", SLACK, {"token": slack_bot_token})
await ws.execute('slack send-message --channel general --text "report is up"')
About
- One interface instead of N SDKs and M MCPs. Every service speaks the same filesystem semantics, and pipelines compose across services as naturally as on a local disk.
- Around 50 built-in backends: RAM, Disk, Redis, S3 / R2 / OCI / Supabase / GCS, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Email, MongoDB / GridFS / Postgres / LanceDB / Qdrant, SSH, and more, mounted side-by-side under a single root.
- Portable workspaces: clone, snapshot, and version a workspace; agent runs move between machines without restarting or reconfiguring the system.
- Embeddable: the Python and TypeScript SDKs run in-process inside FastAPI, Express, browser apps, or any async runtime; no separate process required.
- Agent integrations: OpenAI Agents SDK, Vercel AI SDK, LangChain, Pydantic AI, CAMEL, and OpenHands via the SDKs; coding agents through native adapters, installable plugins, MCP, or FUSE.
Architecture
Installation
- Python ≥ 3.11 for the
mirage-aipackage and themirageCLI - Node.js ≥ 20 for the TypeScript SDK
- macOS or Linux (FUSE-based mounts require platform support)
Python
uv add mirage-ai # installs the `mirage` library and the `mirage` CLI binary
TypeScript
npm install @struktoai/mirage-node # Node.js servers and CLIs
npm install @struktoai/mirage-browser # browser / edge runtimes
npm install @struktoai/mirage-agents # OpenAI / Vercel AI / LangChain / Mastra adapters
Both runtime packages pull in @struktoai/mirage-core automatically.
CLI
curl -fsSL https://strukto.ai/mirage/install.sh | sh
# or
npm install -g @struktoai/mirage-cli
# or
uvx mirage-ai
# or
npx @struktoai/mirage-cli
Quickstart
Python
from mirage import Workspace
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
ws = Workspace({
"/data": RAMResource(),
"/s3": S3Resource(S3Config(bucket="my-bucket")),
})
await ws.execute("cp /s3/report.csv /data/report.csv")
await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
await ws.snapshot("demo.tar")
TypeScript
import { Workspace, RAMResource, S3Resource } from '@struktoai/mirage-node'
const ws = new Workspace({
'/data': new RAMResource(),
'/s3': new S3Resource({ bucket: 'my-bucket' }),
})
await ws.execute('cp /s3/report.csv /data/report.csv')
await ws.execute('grep alert /s3/data/log.jsonl | wc -l')
await ws.snapshot('demo.tar')
CLI
mirage workspace create ws.yaml --id demo
mirage execute --workspace_id demo --command "cp /s3/report.csv /data/report.csv"
mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
mirage workspace snapshot demo demo.tar
mirage workspace load demo.tar --id demo-restored
Agent Frameworks
Mirage plugs into agent frameworks as a sandbox or tool layer. POSIX operations such as read can also be customized per resource and filetype: Mirage ships no filetype renderers, so a format renders however you register it, and a command registered for one resource and extension wins over the generic one.
| Integrations | |
|---|---|
| Python | OpenAI Agents SDK, LangChain, Pydantic AI, CAMEL, OpenHands, Agno |
| TypeScript | Vercel AI SDK, OpenAI Agents SDK, LangChain, Mastra |
| Coding agents | Claude Code, Codex, DeepSeek Harness, Grok Build, OpenCode, Pi |
Cache
Every Workspace has a two-layer cache so repeated work against remote backends hits local state instead of the network:
- Index cache: listings and metadata. The first directory walk hits the API; later ones serve from the index until the TTL expires (default 10 minutes).
- File cache: object bytes. The first read streams from origin; later pipelines read from cache (default 512 MB).
Both layers default to in-process RAM with zero setup. A Redis store shares cache state across workers, processes, and machines:
import { RedisFileCacheStore, S3Resource, Workspace } from '@struktoai/mirage-node'
const ws = new Workspace(
{ '/s3': new S3Resource({ bucket: 'my-bucket' }) },
{
cache: new RedisFileCacheStore({ url: 'redis://localhost:6379/0', cacheLimit: '8GB' }),
index: { type: 'redis', url: 'redis://localhost:6379/0', ttl: 600 },
},
)
See the cache docs for the full miss/hit lifecycle.
Contributors
Thanks to everyone who has contributed to Mirage.
