python3 has its own set of WASM-runtime-level divergences from Python Mirage’s subprocess model. See Python for the full list.Node
1. fs-monkey only patches CJS require('fs'), not ESM node:fs
The problem. patchNodeFs() routes fs calls through the workspace VFS so that third-party libraries “just work” against mounted paths. It works by replacing require.cache’s fs entry (a CJS-only mechanism). ESM is fundamentally different:
import { readFile } from 'node:fs/promises'resolves at parse time to V8’s internal binding.- There is no public hook to replace that binding after the fact.
- Loader hooks (
--loader=…) could intercept the resolution, but they’re a build-time decision, not a runtime monkey-patch.
with Workspace() as ws: swaps builtins.open and sys.modules["os"]: one set of mutable globals, one patch point, works for every caller.
Workarounds.
- Use FUSE instead
- Use Mirage's VFS API directly
- Restrict your code to CJS
If you want ESM-imported
node:fs to see your mounted data, expose the workspace as a real filesystem. Mount FUSE, then every fs call, ESM or CJS, goes through the kernel.node:fs through the workspace would remove the ESM limitation, at the cost of forcing consumers to opt into the loader (node --loader @struktoai/mirage-node/loader main.mjs). Not planned currently.
2. The FUSE mount is served by your process’s event loop
The problem.@zkochan/fuse-native dispatches every FUSE request (getattr, read, …) as a callback on the mounting process’s Node event loop. Any synchronous access to your own mountpoint from that same process (readFileSync, statSync, execFileSync('cat', …)) blocks the loop that must answer the request: the call deadlocks, the kernel eventually times the filesystem out, and later operations fail with Device not configured / ENOTCONN.
fs APIs and async subprocess spawns from the mounting process, or dedicate a child process to owning the mount (see examples/typescript/fuse/helper.ts for the pattern). External processes (a sandbox runtime, another terminal, your shell) are unaffected — the constraint only binds the process that created the mount.
3. Size-unknown API files stat as 0 bytes until first open
The behavior. API-backed resources (Trello, Linear, Slack, MongoDB, …) returnstat.size = null because the byte size isn’t known until the API has been called. Over the FUSE mount these files behave like Linux /proc files: they stat as 0 bytes until first open, and become fully readable the moment anything opens them. Mirage mounts with direct_io (the kernel reads to EOF regardless of the reported size; @zkochan/fuse-native doesn’t expose the option, so Mirage appends it to the mount option string itself) and attr_timeout=0 (post-open fstat returns the real size of the now-fetched content, kept warm in a 30-second cache).
What that means per tool:
Mirage never reports a fake size and never fetches content during
stat: returning real sizes eagerly would fire one API call per file on every ls -l. Earlier TS versions instead reported a fake 100 MiB size with zero-padded reads (JSON.parse of a FUSE-read file failed on the padding); that sentinel is gone and reads are byte-exact.
Why Python behaves the same. Unlike the other entries on this page, this one is not TS-specific: it is a design property of API-backed mounts, and Python’s FUSE layer has identical semantics (see Python FUSE setup). It is listed here because the stat-time zeros are the most common FUSE surprise in practice.
Browser
The browser SDK runs entirely in-page: no kernel, no subprocesses, no Nodefs. That removes the Node sections above (neither FUSE nor fs-monkey apply) but introduces its own constraints.
1. No FUSE
Browsers can’t mount filesystems. A workspace with a fuseMount throws on construction. Use ws.execute(...) (virtual executor) and ws.fs.readFile/writeFile instead. Every builtin (cat, grep, jq, awk, python3, etc.) is reimplemented in-process, so most agent code paths work unchanged from Node.
2. OPFS quotas and persistence
OPFSResource writes through the Origin Private File System. Two things to know:
- Storage quota. The browser sets per-origin quotas (typically a fraction of free disk, single-digit GB on most setups). Hitting it raises
QuotaExceededError. Callnavigator.storage.estimate()to inspect. - Eviction. Origins that aren’t persisted can be cleared by the browser under storage pressure. For long-lived workspaces, request
navigator.storage.persist()early.
3. CORS for HTTP-backed resources
Mounts that hit third-party APIs (S3, GitHub, Linear, etc.) makefetch calls from the page. Anything not configured to allow your origin via CORS will fail with the usual browser error. Workarounds:
- Browser-native auth flows. Resources like Box, Dropbox, GDrive, GDocs ship PKCE OAuth examples that work entirely in-browser.
- Pre-signed URLs. For S3/R2/GCS, generate pre-signed URLs server-side and pass them in. The Mirage browser examples include a Vite dev-server
presignerplugin as reference. - Same-origin proxy. Stand up a tiny proxy on your own domain that forwards to the upstream API with the right auth headers.
4. Python writes need JSPI
The Python FS shim (open() against mounted paths) flushes writes back through an async bridge. That requires JSPI. Reads of preloaded files still work without it, but close() on a write throws RuntimeError: Cannot stack switch.
- Chrome / Edge 137+ (May 2025): works out of the box.
- Firefox: behind
javascript.options.wasm_js_promise_integration. - Safari: not yet shipped.
5. Some resource drivers remain Node-only
SSH, LanceDB, Email, Disk, Redis, and the Hugging Face resources are only exposed by@struktoai/mirage-node because their drivers need Node APIs or raw TCP. Importing them from @struktoai/mirage-browser is a build error.
Postgres and MongoDB do have browser implementations:
- Postgres uses
NeonPgDriverover HTTP. - MongoDB uses
HttpMongoDriverand an HTTP proxy such as the bundled mongo-proxy example.
Quick reference
The Node limitations are runtime-level, not Mirage design decisions. Python’s equivalent behavior is strictly better in each case, so if a workflow absolutely requires ESM-level
fs patching or large API-backed FUSE reads, consider whether the Python SDK fits better for that specific piece. The Browser limitations are by design (no kernel, no subprocess), so the workarounds there are about choosing the right runtime for the task.