Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/mount-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudflare/computer": patch
"@cloudflare/dofs": patch
---

Mounts can now declare a `version` stored under `_vfs_mounts.version` (schema v9); see [mount documentation](https://github.com/cloudflare/computer/blob/main/docs/06_mount_interface.md#versions).
5 changes: 5 additions & 0 deletions .changeset/worker-bundle-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

Add `WorkerBundle`, a read-only mount that copies files from the deployed bundle; see [mount documentation](https://github.com/cloudflare/computer/blob/main/docs/06_mount_interface.md#workerbundlepath-options).
5 changes: 5 additions & 0 deletions .changeset/worker-bundle-vite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

Add `@cloudflare/computer/vite` with a `workerBundle()` plugin that bundles a directory into a Vite-built worker for `WorkerBundle`; see [mount documentation](https://github.com/cloudflare/computer/blob/main/docs/06_mount_interface.md#shipping-the-files-with-vite).
28 changes: 20 additions & 8 deletions docs/03_filesystem_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,17 +241,29 @@ to a later cursor rather than frozen at `fetchRev`. See
CREATE TABLE _vfs_mounts (
root TEXT PRIMARY KEY,
kind TEXT NOT NULL,
indexed INTEGER NOT NULL DEFAULT 0
indexed INTEGER NOT NULL DEFAULT 0,
mode TEXT NOT NULL DEFAULT 'read-only'
CHECK(mode IN ('read-only', 'read-write')),
version TEXT
);
```

*Planned; mount feature not yet implemented — see
[06. Mount Interface](./06_mount_interface.md).* The schema seat is
in place so the migration doesn't need to land alongside the mount
runtime, but no code reads or writes this table yet. When mounts
ship, the row will record that a mount root has been indexed (its
directory tree listed and stub rows inserted into `vfs_nodes`) so a
DO reload doesn't re-list.
One row per registered mount root, written by the workspace's mount
indexer. See [06. Mount Interface](./06_mount_interface.md).

- `kind` names the provider, such as `r2` or `worker-bundle`. It's for
diagnostics only.
- `indexed` is `1` once `materialize()` has finished successfully, so a
reload over the same store doesn't run it again. A failed run leaves
`0`, and the next pass retries.
- `mode` is what the read-only guard reads to reject writes under the
root with `EROFS`. The indexer holds the row at `'read-write'` while it
materializes, then sets the mount's own mode.
- `version` (added in schema v9) is the content version the mount
declared at its last successful index. It's `NULL` for mounts that
don't declare one. When a mount's version changes, the indexer
replaces its subtree; see
[Versions](./06_mount_interface.md#versions).

## Invariants

Expand Down
150 changes: 150 additions & 0 deletions docs/06_mount_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,122 @@ R2Bucket(env.SHARED_FILES, {
- `fetch(relPath)` issues one R2 `get()` per stub on first read.
- `put` and `delete` proxy to R2 when `mode: "read-write"`.

### `WorkerBundle(path, options?)`

> [!NOTE]
> Unlike the rest of this document, this section describes what ships
> today.

Eager, read-only mount over a directory that ships inside the Worker's
own upload. Use it for files that belong to the deployment, such as
skills, templates, or reference material.

```ts
import { WorkerBundle } from "@cloudflare/computer";

new Workspace({
// ...
mounts: {
"/workspace/.agents/skills": WorkerBundle("skills"),
},
});
```

With `nodejs_compat`, workerd exposes every module in the Worker upload
as a read-only file under `/bundle`. `WorkerBundle()` walks a directory
there with the synchronous `node:fs` APIs and copies it into the
workspace. A relative `path` is read from `/bundle`, so `"skills"` and
`"/bundle/skills"` are the same. An absolute path is read as-is, which
is useful in tests that point at a directory on disk.

The mount is always read-only. Writes under the root through
`Workspace.fs` reject with `EROFS`, and writes from the container are
dropped on pull. There's no `mode` option, because the deployment owns
these files and there is nowhere to write changes back to.

```ts
WorkerBundle("skills", {
// Skip entries. Skipping a directory skips everything under it.
filter: ({ path, type }) => !path.startsWith("drafts/"),
// workerd's file system has no permission bits. By default, files
// that start with "#!" get 0o755 and everything else 0o644.
fileMode: (path, bytes) => (path.startsWith("scripts/") ? 0o755 : undefined),
// See "Versions" below. A string skips hashing; false turns refresh off.
version: env.CF_VERSION_METADATA.id,
maxBytes: 10 << 20,
maxEntries: 5_000,
});
```

- `WorkerBundle()` checks that the directory exists when it is called,
and throws with the fix in the message if not. That's usually a
missing wrangler rule or Vite plugin (see below).
- File contents are only read in `materialize()`, which runs on the
first index and after a version change.
- By default the mount's `version` is a SHA-256 of the included paths,
file modes and bytes. It's computed once per isolate, because `/bundle`
can't change while the isolate is alive. When a deploy changes the
files, the hash changes and each workspace replaces its copy on its
next index. Pass an explicit `version`, such as a build id, for large
trees, or when `filter` depends on the session.

#### Shipping the files with wrangler

Files only appear under `/bundle` with their paths intact when wrangler
uploads each one as its own module. Turn on `find_additional_modules`
and add a rule for the directory. Paths are relative to `base_dir`,
which defaults to the directory of `main`:

```jsonc
// wrangler.jsonc, with "main": "src/index.ts"
"find_additional_modules": true,
"rules": [{ "type": "Data", "globs": ["skills/**/*"], "fallthrough": true }]
```

`src/skills/exec/SKILL.md` then lands at `/bundle/skills/exec/SKILL.md`.
A file imported from code doesn't work: wrangler renames it to a
content hash such as `/bundle/76e042f4…-SKILL.md`. Files inside
`node_modules` aren't uploaded at all.

#### Shipping the files with Vite

`@cloudflare/vite-plugin` ignores `find_additional_modules` and `rules`.
The `wrangler.json` it generates for deploy only uploads JavaScript plus
files that match wrangler's default rules (`.txt`, `.html`, `.sql`,
`.bin`, `.wasm`). Add the `workerBundle` plugin from
`@cloudflare/computer/vite`:

```ts
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { workerBundle } from "@cloudflare/computer/vite";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [cloudflare(), workerBundle({ dir: "src/skills" })],
});
```

The plugin copies `dir` into each Worker's build output, keeping its
paths, and adds a `Data` rule for it to the generated `wrangler.json`.
Both `vite preview` and `wrangler deploy` read that file. The options
are:

| Option | Default | Meaning |
| --- | --- | --- |
| `dir` | (required) | Directory to ship, relative to the Vite root. |
| `as` | last segment of `dir` | Path under `/bundle`, so `"src/skills"` matches `WorkerBundle("skills")`. |
| `environment` | every Worker environment | Limit the plugin to one Vite environment. |

`vite dev` isn't supported. It runs the Worker through Vite's module
runner and never puts project files under `/bundle`. `WorkerBundle()`
detects this and throws an error that points at `vite build && vite
preview` or `wrangler dev`.

Wrangler prints `Ignoring duplicate module` for bundled `.bin` files,
because they match both the plugin's rule and wrangler's default `.bin`
rule. The warning is harmless.

### `GitHubRepo(slug, options)`

Eager mount that clones a GitHub repository via `isomorphic-git` and
Expand Down Expand Up @@ -195,6 +311,40 @@ On first call to any `fs`, `shell`, or `prefetch` method, every mount
is indexed in parallel. Index state is persisted to `_vfs_mounts`
in SQLite so DO restarts don't trigger a re-list.

### Versions

> [!NOTE]
> This section describes what ships today.

A mount can declare a `version` string on `MountBase`. After a
successful `materialize()`, the indexer records it in
`_vfs_mounts.version`. On a later boot, it compares that with the
registered mount's `version`:

- **No version on the mount:** the mount is materialized once per store,
as before. `R2Bucket` works this way.
- **Same version:** the mount is skipped.
- **Different version:** the mount is stale. The indexer removes
everything under the root, runs `materialize()` again, and records the
new version.

```text
first boot → materialize, record "sha256:abc…"
same deploy → versions match, skip
new deploy → "sha256:def…" ≠ "sha256:abc…" → rm root, materialize, record "sha256:def…"
```

The old subtree is only removed on a refresh. On a first index,
anything already at the root is left in place. The remove and rewrite
go through the workspace filesystem, so they're recorded as normal
changes and reach the container on its next push. If a refresh fails,
the root is left empty with `indexed = 0`, and the next pass tries
again.

`WorkerBundle()` sets `version` from a content hash by default, so a
deploy that changes the bundled files refreshes every workspace on its
next index.

`workspace.prefetch(root?)` eagerly hydrates lazy stubs under the given
mount root (or every mount if none supplied). Useful from `onStart` /
`waitUntil` to avoid a cold-start fetch fan-out on the first `grep`.
Expand Down
2 changes: 2 additions & 0 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ packages/computer/
│ │ ├── worker-shell/ # Dynamic Worker + just-bash shell backend
│ │ ├── worker-javascript/ # Dynamic Worker ECMAScript module backend
│ │ └── test.ts # In-process test backend
│ ├── mounts/ # Mount registry, indexer, R2Bucket and WorkerBundle
│ ├── vite/ # workerBundle Vite plugin (@cloudflare/computer/vite)
│ ├── proxy.ts # WorkspaceProxy
│ ├── proxy-stub.ts # Client-side stub plumbing
│ ├── stub.ts # DO stub helpers
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The `@cloudflare/computer` package provides an out of the box virtual filesystem
It provides:

- A fs API for working with files and directories compatible with Worker bindings.
- R2-backed mounts for pre-filling read-only data into the workspace tree.
- Read-only mounts for pre-filling the workspace tree from an R2 bucket or from files shipped with the Worker.
- Durability over DO restarts for all file operations.
- Pluggable execution backends selected through `workspace.runtime`: a Cloudflare Container shell, a just-bash Dynamic Worker, or an isolated ECMAScript-module Dynamic Worker.
- Isolated JavaScript with structured input/results, durable relative imports, configured libraries, durable `node:fs/promises`, host modules such as `ws:git` and `ws:container`, and managed execution records.
Expand All @@ -42,7 +42,7 @@ The package ships several entrypoints:

| Entrypoint | Purpose |
| --- | --- |
| `@cloudflare/computer` | The Workspace wrapper, first-class `workspace.runtime`, stub types, the R2 mount, and proxy classes. |
| `@cloudflare/computer` | The Workspace wrapper, first-class `workspace.runtime`, stub types, the R2 and Worker bundle mounts, and proxy classes. |
| `@cloudflare/computer/backends/container` | `ContainerBackend` and `withWorkspaceContainer`, for a container the durable object schedules (`scheduling_policy: "durable_object"`). Same sync plumbing; the launch names the image and the instance size. |
| `@cloudflare/computer/backends/container-legacy` | `LegacyContainerBackend` and `withLegacyWorkspaceContainer`, for a container the platform schedules and sizes from the containers block. |
| `@cloudflare/computer/backends/worker-shell` | `WorkerShellBackend` and the bundled just-bash command runtime. |
Expand Down
2 changes: 1 addition & 1 deletion examples/artifacts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
2 changes: 1 addition & 1 deletion examples/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
2 changes: 1 addition & 1 deletion examples/celld/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
2 changes: 1 addition & 1 deletion examples/container-legacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
16 changes: 16 additions & 0 deletions examples/container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ client ─► Worker /c/<name>/{file,exec}
If a container fails its startup health check, the replacement uses the
same image and instance size.

## Bundled skill

`src/skills/exec/SKILL.md` explains how `exec` works on this backend,
for an agent working in the workspace. It ships inside the Worker
upload through the `find_additional_modules` rule in `wrangler.jsonc`,
and `WorkerBundle("skills")` copies it to `/workspace/.agents/skills`
as a read-only mount. When a deploy changes the file, each workspace
picks up the new copy on its next start. The mount reaches the container
through the normal sync, like any other workspace file.

```sh
curl -X POST -H 'content-type: application/json' \
-d '{"command":"cat /workspace/.agents/skills/exec/SKILL.md"}' \
http://localhost:8787/c/demo/exec
```

## Running it

```bash
Expand Down
2 changes: 1 addition & 1 deletion examples/container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
},
"devDependencies": {
"typescript": "^6.0.3",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
6 changes: 6 additions & 0 deletions examples/container/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { DurableObject, tracing } from "cloudflare:workers";

import {
getWorkspace,
WorkerBundle,
type WorkspaceOptions,
WorkspaceProxy,
withWorkspace,
Expand Down Expand Up @@ -80,6 +81,11 @@ function workspaceOptions(self: InstanceType<typeof ContainerBase>): WorkspaceOp
return {
storage: ctx.storage,
backends: [self.backend],
mounts: {
// Read-only copy of src/skills, shipped with the Worker through the
// rule in wrangler.jsonc. Refreshed when a deploy changes the files.
"/workspace/.agents/skills": WorkerBundle("skills"),
},
// Route every workspace operation through the Cloudflare
// runtime's user-tracing surface. The runtime owns the span
// lifecycle; the observer is a thin wrapper that forwards seed
Expand Down
46 changes: 46 additions & 0 deletions examples/container/src/skills/exec/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: exec
description: How exec works in this workspace. Commands run in bash in a real Linux container, with Node.js, git and network access, and the workspace is synced in and out around each call. Read this before running commands, installing packages, or debugging files that don't show up where you expect.
---

# Running commands

Every `exec` call runs your command with bash in a **Linux container**.
The durable object starts the container, and a small daemon in it,
`computerd`, runs the command.

## What's installed

- Debian, with `bash`, `git`, `curl` and the usual core tools.
- Node.js 22 and `npm`.
- Anything else can be installed with `apt-get` or `npm`. It lasts until
the container stops.

## Network

The container has direct network access, so `npm install`, `git clone`
and `curl` work.

## Files

- `/workspace` in the container is the workspace. `cwd` defaults to it.
- The durable object's storage holds the authoritative copy. Before each
call, changes made outside the container are pushed into
`/workspace`. After the call, changes made in the container are pulled
back. Something written outside `/workspace` stays in the container
and isn't saved.
- `/workspace/.agents/skills` (where this file lives) is a read-only
copy of files shipped with the Worker. Changes you make there in the
container are dropped when the call ends.

## Things to know

- The first call is slow, because the container has to boot. Later calls
reuse it while it stays up.
- A container that stops loses everything outside `/workspace`,
including installed packages. Reinstall rather than assume they're
still there.
- Every file under `/workspace` is synced back to storage after each
call. This example keeps no paths on the container's local disk, so a
large `node_modules` or build folder under `/workspace` slows every
call down. Put throwaway output in `/tmp`.
7 changes: 7 additions & 0 deletions examples/container/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
"compatibility_date": "2026-05-26",
"compatibility_flags": ["nodejs_compat"],

// Upload every file under src/skills as its own module, under its
// path relative to src/, so it shows up at /bundle/skills/... and
// WorkerBundle("skills") can copy it into the workspace. "Data" keeps
// the bytes exactly as they are on disk.
"find_additional_modules": true,
"rules": [{ "type": "Data", "globs": ["skills/**/*"], "fallthrough": true }],

"containers": [
{
"class_name": "ContainerExample",
Expand Down
2 changes: 1 addition & 1 deletion examples/egress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"devDependencies": {
"typescript": "^6.0.3",
"vitest": "^4.1.11",
"wrangler": "^4.137.0"
"wrangler": "^4.148.0"
}
}
Loading
Loading