Skip to content

Latest commit

Β 

History

History

computer

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

README.md

@cloudflare/computer

A persistent, SQLite-backed virtual filesystem for Durable Objects, with pluggable command and code execution. Built for agents that need a small, portable working directory and the tools to read, write, and run things in it.

Important

The @cloudflare/computer package should be considered suitable for production use. The package contains work-in-progress and experimental components such as the ContainerBackend. These should be considered unsuitable for production use. See the reference below for more details.

Status

Stable means suitable for production use. Experimental APIs are provided for feedback: they may change or go away in any release, so use them for experiments and prototypes only. Deprecated APIs will be removed in a future release; each one names its replacement.

Component Entrypoint Status
Workspace, withWorkspace, getWorkspace, workspace.fs, workspace.runtime, and the stub types @cloudflare/computer Stable
The SQLite filesystem layer (@cloudflare/dofs, bundled into this package), including SQLiteWorkspaceProvider @cloudflare/computer Stable
CodemodeBackend @cloudflare/computer/backends/codemode Stable
Read-only mounts (R2Bucket, WorkerBundle) and the Vite plugin @cloudflare/computer, @cloudflare/computer/vite Stable
Agent tools @cloudflare/computer/tools, @cloudflare/computer/tools/* Stable
Host modules for isolate JavaScript: codemode:git, codemode:artifacts, codemode:assets, codemode:tools @cloudflare/computer/modules/git, modules/artifacts, modules/assets, modules/tools Stable
Git @cloudflare/computer/git Stable
Assets and Artifacts @cloudflare/computer/assets, @cloudflare/computer/artifacts Stable
Observability hook and its Cloudflare adapter @cloudflare/computer, @cloudflare/computer/observe/cloudflare Stable
ContainerBackend and withWorkspaceContainer @cloudflare/computer/backends/container Experimental
LegacyContainerBackend and withLegacyWorkspaceContainer @cloudflare/computer/backends/container-legacy Deprecated: use ContainerBackend
Resuming interrupted isolate JavaScript: onInterrupt: "restart" and "replay", workspace.runtime.recover(), unsafeToRepeat(), and waiting for answers with ctx.interrupt(), pending() and respond() @cloudflare/computer, @cloudflare/computer/backends/codemode Experimental
WorkerShellBackend and the shell command groups @cloudflare/computer/backends/worker-shell, @cloudflare/computer/shell/* Experimental
The codemode:container host module @cloudflare/computer/modules/container Experimental
The codemode:browser host module @cloudflare/computer/modules/browser Experimental
Remote exec: the codemode:exec module, executor() and ExecSandbox, the remoteExec() Vite plugin, and the remote-exec binary and its image @cloudflare/computer/remote-exec, @cloudflare/computer/vite, ghcr.io/cloudflare/computer-remote-exec Experimental
Codemode modules: createCodemodeModule(), with objects and scoped context @cloudflare/computer, @cloudflare/computer/backends/codemode Experimental
workspace.push(), workspace.pull(), and the sync protocol between the durable object and computerd @cloudflare/computer Experimental
The computerd daemon and its container image ghcr.io/cloudflare/computer-computerd-linux-x64 Experimental
The RPC wire protocol between the durable object and computerd (@cloudflare/computer-rpc) Internal Experimental

A stable surface that runs work on a backend, such as the agent tools' exec tool, is only as stable as the backend it runs on.

The sync building blocks inside @cloudflare/dofs (applyChanges, fetchChanges, watermarks, and the rest) serve only the sync protocol, so they share its experimental status. The rest of @cloudflare/dofs is stable.

What you get

  • A filesystem in your Durable Object. workspace.fs looks like node:fs/promises β€” readFile, writeFile, mkdir, readdir, rm, grep β€” and is 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, ls, find, grep, write, edit, delete, and optional 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, giving you just the filesystem.

Limits

  • ~10 GB per workspace (it 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. See docs/19_performance.md.

Installation

npm install @cloudflare/computer

Your Worker needs the nodejs_compat compatibility flag. The worker-shell and codemode backends also need a Worker Loader binding, and the worker-shell backend needs the experimental flag. Each backend has its own binding requirements β€” see Choosing a backend.

Optional peer dependencies, installed only if you use the matching feature: just-bash for @cloudflare/computer/backends/worker-shell, zod for every tools entry point, ai for @cloudflare/computer/tools and @cloudflare/computer/tools/ai-sdk, and @platformatic/vfs for the Node-side VFS provider. The pi and TanStack AI entry points need nothing beyond zod; your agent brings its own library.

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. It is experimental; for a stable backend, use the codemode backend, shown in examples/codemode:

import { withWorkspace, getWorkspace } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import curlModules from "@cloudflare/computer/shell/curl";
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,
        commands: [curlModules],
      }),
    ],
  }),
) {}

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

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

The worker shell ships as feature groups: an always-on core plus one optional group per command at @cloudflare/computer/shell/<feature>. Import the groups you want and pass them to WorkerShellBackend's commands option; a group you never import is unreachable in your bundle and the bundler drops it. The optional groups are curl, html-to-markdown, python, sqlite, js-exec, yq, file, xan, and jq. curl runs on the isolate's global fetch (no undici in the bundle); egress stays governed by the Dynamic Worker's globalOutbound.

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();

For a full, runnable version of this see examples/worker-shell.

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 });

See docs/04_filesystem_interface.md for the full surface.

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) },
});

WorkerBundle copies files that ship inside your Worker, such as skills or templates. The copy is read-only, and it is replaced automatically when a deploy changes the files.

With Vite, add computerBundle() from @cloudflare/computer/vite to your plugins and import the directory. This works in vite dev too, with edits picked up as you go. Text files only:

// vite.config.ts: plugins: [cloudflare(), computerBundle()]
import { WorkerBundle } from "@cloudflare/computer";
import skills from "computer:bundle/src/skills";

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

With wrangler, pass a path instead. It's read from the Worker upload under /bundle, so each file has to be uploaded under its own path:

// wrangler.jsonc, with "main": "src/index.ts"
"find_additional_modules": true,
"rules": [{ "type": "Data", "globs": ["skills/**/*"], "fallthrough": true }]
mounts: { "/workspace/.agents/skills": WorkerBundle("skills") }

See Mount Interface for the options, types, and how refresh works.

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 codemode 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. See docs/05_runtime_interface.md.

Choosing a backend

Backend Status Import Runs Needs
Container Experimental @cloudflare/computer/backends/container Shell commands in full Linux userland (real binaries, npm, node, network) A Cloudflare Container running computerd, scheduled by the durable object (scheduling_policy: "durable_object")
Container (legacy) Deprecated @cloudflare/computer/backends/container-legacy The same A Cloudflare Container the platform schedules and sizes from the containers block
Worker shell Experimental @cloudflare/computer/backends/worker-shell Shell commands via just-bash in a Dynamic Worker A Worker Loader binding; experimental flag; the just-bash package
Codemode Stable @cloudflare/computer/backends/codemode ECMAScript modules in a fresh Dynamic Worker A Worker Loader binding
  • Container cold-starts more slowly but gives you a real Linux environment. The container owns its own SQLite-backed VFS and this package syncs the two stores across a capnweb WebSocket. See docs/07_injected_service.md for the container image, and examples/container.
  • Container (legacy) is deprecated. It serves containers the platform schedules from the containers block. Move to the container backend, which needs scheduling_policy: "durable_object" and an images map instead.
  • 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. See docs/12_worker_backend.md and examples/worker-shell.
  • Codemode evaluates a module with structured input/results, durable relative imports, configured libraries, Workspace-backed node:fs/promises, and host modules such as codemode:git, codemode:artifacts, and codemode:container. It runs after runtime.exec() returns; the run stays alive while its event stream is consumed. See docs/17_isolate_javascript.md and examples/codemode.

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

Remote exec

@cloudflare/computer/remote-exec is a third way to reach a container, experimental for now. Codemode code imports codemode:exec and runs a command in any Durable Object with ctx.container.exec(). No computerd, no FUSE and no long-lived sync: the files a command names are pushed before it runs, and what it changed is pulled back after.

import { exec } from "codemode:exec";

const proc = await exec(["npm", "test"], {
  cwd: "/workspace/app",
  push: ["**", "!node_modules/**"],
  pull: ["coverage/**"],
});
const { exitCode, stdout, sync } = await proc.output();

Globs follow .gitignore rules, push only sends files the container doesn't have, and commands that sync take turns. argv runs without a shell. exec() takes ctx.container.exec()'s options and returns a process shaped exactly like its, with streams, exitCode, output() and kill(), in every run, replayable ones included. output() adds the sync reports, and keeps each stream's tail, with the rest saved under the Workspace's output settings.

Inside the container, a small static binary, remote-exec, runs the commands and syncs the files. It isn't in the npm package. Copy it into your image from the image of the same version, also at registry.cloudflare.com/library/computer-remote-exec:

FROM ghcr.io/cloudflare/computer-remote-exec:0.11.2 AS remote-exec
FROM debian:stable-slim
COPY --from=remote-exec /usr/local/bin/remote-exec /usr/local/bin/remote-exec
CMD ["sleep", "infinity"]

Or download remote-exec-linux-x86_64 from the matching @cloudflare/computer GitHub release (SHA256SUMS lists its hash), upload it with the Worker, and pass binaries: fileRemoteExec({ x86_64: "/bundle/remote-exec" }): the module streams it in on first use. That works with any image.

import { CodemodeBackend } from "@cloudflare/computer/backends/codemode";
import { DurableObject } from "cloudflare:workers";
import { createExecModule, type ExecArgs, executor } from "@cloudflare/computer/remote-exec";

// The container: started on the first command, stopped 10 minutes after the last.
export class Sandbox extends DurableObject<Env> {
  #executor = executor(this, { enableInternet: false });

  exec(...args: ExecArgs) {
    return this.#executor.exec(...args);
  }
}

// In the Durable Object that holds the Workspace:
new CodemodeBackend({
  loader: env.LOADER,
  modules: {
    "codemode:exec": createExecModule({
      target: () => env.SANDBOX.getByName(ctx.id.toString()),
      fs: () => this.workspace.fs,
    }),
  },
});

Give executor() an outbound(egress) hook to send all of the container's HTTP and HTTPS through a Fetcher, such as an entrypoint of your Worker; the container then trusts the platform's intercepting CA. egress is whatever the caller passed as exec()'s third argument, and withEgress(stub, value) passes it with every command the module runs. readOnly: true runs commands as nobody and refuses pull and user, for a read-only backend. See 20. Remote exec for setup and every option, and examples/remote-exec.

Tools for agents

@cloudflare/computer/tools/ai-sdk ships createAITools(), 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, ls, find, grep, write, edit, and delete, plus exec when the Workspace has a backend and publish when assets are configured. Read-only mode keeps read, ls, find, and grep.

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

const tools = createAITools({
  workspace,
  read: { maxBytes: 32 * 1024, maxLines: 800 },
  // The backends the model can use. Omit for every backend.
  exec: { backends: { shell: { description: "Try this first." }, container: {} } },
});

Each backend describes itself to the model, and the text you give in exec.backends comes first. The model reads both when deciding where a command should run, so write yours in plain language. Truncated text model output keeps both line and byte continuations; pass both to the next call to avoid transferring the same bytes again. Eligible image and PDF bytes are captured once during the bounded tool execution and returned as AI SDK file model output without re-reading the file. SVG source remains text. ls, find, and grep pass pagination through to the storage layer and return nextOffset when more results exist. edit falls back to Unicode- and whitespace-tolerant matching while splicing replacements into the original text, so untargeted content stays byte-for-byte unchanged. File mutations share locks across tool sets for the same workspace, and recursive deletion excludes mutations throughout its subtree. See docs/09_tool_interface.md.

The same tools, with the same options, come for two more agent libraries. Each entry point loads only zod and its own code, so importing one never pulls in another library.

import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
import { createTanStackTools } from "@cloudflare/computer/tools/tanstack-ai";

// pi: declarations for the model, and a function your loop calls per tool call.
const { tools, execute } = createPiTools({ workspace });

// TanStack AI: a list for chat({ tools }). This one asks before changing files.
const tanstackTools = createTanStackTools({ workspace, approve: "mutating" });

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" });

Pass a Fetch-compatible implementation to route Git traffic through a host wrapper or a service binding. It covers typed calls, the shell's git command and codemode:git:

createGitClient({ fetch: env.GIT_SERVICE.fetch.bind(env.GIT_SERVICE) });

For private repositories, a host fetch wrapper can inject credentials without putting tokens in model-visible commands or remote URLs. The host must scope credentials to trusted HTTPS repositories and enforce redirect safety; see the authentication example.

The git subpath bundles isomorphic-git lazily and swaps its pako dependency for the Workers node:zlib implementation, so the default package graph stays free of git. 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. See docs/13_git_interface.md.

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. See docs/14_assets_interface.md.
  • Artifacts (@cloudflare/computer/artifacts): createArtifact(binding, sessionId) is a wrapper over the Cloudflare Artifacts binding. Every repository name is implicitly prefixed with the session id, so one namespace hosts many isolated sessions. The session id is optional: leave it off and the client spans the namespace, listing and reaching every repository including those other sessions own.
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

const all = createArtifact(env.ARTIFACTS);
await all.list(); // every repo in the namespace, under its stored name

Artifacts also offers an argv CLI (artifacts.cli({ argv })), and when Workspace is configured with an Artifacts binding the worker-shell backend exposes an artifacts command. See docs/15_artifacts_interface.md.

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. The full contract is in docs/11_lifecycle.md.

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

Entrypoint Purpose
@cloudflare/computer The Workspace wrapper, workspace.runtime, stub types, the R2 and Worker bundle mounts, and proxy classes.
@cloudflare/computer/backends/container ContainerBackend and withWorkspaceContainer. Pulls in the computerd / capnweb sync plumbing.
@cloudflare/computer/backends/container-legacy Deprecated. LegacyContainerBackend and withLegacyWorkspaceContainer; use @cloudflare/computer/backends/container instead.
@cloudflare/computer/backends/worker-shell WorkerShellBackend and the bundled just-bash runtime.
@cloudflare/computer/backends/codemode CodemodeBackend, configured libraries, durable imports, node:fs/promises, and host modules.
@cloudflare/computer/modules/container createContainerModule() for codemode:container: run container commands from isolate JavaScript.
@cloudflare/computer/modules/git createGitModule() for codemode:git: confined Git from isolate JavaScript.
@cloudflare/computer/modules/artifacts createArtifactsModule() for codemode:artifacts: Artifacts and git tokens from isolate JavaScript.
@cloudflare/computer/modules/assets createAssetsModule() for codemode:assets: publish a Workspace file from isolate JavaScript.
@cloudflare/computer/modules/tools createToolBindings() for codemode:tools: an agent's own tools, callable from isolate JavaScript.
@cloudflare/computer/modules/browser createBrowserModule() for codemode:browser: an agents Browser, driven from isolate JavaScript.
@cloudflare/computer/remote-exec createExecModule() for codemode:exec: run commands in any container Durable Object, with Workspace files synced in and out. Also ExecSandbox, withEgress(), bundledRemoteExec(), and remoteExec().
@cloudflare/computer/tools AI SDK tools for agents: read, ls, find, grep, write, edit, delete, and optional exec and publish.
@cloudflare/computer/tools/core defineExec(): the exec tool's description, schema, and executor, with no agent library.
@cloudflare/computer/tools/ai-sdk createAITools(): the AI SDK tool set for a Workspace, and forAISDKTools() to serve AI SDK tools as codemode:tools.
@cloudflare/computer/tools/pi-ai createPiTools(): the same tool set for pi (@earendil-works/pi-ai), and forPiTools() to serve pi tools as codemode:tools.
@cloudflare/computer/tools/tanstack-ai createTanStackTools(): the same tool set for TanStack AI (@tanstack/ai).
@cloudflare/computer/git Opt-in isomorphic-git glue for checkouts inside the workspace.
@cloudflare/computer/assets createAssets β€” share a workspace file to R2 as a presigned URL.
@cloudflare/computer/artifacts createArtifact and its CLI, an optionally session-scoped wrapper over the Cloudflare Artifacts binding.
@cloudflare/computer/observe/cloudflare Cloudflare-runtime adapter for the observability hook.
@cloudflare/computer/vite computerBundle(): a Vite plugin that serves computer:bundle/<dir> imports for WorkerBundle. remoteExec() (experimental): a Vite plugin that downloads the remote-exec builds and serves them as computer:remote-exec.
@cloudflare/computer/vite/client Types for computer:bundle/<dir> and computer:remote-exec imports.

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. WorkerShellBackend defaults to "worker-shell", ContainerBackend to "container-shell", and CodemodeBackend to "codemode". 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 ContainerBackend({ 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 codemode 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

Resuming an incomplete sync

A command can change backend files and then have its post-command pull fail; the result exposes sync: { status: "pending", ... }. Nothing further is required to recover it: the next pull() that runs to completion has pulled every command that reported pending before it started. The fetch cursor is durable, so it resumes where the failed pull stopped.

A deferred exec fixes a pull target when the command finishes. If a pull is already pending, the command joins it, and pull() keeps going past that older target until the command's changes are in.

A pending sync remembers the container runtime that ran the command. If that container was replaced before the sync finished, its changes are gone with it, and the next pull() rejects with ESYNC_LOST instead of reporting a clean sync from the empty replacement. The pull() after that synchronizes the live container.

Attempt counts, backoff, and exhaustion are not part of the API: a caller that wants to stop trying stops iterating.

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. An adapter for the Cloudflare runtime lives at @cloudflare/computer/observe/cloudflare.

Examples

  • examples/worker-shell β€” the worker-shell backend behind a write / read / exec HTTP surface. No container.
  • examples/codemode β€” the same shape, running ECMAScript modules instead of shell commands.
  • examples/container β€” the container backend running computerd.
  • examples/container-legacy β€” the deprecated legacy container backend.
  • examples/remote-exec β€” codemode running commands in a container through codemode:exec, with end-to-end tests.
  • examples/think β€” a chat agent that uses the workspace as its working directory.
  • examples/tutorial β€” a step-by-step build: write a markdown card on the host, render it to PDF with pandoc in the container.

Documentation

The design specification lives under docs/. Its topic index marks each document stable or experimental. Start with 01. VFS, 04. Filesystem Interface, and 05. Runtime Interface. It is forward-looking β€” read it for intent, not as a description of the code today.

License

MIT. See LICENSE.