feat(wasm): host-backed filesystem for the wasm bindings - #2273
Closed
claude[bot] wants to merge 3 commits into
Closed
feat(wasm): host-backed filesystem for the wasm bindings#2273claude[bot] wants to merge 3 commits into
claude[bot] wants to merge 3 commits into
Conversation
`new Bash({ fs })` runs scripts directly against storage the embedder
owns — a Durable Object, OPFS, IndexedDB — instead of the in-memory VFS.
Nothing is copied in or diffed back out: every read and write during a
run is a call into the host object, so there is no workspace-size ceiling
beyond the host's own and no lost-update window between runs.
The bridge implements `FsBackend` and wraps it in `PosixFs`, so hosts
supply raw storage (seven required methods, validated at construction)
and inherit POSIX semantics. `append`, `copy`, and `rename` are
synthesized when omitted, `chmod` is accepted and ignored so `chmod +x`
works against hosts with no permission model, and host errors carrying a
`code` map onto the matching `io::ErrorKind` so builtins that branch on
kind behave as they do over the built-in VFS.
Host calls may return promises, so a host filesystem implies `execute()`;
`executeSync` reports the suspension rather than blocking, and `files` is
rejected alongside `fs` because seeding cannot complete synchronously.
A host that answers `undefined` is broken, and reading that as "missing" turns every existence probe into a silent miss — a redirect reports the workspace as gone rather than reporting the host. Fail with a message that names the contract instead. Also documents the host filesystem as TM-FS-017: it widens the sandbox to whatever the embedder's object exposes, and its bytes live outside the VFS quotas, so scoping and storage limits are the embedder's.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 23e4762 | Commit Preview URL Branch Preview URL |
Aug 06 2026, 04:01 AM |
Contributor
|
Superseded by #2275, recreated under my account and merged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
new Bash({ fs })in the wasm bindings now runs scripts against storage theembedder owns — a Durable Object, IndexedDB, an OPFS handle — instead of the
built-in in-memory VFS. Nothing is copied in and nothing is diffed back out:
every read and write during a run is a call into the embedder's object, so
there is no workspace-size ceiling beyond the host's own and no lost-update
window between runs.
Hosts implement raw storage only — seven required methods, validated at
construction so a missing one fails loudly instead of surfacing mid-script.
POSIX semantics (parent checks, "is a directory", symlink resolution) stay in
PosixFs, so hosts stay small.append,copy, andrenameare synthesizedfrom the required set when omitted;
chmodis accepted and ignored sochmod +xworks against hosts with no permission model;symlink/readLinkreport
Unsupportedbecause they cannot be faked.Host errors carrying a POSIX
codemap onto the matchingio::ErrorKind, sobuiltins that branch on kind behave exactly as they do over the built-in VFS.
Two contract points, both enforced rather than documented-and-hoped:
execute()only. A host call can suspend the interpreter, andexecuteSynccannot await — it reports the suspension instead of blocking.filesis rejected alongsidefs. Seeding writes throughnow_or_never,which a promise-returning host can never satisfy, so the combination is
refused at construction instead of silently dropping the seed.
Why
The interpreter could only ever run against its own in-memory VFS, so any
embedder with real storage had to copy the tree in before a run and diff it
back out after — O(workspace) per execution, text-only, and racy the moment
anything else writes. The immediate consumer is a
@cloudflare/computerbackend that runs bashkit inside a Durable Object against the workspace's
SQLite directly, but the contract is generic.
Before / After
Before —
fswas not an option; scripts saw only the in-memory VFS, and anembedder's bytes had to be seeded through
filesand read back throughbash.readFile(...).After — a script reads and writes the host's store directly:
/w/count.txtwas never in the interpreter — the redirect landed in the hostMap, and the host's own ENOENT came back through
catas a normal shellfailure.
Test suite (
node --test "crates/bashkit-wasm/__test__/*.test.mjs"):19 of those are new. The fake host in
host-fs.test.mjsresolves every methodon a later microtask, so the suspend/resume path is what's under test rather
than an accidentally-synchronous store.
Risk
Low. Purely additive: without
fs, construction and execution arebyte-for-byte the path they were before. The bridge is only reachable through
the new option.
What can break, in order of likelihood: a host whose
stat/readDirshapesdrift from the contract (surfaces as a clear
must resolve to …error, not awrong answer); a script reaching for symlinks against a host that omits them
(
ENOSYS); scripts redirecting to/dev/null, which a host filesystem mustnow supply itself since there is no VFS underneath.
Security: recorded as TM-FS-017. A host filesystem widens the sandbox to
whatever the embedder's object exposes, and its bytes live outside the VFS
quotas — so scoping and storage limits are the embedder's, the same stance
RealFstakes. Paths are normalized byPosixFsbefore any host call, sotraversal cannot select a path the embedder did not scope.
Public docs:
docs/filesystem.mdgains a "Host-backed filesystem (JS)"section (cross-linked from Binding parity) and
docs/start-node.mdpointsat it from Sandbox options, per the docs requirement added in chore(ship): require public docs for user-facing features #2270.
Checklist
Generated by Claude Code