Skip to content

feat(wasm): host-backed filesystem for the wasm bindings - #2273

Closed
claude[bot] wants to merge 3 commits into
mainfrom
claude/bashkit-backend-cafw4w
Closed

feat(wasm): host-backed filesystem for the wasm bindings#2273
claude[bot] wants to merge 3 commits into
mainfrom
claude/bashkit-backend-cafw4w

Conversation

@claude

@claude claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

What changed

new Bash({ fs }) in the wasm bindings now runs scripts against storage the
embedder 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, and rename are synthesized
from the required set when omitted; chmod is accepted and ignored so
chmod +x works against hosts with no permission model; symlink / readLink
report Unsupported because they cannot be faked.

Host errors carrying a POSIX code map onto the matching io::ErrorKind, so
builtins 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, and
    executeSync cannot await — it reports the suspension instead of blocking.
  • files is rejected alongside fs. Seeding writes through now_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/computer
backend that runs bashkit inside a Durable Object against the workspace's
SQLite directly, but the contract is generic.

Before / After

Beforefs was not an option; scripts saw only the in-memory VFS, and an
embedder's bytes had to be seeded through files and read back through
bash.readFile(...).

After — a script reads and writes the host's store directly:

stdout: "1\n" exit: 0
host store now holds: / /w /w/notes.md /w/count.txt
count.txt in the host: "1\n"
missing file -> "cat: /w/gone.txt: io error: no such file: /w/gone.txt\n" exit: 1

/w/count.txt was never in the interpreter — the redirect landed in the host
Map, and the host's own ENOENT came back through cat as a normal shell
failure.

Test suite (node --test "crates/bashkit-wasm/__test__/*.test.mjs"):

# tests 63
# pass 63
# fail 0

19 of those are new. The fake host in host-fs.test.mjs resolves every method
on 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 are
    byte-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/readDir shapes
    drift from the contract (surfaces as a clear must resolve to … error, not a
    wrong answer); a script reaching for symlinks against a host that omits them
    (ENOSYS); scripts redirecting to /dev/null, which a host filesystem must
    now 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
    RealFs takes. Paths are normalized by PosixFs before any host call, so
    traversal cannot select a path the embedder did not scope.

  • Public docs: docs/filesystem.md gains a "Host-backed filesystem (JS)"
    section (cross-linked from Binding parity) and docs/start-node.md points
    at it from Sandbox options, per the docs requirement added in chore(ship): require public docs for user-facing features #2270.

Checklist

  • Tests added or updated
  • Backward compatibility considered

Generated by Claude Code

chaliy added 3 commits August 6, 2026 03:48
`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.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@chaliy

chaliy commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Superseded by #2275, recreated under my account and merged.

@chaliy chaliy closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant