agentleFS
lsjwzhlsjwzh/MultiCC/skills/cloudflare-computer/SKILL.md

cloudflare-computer

Skill15 starsChanged 7 days agoMITView on GitHub

Use the @cloudflare/computer package to give a Durable Object a durable SQLite-backed filesystem (workspace.fs) and pluggable command/code execution backends (container, worker-shell, worker-javascript), including AI SDK tools, git, and stub disposal rules. Load when working with @cloudflare/computer, Workspace, withWorkspace/getWorkspace, workspace.fs, or runtime.exec.

  • Reads credentials
  • Installs packages

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add the loader binding and the experimental flag to wrangler.jsonc:

    line 126
  2. Very specific:

    Don't worry about ws.fs, ws.runtime, or ws.git — they ride with the parent.

    line 351
  3. Very specific:

    When assigning a workspace to a Think agent's workspace, pass useThink: true so Think's compatibility methods are added alongside …

    line 425

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Using @cloudflare/computer

@cloudflare/computer gives a Cloudflare Durable Object a persistent, SQLite-backed virtual filesystem plus pluggable command and code execution. It is built for agents that need a small, portable working directory and the tools to read, write, and run things in it.

PREVIEW ONLY. The package is provided as a preview for feedback only. APIs are unstable and the design is subject to change. Suitable for experiments and prototypes; NOT suitable for production use.

Upstream: https://github.com/cloudflare/computer (MIT license — see the LICENSE file next to this skill).

What you get

  • A filesystem in your Durable Object. workspace.fs looks like node:fs/promises — readFile, writeFile, mkdir, readdir, rm, grep — durable across DO restarts, backed by the DO's own SQLite storage.
  • One execution surface, several backends. workspace.runtime.exec() runs a shell command or an ECMAScript module. Pick a full Linux container, a fast in-Worker shell, or an isolated JavaScript runtime — all against the same files.
  • Batteries for agents. Ready-made AI SDK tools (read, write, edit, ls, exec), a git client, R2-backed read-only mounts, and helpers for publishing files.

The Workspace can also run with no execution backend at all — just the filesystem.

Limits

  • ~10 GB per workspace (shares storage with the DO).
  • The container-side filesystem is held in memory. Aim for agent-scale workspaces, not full monorepos.
  • Container access goes through FUSE, so heavy I/O (large node_modules installs, big tarball extractions) is slower than a native disk.

Installation

npm install @cloudflare/computer

Your Worker needs the nodejs_compat compatibility flag. The worker-shell and worker-javascript backends additionally need the experimental flag and a Worker Loader binding. Each backend has its own binding requirements — see Choosing a backend.

Optional peer dependencies, installed only if you use the matching feature: ai and zod (for @cloudflare/computer/tools), @platformatic/vfs (for the Node-side VFS provider).

Quick start

The smallest useful thing is a filesystem with no execution backend. Add withWorkspace to a Durable Object and you have durable files:

import { withWorkspace, getWorkspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";

export class Agent extends withWorkspace(
  class extends DurableObject<Env> {},
  (self) => ({ storage: self.ctx.storage }),
) {}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.Agent.idFromName("user-123");
    using ws = await getWorkspace(env.Agent.get(id));

    await ws.fs.writeFile("/notes.md", "- [ ] ship it\n");
    const notes = await ws.fs.readFile("/notes.md", "utf8");

    return new Response(notes);
  },
} satisfies ExportedHandler<Env>;

wrangler.jsonc:

{
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [{ "name": "Agent", "class_name": "Agent" }]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Agent"] }
  ]
}

To run commands against those files, add an execution backend. The worker-shell backend needs no container and no Docker, so it's the quickest way to get exec working:

import { withWorkspace, getWorkspace } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import { DurableObject } from "cloudflare:workers";

export class Agent extends withWorkspace(
  class extends DurableObject<Env> {},
  (self) => ({
    storage: self.ctx.storage,
    backends: [
      new WorkerShellBackend({
        loader: self.env.LOADER,
        workspace: { binding: "Agent", id: self.ctx.id.toString() },
        ctx: self.ctx,
      }),
    ],
  }),
) {}

Add the loader binding and the experimental flag to wrangler.jsonc:

{
  "compatibility_flags": ["nodejs_compat", "experimental"],
  "worker_loaders": [{ "binding": "LOADER" }]
}

Now exec runs against the same files your fs calls wrote:

using ws = await getWorkspace(env.Agent.get(id));
await ws.fs.writeFile("/hello.txt", "world");
using run = await ws.runtime.exec("cat /hello.txt");
const { stdout, exitCode } = await run.result();

The filesystem

workspace.fs is async, uses absolute paths, and is durable across DO restarts. Strings default to UTF-8; pass a Uint8Array or a ReadableStream for binary content.

// Write a string, bytes, or a stream straight to disk.
await ws.fs.writeFile("/notes/todo.md", "- [ ] ship it\n");
await ws.fs.writeFile("/data/blob.bin", new Uint8Array([1, 2, 3]));
await ws.fs.writeFile("/uploads/big.csv", request.body!);

// Read back as a string or as a stream.
const todo = await ws.fs.readFile("/notes/todo.md", "utf8");
const stream = await ws.fs.readFile("/uploads/big.csv");
return new Response(stream);

// Directories.
await ws.fs.mkdir("/notes/daily", { recursive: true });
for (const entry of await ws.fs.readdir("/notes")) {
  console.log(entry.isDirectory ? `d ${entry.name}` : `f ${entry.name}`);
}

// Remove and search.
await ws.fs.rm("/notes/daily", { recursive: true });
const hits = await ws.fs.grep("TODO", "/", { ignoreCase: true });

Read-only mounts

Pre-fill part of the tree from an R2 bucket. Files under the mount point are read-only; writes reject with EROFS.

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

new Workspace({
  storage: ctx.storage,
  mounts: { "/workspace/r2": R2Bucket(env.Bucket) },
});

Running commands and code

workspace.runtime.exec(source, options) is the single execution entry point. What source means depends on the backend: a shell command for the container and worker-shell backends, an ECMAScript module for the worker-javascript backend.

using run = await ws.runtime.exec("ls -la /workspace", { encoding: "utf8" });
const { stdout, stderr, exitCode } = await run.result();

The handle is also a ReadableStream of live events, so you can forward output as it happens — for example, as Server-Sent Events:

async fetch(request: Request) {
  const run = await ws.runtime.exec("npm test", { encoding: "utf8" });

  const sse = run.pipeThrough(
    new TransformStream({
      transform(event, controller) {
        const frame = `event: ${event.name}\ndata: ${JSON.stringify(event.value)}\n\n`;
        controller.enqueue(new TextEncoder().encode(frame));
      },
    }),
  );

  return new Response(sse, {
    headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
  });
}

Alongside exec, the runtime exposes getExec, killExec, and disposeExec.

Choosing a backend

BackendImportRunsNeeds
Container@cloudflare/computer/backends/containerShell commands in full Linux userland (real binaries, npm, node, network)A Cloudflare Container running computerd
Worker shell@cloudflare/computer/backends/worker-shellShell commands via just-bash in a Dynamic WorkerA Worker Loader binding; experimental flag
Worker JavaScript@cloudflare/computer/backends/worker-javascriptECMAScript modules in a fresh Dynamic WorkerA Worker Loader binding; experimental flag
  • Container cold-starts more slowly but gives you a real Linux environment. The container owns its own SQLite-backed VFS and the package syncs the two stores across a capnweb WebSocket.
  • Worker shell is fast and needs no container. Every filesystem operation forwards back to the same Durable Object, so there's no second store and no sync round trip.
  • Worker JavaScript evaluates a module with structured input/results, durable relative imports, configured libraries, Workspace-backed node:fs/promises, and trusted ws:git / ws:artifacts modules.

You can register several backends on one Workspace and route each call to a named one — see Multiple backends.

Tools for agents

@cloudflare/computer/tools ships AI SDK tools that wrap the Workspace surfaces, ready to hand to generateText, streamText, or an agent framework's getTools(). The default set is read, write, edit, and ls; exec and publish are added when you configure them.

import { createAITools } from "@cloudflare/computer/tools";

const tools = createAITools({
  workspace,
  read: { maxBytes: 32 * 1024, maxLines: 800 },
  shell: {
    defaultBackend: "shell",
    backends: {
      shell: { description: "Fast Worker shell with built-in text commands." },
      container: { description: "Full Linux userland in a Cloudflare Container." },
    },
  },
});

The model reads each backend's description when deciding where a command should run, so write them in plain language.

Git

workspace.git is an opt-in typed git client backed by isomorphic-git, operating directly on the local SQLite VFS — no backend or shell required. Enable it by passing createGitClient() from @cloudflare/computer/git:

import { Workspace } from "@cloudflare/computer";
import { createGitClient } from "@cloudflare/computer/git";

const ws = new Workspace({
  storage: ctx.storage,
  git: createGitClient(),
  defaultGitIdentity: { name: "Agent", email: "agent@example.test" },
});

await ws.git.clone({ url: "https://github.com/example/repo.git" });
await ws.fs.writeFile("/notes.md", "hello");
await ws.git.add({ paths: ["notes.md"] });
await ws.git.commit({ message: "add notes" });

There's also an argv entry point (workspace.git.cli({ argv })), and when git is configured the worker-shell backend exposes a built-in git command.

Sharing files

Two ways to get a file out of the workspace and into the world:

  • Assets (@cloudflare/computer/assets): createAssets(...).share uploads a workspace file to R2 and returns a presigned URL. Attach it through WorkspaceOptions.assets to expose an in-shell assets publish <path> [<expiry>] command.
  • Artifacts (@cloudflare/computer/artifacts): createArtifact(binding, sessionId) is a session-scoped facade over the Cloudflare Artifacts binding. Every repository name is implicitly prefixed with the session id, so one namespace hosts many isolated sessions.
import { createArtifact } from "@cloudflare/computer/artifacts";

const artifacts = createArtifact(env.ARTIFACTS, agentId);
const repo = await artifacts.create("build-cache", { description: "CI artifacts" });
const token = await artifacts.createToken("build-cache", "read", 3600);
const mine = await artifacts.list(); // only this session's repos

Crossing the Worker → DO boundary

The Durable Object owns the Workspace; a Worker reaches it through a stub. withWorkspace installs the plumbing, and getWorkspace(stub) returns a client:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.Agent.idFromName("user-123");
    using ws = await getWorkspace(env.Agent.get(id));

    await ws.fs.writeFile("/notes.md", "hello");
    using run = await ws.runtime.exec("ls /workspace");
    const { exitCode, stdout } = await run.result();

    return new Response(stdout, { status: exitCode === 0 ? 200 : 500 });
  },
} satisfies ExportedHandler<Env>;

Dispose your stubs

The one gotcha worth internalizing: the RPC layer does not garbage-collect remote stubs. On long-lived sessions undisposed stubs accumulate on the peer until the session ends. The rules are short:

  • using the value from getWorkspace(...).
  • using the handle from ws.runtime.exec(...).
  • Don't worry about ws.fs, ws.runtime, or ws.git — they ride with the parent.
  • Pure-value returns (readFile as a string, stat, readdir, git.cli({...})) carry no stubs; nothing to dispose.

Short-lived single-shot Workers tear the session down with the request, so this matters most on long-lived isolates that keep grabbing fresh stubs or on busy exec workloads. To hunt leaks, set CAPNWEB_TRACK_STUBS=1 and read stubSnapshot() from @cloudflare/computer-rpc/debug, or hit GET /__computerd/stubs on a computerd instance.

Package entrypoints

EntrypointPurpose
@cloudflare/computerThe Workspace facade, workspace.runtime, stub types, the R2 mount, and proxy classes.
@cloudflare/computer/backends/containerCloudflareContainerBackend and withWorkspaceContainer. Pulls in the computerd / capnweb sync plumbing.
@cloudflare/computer/backends/worker-shellWorkerShellBackend and the bundled just-bash runtime.
@cloudflare/computer/backends/worker-javascriptWorkerJavaScriptBackend, configured libraries, durable imports, node:fs/promises, and trusted ws:git / ws:artifacts.
@cloudflare/computer/toolsAI SDK tools for agents: read, write, edit, ls, optional exec and publish.
@cloudflare/computer/gitOpt-in isomorphic-git glue for checkouts inside the workspace.
@cloudflare/computer/assetscreateAssets — share a workspace file to R2 as a presigned URL.
@cloudflare/computer/artifactscreateArtifact and its CLI, a session-scoped facade over the Cloudflare Artifacts binding.
@cloudflare/computer/observe/cloudflareCloudflare-runtime adapter for the observability hook.

A consumer that only uses the container backend never imports the worker subpaths, so unused backend payloads tree-shake away. Wire types shared with the in-container service live in the sibling package @cloudflare/computer-rpc.

Advanced

Multiple backends

A Workspace can carry more than one backend, each registered under a stable selector id (defaulting to "worker-shell", "container-shell", or "worker-javascript"). runtime.exec uses the first backend unless the caller names one:

const ws = new Workspace({
  storage: ctx.storage,
  backends: [
    new WorkerShellBackend({ id: "shell", loader: env.LOADER, /* ... */ }),
    new CloudflareContainerBackend({ id: "sandbox", container: () => this, /* ... */ }),
  ],
});

const grep = await ws.runtime.exec("grep -r TODO /workspace");  // default: "shell"
const build = await ws.runtime.exec("npm test", { backend: "sandbox" });

Backends connect lazily — the first exec, push, pull, or ready(id) for an id dials it. ready({ all: true }) pre-warms every backend, which is handy from an agent's onStart. Per-backend sync cursors are independent, so activity on one never disturbs the other.

A backend that accepts a structured input and returns a structured value declares callable: true (the worker-javascript backend does). Passing input to a non-callable backend is a clear error rather than a silent drop.

Constructing without the mixin

withWorkspace is the shortcut. You can also construct a Workspace directly and expose it through a stub yourself:

const ws = new Workspace({ storage: ctx.storage, backends: [/* ... */] });
await ws.ready();
const stub = ws.stub();  // crosses the Workers-RPC boundary

When assigning a workspace to a Think agent's workspace, pass useThink: true so Think's compatibility methods are added alongside workspace.fs and workspace.runtime.

Durable pending-sync retries

A command can change backend files and then have its post-command pull fail; the result exposes sync: { status: "pending", ... }. Configure a SyncRetryScheduler on Workspace to persist one coalesced retry per backend, then call workspace.retryPendingSync(backend) from your DO's alarm. Retries use bounded exponential backoff and return "exhausted" after the configured maximum. The library does not own your DO's alarm.

Observability

Pass an observer to the Workspace constructor to receive one span per documented operation (workspace.connect, workspace.sync.push, workspace.sync.pull, workspace.runtime.exec.spawn, workspace.fs.<op>). The span(name, attributes, run) shape fits the Cloudflare runtime's ctx.tracing, OpenTelemetry, or a test recorder; attribute values are restricted to boolean | number | string. The default is a zero-cost no-op, so there's no overhead unless you opt in.

Where to read more

  • Upstream repo and examples: https://github.com/cloudflare/computer (examples/worker-shell, examples/worker-javascript, examples/container, examples/think, examples/tutorial).
  • Design spec under the repo's docs/ — forward-looking; read it for intent, not as a description of the code today.
  • For broader Cloudflare platform guidance, load the companion cloudflare-platform-skills skill.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.