| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
Important
PREVIEW ONLY This package is provided as a preview for feedback only. APIs are unstable and the design is subject to change.
Suitable for experiments, exploration and prototypes. It is NOT suitable for production use at this time.
The specification under docs/ is forward-looking — read it for intent, not as description of the code today.
Durable Object-side facade for Cloudflare Computer. Pairs a local SQLite-backed VFS (via @cloudflare/dofs) with pluggable execution backends selected through workspace.runtime.exec().
Three backends ship today on tree-shakeable subpaths:
The worker-JavaScript backend runs after runtime.exec() returns. Pass waitUntil: ctx.waitUntil.bind(ctx) to Workspace so completion remains attached to the Durable Object event. The backend refuses to connect without this lifecycle hook. It admits one execution at a time by default and bounds completed execution retention by time and count.
A backend can declare sync: "none" on the handle it returns to opt out of the push/pull bracket entirely — the worker backend does this because its shell shares the host store directly. The bracket still runs around runtime.exec so the surface stays uniform; the counts are just always zero.
Container backend:
import { Workspace, WorkspaceProxy } from "@cloudflare/computer";
import { CloudflareContainerBackend, withWorkspaceContainer }
from "@cloudflare/computer/backends/container";
import { DurableObject } from "cloudflare:workers";
export { WorkspaceProxy };
export class ContainerExample extends withWorkspaceContainer(class extends DurableObject<Env> {}) {
#workspace = new Workspace({
storage: this.ctx.storage,
backends: [
new CloudflareContainerBackend({
container: () => this,
workspace: { binding: "ContainerExample", id: this.ctx.id.toString() },
}),
],
});
async getWorkspace(): Promise<WorkspaceStub> {
await this.#workspace.ready();
return this.#workspace.stub();
}
override fetch(req: Request) { return this.#workspace; /* see example */ }
}Worker backend:
import { Workspace, WorkspaceServiceProxy } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import { DurableObject } from "cloudflare:workers";
export { WorkspaceServiceProxy };
export class WorkerExample extends DurableObject<Env> {
#workspace = new Workspace({
storage: this.ctx.storage,
backends: [
new WorkerShellBackend({
loader: this.env.LOADER,
workspace: { binding: "WorkerExample", id: this.ctx.id.toString() },
ctx: this.ctx,
}),
],
});
async getWorkspace(): Promise<WorkspaceStub> {
await this.#workspace.ready();
return this.#workspace.stub();
}
}Filesystem only — no execution backend:
const ws = new Workspace({ storage: ctx.storage });
await ws.ready();
await ws.fs.writeFile("/notes.md", "hello");
const body = await ws.fs.readFile("/notes.md", "utf8");
// ws.runtime throws — there's no backend wired up.Pass useThink: true when assigning a workspace to Think.workspace. That adds Think's compatibility methods directly to the local instance and to clients returned by getWorkspace(), while keeping workspace.fs and workspace.runtime as the primary surfaces.
Git, also without a backend:
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" });
const log = await ws.git.log({ depth: 1 });Every workspace.git operation reads and writes through the local store; no backend or shell is required. See the doc above for the full method surface, error hierarchy, and CLI shape.
createArtifact(binding, sessionId) wraps the Cloudflare Artifacts Workers binding in a session-scoped client. Names go in and out local; the session id is added as a prefix on the way to the namespace and stripped on the way back.
import { createArtifact } from "@cloudflare/computer/artifacts";
const artifacts = createArtifact(env.ARTIFACTS, agentId);
const repo = await artifacts.create("build-cache", {
description: "CI artifacts for this agent",
});
// repo.name -> "build-cache"; stored as `${agentId}__build-cache`.
const token = await artifacts.createToken("build-cache", "read", 3600);
// token.plaintext is a git token — a secret, shown once.
const mine = await artifacts.list();
// Only this session's repos, names unscoped.The binding (Artifacts) and its result shapes are the global types from a Workers project's @cloudflare/workers-types setup; the facade adds session scoping on top rather than redeclaring the wire protocol. Pass the binding to Workspace as artifacts: { binding: env.ARTIFACTS } to expose the same surface as an in-shell artifacts command. See docs/15_artifacts_interface.md.
A Workspace can carry more than one backend. Each backend registers under a stable selector id (defaulting to "worker-shell", "container-shell", or "worker-javascript"; this is intentionally separate from the diagnostic type). runtime.exec picks the default (the first backend in the list) unless the caller names one through WorkspaceRuntimeExecOptions.backend. Per-backend sync cursors live in dofs's _vfs_watermark table keyed by the same id; a push or pull against one backend never disturbs the other's cursors.
A backend also declares whether it is callable. A callable backend accepts a structured input value on runtime.exec and returns a structured value on the result; the worker-javascript backend sets callable: true because it runs a module that takes an argument and produces a return value. This is independent of the backend kind — a shell backend that coerced JSON into argv and stdin and parsed stdout back into a value could declare itself callable too. When a caller passes input to a backend that is not callable, the runtime rejects the call with a clear error rather than silently dropping the value, so a custom backend must set callable: true before it can receive input.
const workspace = new Workspace({
storage: ctx.storage,
backends: [
new WorkerShellBackend({
id: "shell",
loader: env.LOADER,
workspace: { binding: "AgentDO", id: ctx.id.toString() },
ctx,
}),
new CloudflareContainerBackend({
id: "sandbox",
container: () => this,
workspace: { binding: "AgentDO", id: ctx.id.toString() },
}),
],
});
// Default: the first backend in the list runs the command.
const grep = await ws.runtime.exec("grep -r TODO /workspace");
// Explicit: route a heavy build to the container.
const build = await ws.runtime.exec("npm test", { backend: "sandbox" });Backends connect lazily — the first exec (or push / pull / ready(id)) for an id dials it. ready({ all: true }) pre-warms every configured backend in parallel; useful from an agent's onStart. Workspace.push(id?) and Workspace.pull(id?) target a single backend, defaulting to the first one.
A workspace with two backends that both write into /workspace has no global ordering between them; see docs/05_runtime_interface.md for the caveat.
A command can finish after changing backend files while its post-command pull fails. The command result exposes sync: { status: "pending", error, ... }. Configure a SyncRetryScheduler on Workspace to persist one coalesced retry intent per backend, then call workspace.retryPendingSync(backend) from the owning Durable Object's alarm or another durable scheduler. Retries share the same per-backend mutation FIFO as push, pull, and command brackets, use bounded exponential backoff, clear their intent after success, and return "exhausted" after the configured maximum. The library intentionally does not own the host Durable Object's alarm.
See SyncRetryScheduler, SyncRetryIntent, and SyncRetryOptions in the root package exports for the persistence contract.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.ContainerExample.idFromName("user-123");
using ws = await env.ContainerExample.get(id).getWorkspace();
await ws.fs.writeFile("/notes.md", "hello");
using handle = await ws.runtime.exec("ls /workspace");
const { exitCode, stdout } = await handle.result();
return new Response(stdout, { status: exitCode === 0 ? 200 : 500 });
},
} satisfies ExportedHandler<Env>;The package emits one span per documented operation through an optional observer hook. Pass an observer to the Workspace constructor:
import { Workspace, type WorkspaceObserver } from "@cloudflare/computer";
const observer: WorkspaceObserver = {
async span(name, attributes, run) {
// Wrap `run` however your tracing backend wants. The Cloudflare
// runtime, OpenTelemetry, and a plain console.log adapter all fit
// the same shape.
return run({ setAttribute: () => {} });
},
};
const ws = new Workspace({
storage: this.ctx.storage,
backends: [...],
observer,
});The observer's span(name, attributes, run) wraps each operation. It starts a span, runs the callback, and ends the span when the callback returns or its promise settles. Errors thrown by the work record error.name and error.message and propagate.
The span names the package emits today:
Attribute values are restricted to boolean | number | string so the same observer shape works against the Cloudflare runtime's built-in ctx.tracing.enterSpan(...) API, OpenTelemetry, or a recording test observer. Adapter packages for the Cloudflare runtime and for OpenTelemetry are forthcoming.
The default is a no-op observer with no allocation or async overhead beyond what the callback itself does, so the package has no observability cost when callers do not opt in.
capnweb does not garbage-collect remote stubs. On the long-lived sessions this package depends on (Worker ↔ DO over Workers RPC, DO ↔ computerd over capnweb), undisposed stubs accumulate on the peer side until the session ends. The worker backend uses Workers RPC over an isolate boundary rather than capnweb, but the disposal discipline is the same.
The minimum a caller needs to know:
Short-lived single-shot Workers (one getWorkspace(), a few calls, return a response) tear the session down with the request, so the discipline matters most on long-lived isolates that keep grabbing fresh WorkspaceStubs or on busy exec workloads inside a single request.
The full contract — including the boundary between the driver code and direct streaming callers, and how it interacts with hibernation and reconnect — is in docs/11_lifecycle.md.
Leak discovery: set CAPNWEB_TRACK_STUBS=1 and read the snapshot via stubSnapshot() from @cloudflare/computer-rpc/debug, or hit GET /__computerd/stubs on a computerd instance. The soak scripts at script/computerd-stub-soak.mjs and tests/stub-soak.test.ts exercise both boundaries.
| Back | FazBrowse Home | New Git URL |