Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
99babd6
computerd: vendor fuse-napi and port the mount to libfuse 3
aron-cf Oct 1, 2026
758685a
computerd: update the mount option and driver tests for libfuse 3
aron-cf Oct 2, 2026
094f239
computerd: Keep oxlint and oxfmt out of the vendored fuse-napi tree
aron-cf Oct 14, 2026
b15e32e
computerd, computer: Build the binary against the system libfuse 3
aron-cf Oct 5, 2026
6fba45b
computerd: Make the libfuse 3 mount work with fuse-napi
aron-cf Oct 5, 2026
c5c6ba3
computerd: Hand local-only files to the kernel with FUSE passthrough
aron-cf Oct 5, 2026
8e148a7
computerd: Report live passthrough state on /__computerd/info
aron-cf Oct 5, 2026
6aa78c0
computerd: Test FUSE passthrough against a real mount
aron-cf Oct 5, 2026
0a68156
ci: Install libfuse 3 headers and drop libfuse 2
aron-cf Oct 5, 2026
b6f7887
chore: Move the FUSE images to libfuse 3
aron-cf Oct 5, 2026
016ddba
computerd: Benchmark local-only paths natively on arm64
aron-cf Oct 5, 2026
543da34
computerd: Document libfuse 3 and local-only passthrough
aron-cf Oct 5, 2026
fa693f8
computerd: cleanup code comments
aron-cf Oct 5, 2026
d48158d
computer: Let the computerd harness pick the platform and image
aron-cf Oct 22, 2026
51d2915
computerd: Bind-mount local-only directories named in MOUNT_NATIVE
aron-cf Oct 22, 2026
ac17125
computerd, docs: Benchmark native roots
aron-cf Oct 22, 2026
fb6b789
Merge main into computerd-passthrough
aron-cf Oct 22, 2026
a301073
docs, examples: Date libfuse 2 to 0.6.0 and earlier
aron-cf Oct 22, 2026
68414d1
computerd: Mount every local-only directory natively
aron-cf Oct 22, 2026
acf7700
computerd: Let commands remove and rename native roots
aron-cf Oct 22, 2026
f01e220
computerd: Test root removal through the guard under real FUSE
aron-cf Oct 22, 2026
c6ed27d
computerd: Reproduce restore-time bind escape
aron-cf Oct 23, 2026
c40b7aa
computerd: WIP bind native roots with pinned fds
aron-cf Oct 23, 2026
4f379a1
Merge origin/main into computerd-passthrough
aron-cf Oct 23, 2026
eef8037
computerd: Condense FUSE performance changeset
aron-cf Oct 23, 2026
49bef86
computerd, docs: Remove obsolete FUSE guidance
aron-cf Oct 23, 2026
7fc10f4
computerd: Simplify native-root guard handoff
aron-cf Oct 23, 2026
377e1ca
docs: Clarify native-root mount eligibility
aron-cf Oct 23, 2026
bf09921
computerd: Expect removal guard fields in FUSE info
aron-cf Oct 23, 2026
d562de8
computerd: Wait for pending native binds before detach
aron-cf Oct 23, 2026
de6822e
Merge origin/main into computerd-passthrough
aron-cf Oct 23, 2026
9935f66
Merge origin/main into computerd-passthrough
aron-cf Oct 26, 2026
51b29c1
Merge origin/main into computerd-passthrough
aron-cf Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions .agents/skills/debugging-computerd-fuse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ capnweb endpoints from a plain Node script.

There's already a recipe — `packages/computer/test-harness/run-computerd.sh`.
It picks a host port, runs the binary with `--privileged
--device /dev/fuse --cap-add SYS_ADMIN --cap-add MKNOD`, installs
fuse3 + libfuse2t64 from apt, waits for `/health`, prints the URL
on stdout and the container id on stderr.
--device /dev/fuse --cap-add SYS_ADMIN --cap-add MKNOD` on Debian
trixie with fuse3 from apt (computerd links the system libfuse 3 and
needs 3.17 or newer), waits for `/health`, prints the URL on stdout
and the container id on stderr. It forwards `MOUNT_IGNORE`,
`MOUNT_IGNORE_PATH`, and any `COMPUTERD_FUSE_*` variable from your
environment into the container.

```bash
# Start
Expand Down Expand Up @@ -179,8 +182,8 @@ running container:
docker exec <CID> sh -c 'echo container-write > /workspace/r2/hello.txt'
```

This is a real FUSE write: kernel → fuse-native → computerd's
`writeBuf` op → computerd's in-memory buffer for that file → on
This is a real FUSE write: kernel → fuse-napi → computerd's
`write` op → computerd's in-memory buffer for that file → on
`release`/`flush`/`fsync` the buffer spills into the backing VFS
(commit `68407fc`). On the host side you can then `pullOnce` and
see the new entry land in `applyChanges`.
Expand Down Expand Up @@ -261,9 +264,31 @@ goes through Node's `fs` against the mount point must be async

## Recipe library

### Is passthrough engaged?

With `MOUNT_IGNORE` set, an open of a local-only file should hand the
file to the kernel and keep computerd out of the data path. Two ways to
check, from inside the container:

```bash
# What the mount negotiated, and how opens were served.
curl -s localhost:8080/__computerd/info | jq .ignore.fastPaths

# Whether data still reaches the daemon. Boot with
# COMPUTERD_FUSE_TRACE=summary and COMPUTERD_FUSE_TRACE_FILE=/tmp/trace.json,
# do the I/O, then dump the counters. read and write should not move
# for local-only files.
kill -USR2 1 && jq '.ops[] | select(.op == "read" or .op == "write")' /tmp/trace.json
```

Keep the trace file outside the mount: a process serving a FUSE mount
deadlocks if it writes into that mount. `passthroughReason` names the
likely cause when passthrough is off, and `COMPUTERD_FUSE_PASSTHROUGH=0`
turns it off on purpose, to rule it in or out of a bug.

### Reset between probes

A wedged computerd container can leave fuse-native zombie processes
A wedged computerd container can leave FUSE zombie processes
that `docker kill` refuses to reap. The cleanest fix:

```bash
Expand Down Expand Up @@ -369,7 +394,7 @@ your host) so the inspector port is reachable, or `docker run
through computerd's launch env.

If even that doesn't surface it, the wedge is probably below the
JS layer — in fuse-native or libuv. At that point reach for
JS layer — in the fuse-napi binding or libuv. At that point reach for
`strace -fp <computerd-pid>` inside the container; the FUSE callbacks
all surface as `read(/dev/fuse, ...)` and the per-call timing
will show you exactly which op deadlocks.
32 changes: 32 additions & 0 deletions .changeset/libfuse3-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@cloudflare/computerd": minor
---

Move to libfuse 3, and serve local-only paths through FUSE passthrough

computerd now mounts through a vendored, patched copy of `fuse-napi`, which
binds libfuse 3, instead of `fuse-native` and libfuse 2.9. This changes what
the container image needs.

The binary links the system libfuse 3 and needs 3.17 or newer. On Debian that
is the `fuse3` package from trixie onward; bookworm's 3.14 is too old.
`libfuse2t64` is no longer needed. A missing or too-old library stops the
binary at startup with a message naming the package to install.

No macOS binary is published any more. Running from source with macFUSE has
not been tested since the move.

With `MOUNT_IGNORE` set and a kernel of Linux 6.9 or newer, opening a
local-only file hands it to the kernel, which then serves its reads, writes,
and mmap straight from the container's disk without going through computerd.
Without kernel support, or without `CAP_SYS_ADMIN`, those paths work as
before and are served by computerd. `/__computerd/info` reports which case
applies in `ignore.fastPaths`, with counts, and `COMPUTERD_FUSE_PASSTHROUGH=0`
turns passthrough off.

Three behavior changes on the synced mount come with the new binding.
`touch -a` and `touch -m` now set the one time they name and leave the other
alone, where before they had no effect. `COMPUTERD_FUSE_AC_ATTR_TIMEOUT` is gone, along with
`ac_attr_timeout` in `COMPUTERD_FUSE_EXTRA_OPTS`: it only tuned `auto_cache`,
which the mount no longer uses. And `COMPUTERD_FUSE_EXTRA_OPTS` entries now
fail the mount, naming the option, when the binding does not recognize them.
31 changes: 31 additions & 0 deletions .changeset/native-roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@cloudflare/computerd": minor
---

Bind-mount local-only directories

With `MOUNT_IGNORE` set, computerd now bind-mounts each local-only directory
onto the mount from the place it already stores it, under `MOUNT_IGNORE_PATH`.
Everything under such a directory is then served by the kernel from the
container's disk: lookups, `stat`, creates, and listings no longer reach
computerd, where FUSE passthrough alone only took the file data off its path.
Content is the same either way, so a mount that fails or lands late changes
speed, not what a command sees.

A directory is mounted when a command creates it, when a lookup finds one on
disk that is not mounted yet (after a snapshot restore), and at startup.
Native roots need negotiated FUSE passthrough, `CAP_SYS_ADMIN`, and `mount`
from util-linux; without them local-only paths are served as before.
`COMPUTERD_FUSE_NATIVE_ROOTS=0` turns them off. `/__computerd/info` reports
the state under `ignore.fastPaths.nativeRoots`.

Removing or renaming a mounted directory itself would fail with `EBUSY`, as
for a Docker volume. For the commands computerd runs, a removal guard prevents
that: computerd installs a seccomp filter on the thread it spawns commands
from, and when a command removes or renames a native root, it detaches the
mount first and lets the call continue. `rm -rf node_modules`, `fs.rm`, and
`mv` behave as they do without a mount. It needs `CAP_SYS_ADMIN` and costs
about 15 µs per directory removal or rename; file operations are not
affected. Processes computerd did not start still get `EBUSY`, and can call
`POST /__computerd/native-roots/detach` first. `/__computerd/info` reports
the guard as `nativeRoots.transparentRemoval`.
8 changes: 8 additions & 0 deletions .github/actions/install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ runs:
registry-url: ${{ inputs.registry-url }}
cache: npm

# npm ci compiles computerd's vendored FUSE addon against these.
- name: Install the libfuse 3 headers
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libfuse3-dev

- name: Install dependencies
shell: bash
run: npm ci
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,9 @@ jobs:
path: packages/computerd
needs-siblings: true
build-computerd-bin: true
# libfuse2 (libfuse.so.2) is what fuse-native binds to at
# runtime; fuse3 ships /dev/fuse helpers and the
# FUSE_MOUNT=shim tests need it for the userspace mount path.
# xz-utils extracts the target Node binary used by build:bin.
system-deps: "libfuse2t64 fuse3 xz-utils"
# fuse3 provides fusermount3 for the real-FUSE tests; xz-utils
# unpacks the Node binary build:bin injects into.
system-deps: "fuse3 xz-utils"
steps:
- uses: actions/checkout@v6
with:
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/next-computerd-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ jobs:

- uses: ./.github/actions/install

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libfuse2t64 fuse3

# build-bin generates the SEA blob with the active Node executable and
# injects it into the pinned Node 22.23.4 target binary.
- name: Use the target Node version for the binary build
Expand All @@ -72,6 +67,7 @@ jobs:
- name: Build all workspaces
run: npm run build --workspaces --if-present

# Compiles the FUSE addon in a container, so no FUSE packages here.
- name: Build the computerd binary
run: npm run build:bin --workspace @cloudflare/computerd

Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,12 @@ jobs:
registry-url: "https://registry.npmjs.org"

# @cloudflare/computer's prepublishOnly runs `npm run build && npm
# test`; its tests spawn the computerd binary, which dynamically links
# libfuse2 and uses fuse3 helpers for the FUSE_MOUNT=shim path. Without
# these the suite fails with `libfuse.so.2: cannot open shared object
# file`. Mirrors ci.yml.
# test`. fuse3 provides fusermount3 for the suites that mount.
# Mirrors ci.yml.
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libfuse2t64 fuse3
sudo apt-get install -y --no-install-recommends fuse3

# Build every workspace's dist/ up front. changesets doesn't run the
# build itself (changesets/changesets#860), and package builds/tests
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ worker-configuration.d.ts
# Local celld example state.
examples/celld/.celld-s3*/
examples/celld/.celld-state*/

# Vendored native addon: build output and its install tree
packages/computerd/vendor/*/build/
packages/computerd/vendor/*/node_modules/
packages/computerd/vendor/*/prebuilds/
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
],
"ignorePatterns": [
"**/node_modules",
"**/vendor",
"**/node_modules.host-backup.*",
"**/dist",
"/artifacts",
Expand Down
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"ignorePatterns": [
"**/node_modules",
"**/vendor",
"**/node_modules.host-backup.*",
"**/dist",
"/artifacts",
Expand Down
44 changes: 24 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,35 @@ the file directly when the trigger applies:
A fresh container does not have everything the tests need. The traps
below cost real time if you discover them one failure at a time.

**Native build tools.** `packages/computerd` depends on `fuse-native`, a
native addon. Building it needs a C toolchain and the libfuse2 headers.
On Debian or Ubuntu:
**Native build tools.** `packages/computerd` depends on a vendored copy
of `fuse-napi` at `packages/computerd/vendor/fuse-napi`, a native addon
compiled from source on install. Building it needs a C toolchain and the
libfuse 3 headers. On Debian or Ubuntu:

```bash
apt-get install build-essential libfuse-dev
apt-get install build-essential pkg-config libfuse3-dev fuse3
```

If the `fuse-native` build fails, `npm install` aborts the whole
install, not just that one package. When you only need the rest of the
workspace, install with `npm install --ignore-scripts` to skip the
native build.
Any libfuse 3 compiles. Kernel passthrough for local-only paths needs
3.17 or newer (Debian trixie); against an older one the addon builds
without it and those paths are served by `computerd` instead.

**arm64 hosts.** `fuse-native` ships a prebuilt libfuse for x64 only.
On a Linux arm64 host or container (including a Linux container on
Apple Silicon, or arm64 CI) the link fails with `file in wrong
format`. The path below is Debian or Ubuntu arm64; a native macOS host
uses macFUSE instead and does not hit this. Replace the bundled library
with the system one and rebuild:
If the addon build fails, `npm install` aborts the whole install, not
just that one package. When you only need the rest of the workspace,
install with `npm install --ignore-scripts`. That also skips the root
`postinstall`, so run `npm run build:types` before building the
examples, and build the addon yourself when you need it:

```bash
cp /usr/lib/aarch64-linux-gnu/libfuse.so.2 \
node_modules/fuse-shared-library-linux/libfuse/lib/libfuse.so
cd node_modules/fuse-native && npx node-gyp rebuild
cd packages/computerd/vendor/fuse-napi && npx node-gyp rebuild
```

**The binary build uses Docker.** `npm run build:bin` compiles the addon
for linux-x64 inside a `node:22-trixie-slim` container, so it works the
same from an arm64 machine (through emulation, which is slow the first
time; the result is cached under `packages/computerd/dist/sea`). Set
`COMPUTERD_FUSE_ADDON` to a prebuilt `fuse.node` to skip that step.

**Build before you test.** The test scripts don't build the sibling
packages first. Several suites need build output that is absent in
a clean checkout: `packages/computerd` imports the sibling `@cloudflare/dofs`
Expand All @@ -77,9 +80,10 @@ so a `mknod`'d `/dev/fuse` in an unprivileged container defeats the
skip and the mount then fails with `EPERM`, turning a clean skip into a
hard failure. Leave the device absent unless the container is
privileged (`--privileged`, or `CAP_SYS_ADMIN` with device access). The
`src/exec/runner.fuse.test.ts` suite is separate: it skips unless both
Docker and the prebuilt `computerd` binary are available, and runs `computerd`
inside a privileged container. See the
`src/exec/runner.fuse.test.ts` and `src/fuse/passthrough.fuse.test.ts`
suites are separate: they skip unless both Docker and the prebuilt
`computerd` binary are available, and run `computerd` inside a privileged
container. See the
[`debugging-computerd-fuse`](.agents/skills/debugging-computerd-fuse/SKILL.md) skill
for the privileged Docker setup.

Expand Down
19 changes: 10 additions & 9 deletions docs/07_injected_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ exec runner, and the capnweb RPC endpoint the DO talks to.

The package ships it as a single self-contained Node SEA binary —
**`computerd`** — produced by `packages/computerd/` (npm package
`@cloudflare/computerd`, bin name `computerd`). The binary embeds Node,
the `fuse-native` prebuilds, and `libfuse` as SEA assets, so the host
image does **not** need a Node runtime. Build it with:
`@cloudflare/computerd`, bin name `computerd`). The binary embeds Node
and the FUSE addon as SEA assets, so the host image does **not** need a
Node runtime. It links the system libfuse 3, so the image does need
libfuse 3.17 or newer. Build it with:

```bash
npm run build:bin --workspace @cloudflare/computerd
# → artifacts/computerd/computerd-linux-x64
# → artifacts/computerd/computerd-macos-x64
```

`examples/container/Dockerfile` is the canonical recipe for staging the
Expand Down Expand Up @@ -73,11 +73,11 @@ The capnweb bootstrap interface is **`WorkspaceRPC`** (defined in
The canonical recipe is `examples/container/Dockerfile`:

```dockerfile
FROM --platform=linux/amd64 debian:stable-slim
FROM --platform=linux/amd64 debian:trixie-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
fuse3 libfuse2t64 ca-certificates \
fuse3 ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY build/computerd-linux-x64 /usr/local/bin/computerd
Expand All @@ -95,9 +95,10 @@ Notes:

- No Node, no `npm install`, no `package.json` — the SEA binary
embeds everything.
- `libfuse` itself is bundled into the binary as a SEA asset; the apt
install of `fuse3 libfuse2t64` provides the userland tooling and
`/dev/fuse` plumbing for the host kernel.
- `fuse3` provides libfuse 3 and `fusermount3`. computerd needs libfuse
3.17 or newer, which is Debian trixie or later; bookworm's 3.14 is too
old. computerd 0.6.0 and earlier link libfuse 2 instead and also need
`libfuse2t64`.
- `EXPOSE 8080` matches the Cloudflare backend's pinned port. If you
run `computerd` outside Cloudflare Containers, leave `PORT` unset (default
`45678`) or pick your own.
Expand Down
13 changes: 9 additions & 4 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,26 +146,31 @@ packages/computerd/
│ ├── cli/
│ │ └── computerd.ts # CLI entry
│ ├── fuse/
│ │ ├── driver.ts
│ │ ├── driver.ts # VFS op table and mountFuse
│ │ ├── binding-ops.ts # adapts the op table to fuse-napi
│ │ ├── passthrough.ts # local-only paths (MOUNT_IGNORE)
│ │ ├── ignore.ts # MOUNT_IGNORE matcher
│ │ ├── ignore-config.ts # startup config and /__computerd/info
│ │ ├── options.ts # mount options and init config
│ │ ├── backend.ts
│ │ ├── vfs.ts
│ │ ├── fuse-native.d.ts # fuse-native typings
│ │ └── index.ts
│ └── exec/
│ ├── runner.ts
│ ├── schema.ts
│ ├── types.ts
│ ├── log.ts
│ └── index.ts
├── vendor/
│ └── fuse-napi/ # patched libfuse 3 binding; see PATCHES.md
├── scripts/
│ ├── build.mjs # → dist/cli/computerd.cjs
│ ├── build-bin.mjs # SEA driver
│ └── sea/
│ └── bundle.mjs # esbuild → SEA bundle
├── artifacts/
│ └── computerd/
│ ├── computerd-linux-x64
│ └── computerd-macos-x64
│ └── computerd-linux-x64
├── tsconfig.json
└── package.json
```
Expand Down
Loading
Loading