Skip to content

computer: add WorkerBundle mount to copy files from bundle into workspace - #215

Open
aron-cf wants to merge 10 commits into
mainfrom
mount-bundle
Open

aron-cf wants to merge 10 commits into
mainfrom
mount-bundle

Conversation

@aron-cf

@aron-cf aron-cf commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

WorkerBundle() copies a directory from deployed Worker bundle into the workspace as a read-only mount. An additional Vite plugin is required when using Vite to ensure the files are copied into the bundle.

This primarily exists to make it easier to ship skills with your application.

1. The provider: WorkerBundle(path, options?)

The code is in packages/computer/src/mounts/providers/worker-bundle.ts, exported from @cloudflare/computer.

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

mounts: {
  "/workspace/templates": WorkerBundle("templates"),
}

Options

WorkerBundle("templates", {
  // Skip entries. Skipping a folder skips everything under it.
  filter: ({ path, type }) => !path.startsWith("drafts/"),
  // workerd has no file permissions. Default: 0o755 if the file starts with "#!", else 0o644.
  fileMode: (path, bytes) => (path.startsWith("scripts/") ? 0o755 : undefined),
  // Default: a SHA-256 of paths, modes and bytes. A string skips hashing; false turns refresh off.
  version: env.CF_VERSION_METADATA.id,
  maxBytes: 10 << 20,
  maxEntries: 5_000,
});

Deploys are versioned. Pushing a new bundle will update the files in the workspace.

The examples have been updated:

worker-shell, worker-javascript and container each ship src/skills/exec/SKILL.md and mount it:

mounts: {
  "/workspace/r2": R2Bucket(env.Bucket),
  "/workspace/.agents/skills": WorkerBundle("skills"),
},

Devin Review

@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: da8a3b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/computer Patch
@cloudflare/dofs Patch
@cloudflare/computer-rpc Patch
@cloudflare/computerd Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Devin Review

// recorded as changes and reaches the container on the next
// push. If materialize() then fails, the rollback below leaves
// the root empty rather than restoring the old copy.
if (stale.has(root)) await fs.rm(root, { recursive: true, force: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bundle refresh deletes existing workspace files

When a versioned mount refreshes, fs.rm deletes files that predated its first index. The new bundle cannot restore those files, so deployment loses workspace data.

Learn more

A mount's first index leaves existing files beneath its root intact. On a later version change, recursively removing that root also removes files the mount never created. The replacement bundle cannot recreate those files.

Example: A workspace has /workspace/.agents/skills/custom/SKILL.md before installing the bundled exec/SKILL.md. Both survive the first index. Updating the bundle removes custom/SKILL.md, although it never belonged to the bundle.

Recommended fix: Track the entries owned by the mount and delete only those during refresh, or preserve pre-existing entries. Test a version refresh after indexing a populated root.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

);
const versionMatches = mount.version === undefined || row?.version === mount.version;
status.set(root, row?.indexed === 1 && versionMatches);
if (row?.indexed === 1 && !versionMatches) stale.add(root);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Interrupted refresh leaves stale bundle files

After an interrupted refresh, stale excludes the unindexed mount and a retry keeps its partial subtree. Files removed from the new bundle remain visible after indexing succeeds.

Learn more

A mount refresh updates its row to indexed=0 before removing the old subtree and writing the new one. Each filesystem write commits independently. If the durable object restarts between those operations, the next boot finds an unindexed row and does not clear the partial subtree. A successful retry writes its current files over that subtree, leaving any unrelated old files behind.

Example: Bundle v1 has a.txt and removed.txt; v2 drops removed.txt. A restart after setting indexed=0 but before removing the subtree leaves both old files. Retrying v2 overwrites a.txt and marks the mount indexed, but removed.txt still exists.

Recommended fix: Clear the mount subtree on every retry when an existing row has indexed=0, except when explicitly preserving content from a first index. Persist enough state to distinguish an interrupted refresh from a fresh index, and test interruption at the row update and mid-materialization.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +191 to +195
const key = [root, options.filter?.toString() ?? "", options.fileMode?.toString() ?? ""].join(
"\0",
);
const cached = versionCache.get(key);
if (cached !== undefined) return cached;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Session filters reuse another bundle version

When filter or fileMode closes over session data, versionCache reuses the first session's hash. Later sessions can skip indexing changed selections and keep stale files.

Learn more

The default version represents the filtered paths, file modes, and bytes. Two closure instances with identical function source can nevertheless select different entries or modes. The module-wide cache keys only the directory and function text, so the second instance gets the first one's hash without evaluating its own filter. A workspace with the first hash stored skips refreshing, even though its desired contents have changed.

Example: A mount factory closes over a session's selected locale: filter: ({path}) => path.startsWith(locale). An en mount hashes first; another fr mount has the same function text and receives the en version. If a workspace previously indexed en, switching its selection to fr does not replace the files.

Recommended fix: Hash each mount instance when callbacks are supplied, or require an explicit stable cache key for closure-dependent options. Retain a cache only for options whose output depends solely on immutable bundle contents.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

if (!isDirectory(sourceDir)) {
this.error(`workerBundle: ${sourceDir} does not exist or is not a directory`);
}
for (const file of listFiles(sourceDir)) this.addWatchFile(file);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New bundle files do not trigger rebuild

During Vite watch builds, addWatchFile registers only existing files. Adding a file under the bundle directory triggers no rebuild, so preview keeps shipping the old tree.

Learn more

The plugin emits all files under its configured directory on each build. Vite's watch dependency list here includes only files present when buildStart runs. New files have not been registered and do not trigger another build, so the emitted asset list and Data modules remain unchanged.

Example: Start vite build --watch with src/templates/a.txt, then add src/templates/b.txt. No registered watched file changed, so the output still contains only a.txt until a different file forces a rebuild.

Recommended fix: Watch the directory itself or register a directory-specific watcher that invalidates the Worker environment when children are added or removed; test additions and removals during watch mode.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +53 to +55
return {
name: "@cloudflare/computer:worker-bundle",
apply: "build",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Bundled skills unavailable in Vite development

vite dev does not populate /bundle, so WorkerBundle rejects when constructing the workspace. Review whether build-and-preview is an acceptable development loop for consumers of the new Vite plugin.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@215

commit: da8a3b1

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