# CapnWeb RPC Interface for Durable Object to Computerd Communication

> Explore the CapnWeb RPC interface for Durable Object to computerd communication. Learn how WebSockets and WorkspaceRPC enable filesystem sync and process execution.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: api-reference
- Published: 2026-08-08

---

**The CapnWeb RPC interface connects Cloudflare Durable Objects to the in-container computerd process via a long-lived WebSocket carrying JSON-framed WorkspaceRPC calls, split into SyncRPC for filesystem synchronization and ShellRPC for process execution.**

The cloudflare/computer repository implements a sandboxed compute environment where Durable Objects orchestrate containers running the computerd daemon. Communication between these layers relies on the CapnWeb RPC interface, a lightweight transport that exposes filesystem changes and command execution primitives over a stable WebSocket channel. This architecture enables bidirectional synchronization of filesystem state and interactive shell access while maintaining strict error boundaries between the DO and container contexts.

## Transport Layer and Connection Model

The CapnWeb RPC layer operates over two primary transport mechanisms defined in the computerd architecture. Understanding these endpoints is critical for implementing custom clients or debugging connection failures.

### WebSocket and HTTP Fallback Endpoints

The primary transport uses a **single long-lived WebSocket** connected to the `/ws` endpoint. When WebSocket connections are unavailable, the system falls back to an HTTP batch interface at `/api`. The wire format strictly uses **JSON-text frames**; binary WebSocket frames are not supported in the current implementation.

Connection resilience is built into the protocol: when the socket closes or errors, the Durable Object side aborts the session synchronously. Subsequent RPC calls automatically trigger re-establishment of a fresh connection, ensuring that transient network disruptions do not require manual reconnection logic at the application level.

## WorkspaceRPC Root Interface

The RPC surface exposed over this channel centers on the **`WorkspaceRPC`** interface defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts). This top-level stub acts as the exclusive entry point for all cross-container communication and splits cleanly into two independent halves.

### SyncRPC for Filesystem Operations

**`SyncRPC`** handles bidirectional synchronization of filesystem changes and object blobs. The DO pushes batches of `ChangeEntry` records (containing only chunk hashes) via the `push` method. The container probes its local cache using `hasObjects`, then the DO streams missing blobs via `pushObjects`. The reverse flow operates through `fetchChanges` followed by `fetchObjects`, allowing each side to pull missing updates. Diagnostic visibility comes through `watermarks`, which expose current revision numbers and fetch cursors.

### ShellRPC for Process Supervision

**`ShellRPC`** provides process-supervision primitives including `exec`, `getExec`, `killExec`, and `disposeExec`. This separation keeps sync logic testable while presenting a single stable stub to callers, preventing filesystem operations from blocking on long-running shell commands.

## Synchronization Protocol Details

The filesystem synchronization protocol operates on a content-addressed model where only hashes traverse the RPC boundary initially.

### Pushing Changes from DO to Container

When the Durable Object has local filesystem modifications, it calls `sync.push()` with a `ReadableStream<ChangeEntry>` and the current revision number. The container examines the change entries, identifies missing object hashes, and requests them via subsequent `hasObjects` and `pushObjects` calls. The response contains the new remote revision (`rev`) and the applied push cursor, confirming atomic application of the batch.

### Fetching Incremental Updates

To pull updates from the container, the DO invokes `sync.fetchChanges()` passing a `ChangeCursor` representing the last seen state. The response provides a stream of `ChangeEntry` objects along with `currentCursor` and `appliedPushCursor` metadata, enabling the DO to resume synchronization without missing intermediate updates.

## Shell Execution and Stream Management

Process execution follows a streaming model that maintains back-pressure between the container and the orchestrating Durable Object.

### Spawning and Monitoring Processes

The `shell.exec()` method accepts a command specification including `source` and `cwd`, returning a unique execution `id` and a `ReadableStream<ExecEvent>`. This stream yields stdout, stderr, and exit frames until the process terminates. Consumer back-pressure propagates to the spawned process, preventing buffer overflow in the container.

### Lifecycle Management

After disconnections, the DO can re-attach to running executions using `getExec()` with the execution id. Termination signals travel via `killExec()`, while `disposeExec()` performs cleanup of completed process logs. These primitives allow the DO to survive intermittent connectivity without losing track of background jobs.

## Error Handling with WireError

Errors traverse the CapnWeb boundary as structured **`WireError`** objects carrying specific `code` values. Common error codes include `ENOENT` for missing files, `EUNKNOWN_HASH` for unrecognized content addresses, and `EEXEC_BUSY` when attempting conflicting operations on active executions.

The client adapter in the Durable Object re-throws these as `WorkspaceError` instances, preserving the error code for reliable handling. This structure allows the DO to distinguish between filesystem errors, RPC transport failures, and container state conflicts without parsing error message strings.

## Versioning and Compatibility Constraints

The RPC contract has **no runtime version negotiation** mechanism. Any change to request or response shapes constitutes a hard wire break requiring coordinated rollout of both the Durable Object code and the computerd container image. This design choice eliminates compatibility code paths but mandates strict deployment synchronization between edge and container infrastructure.

## Implementation Examples

### Creating a CapnWeb RPC Session

The following pattern from [`packages/computer/src/backends/container/cloudflare-container.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/cloudflare-container.ts) demonstrates establishing the root WorkspaceRPC stub:

```typescript
import { RpcTarget } from "capnweb";
import { WorkspaceRPC } from "@cloudflare/computer-rpc";
import { newWebSocketRpcSession } from "capnweb";

/**
 * Connect to the computerd WebSocket endpoint and obtain the WorkspaceRPC stub.
 */
async function createWorkspaceClient(url: string): Promise<WorkspaceRPC> {
  // `url` points to the DO's `/ws` endpoint, e.g. `https://example.com/ws`
  const wsTransport = await newWebSocketRpcSession(url);
  // The root stub implements `WorkspaceRPC`
  const rootStub: RpcTarget = wsTransport.stub;
  // The `RpcTarget` is a Proxy that exposes the typed interface
  return rootStub as WorkspaceRPC;
}

```

### Pushing Filesystem Changes

To synchronize local state to the container:

```typescript
async function pushChanges(workspace: WorkspaceRPC) {
  const changes: ReadableStream<ChangeEntry> = /* … generate ChangeEntry stream … */;

  const result = await workspace.sync.push({
    senderRev: 5,               // DO's current revision
    changes,
  });

  console.log("Remote rev:", result.rev);
  console.log("Applied push cursor:", result.appliedPushCursor);
}

```

### Fetching Remote Updates

To pull incremental changes from the container:

```typescript
async function fetchUpdates(workspace: WorkspaceRPC, after: ChangeCursor) {
  const { currentCursor, appliedPushCursor, stream } =
    await workspace.sync.fetchChanges({ after });

  // Consume the ChangeEntry stream
  const reader = stream.getReader();
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    // Process each ChangeEntry (e.g., apply to local VFS)
  }

  console.log("Current cursor:", currentCursor);
  console.log("Applied push cursor:", appliedPushCursor);
}

```

### Executing Shell Commands

To run a command and stream its output:

```typescript
async function runCommand(workspace: WorkspaceRPC) {
  const { id, events } = await workspace.shell.exec({
    source: "ls -l /workspace",
    cwd: "/workspace",
  });

  const reader = events.getReader();
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    if (value.name === "stdout") {
      console.log("[stdout]", new TextDecoder().decode(value.value));
    } else if (value.name === "stderr") {
      console.error("[stderr]", new TextDecoder().decode(value.value));
    } else if (value.name === "exit") {
      console.log(`Process exited with code ${value.code}`);
    }
  }

  // Clean up the exec log when finished
  await workspace.shell.disposeExec({ id });
}

```

## Summary

- The CapnWeb RPC interface uses a **WebSocket transport** at `/ws` with an HTTP fallback at `/api`, transmitting only JSON-text frames.
- The root **`WorkspaceRPC`** interface defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) splits into **SyncRPC** for filesystem operations and **ShellRPC** for process management.
- **Synchronization** follows a hash-then-content pattern: `ChangeEntry` records flow first, followed by blob transmission via `pushObjects` and `fetchObjects`.
- **Process execution** returns `ReadableStream<ExecEvent>` allowing real-time stdout/stderr consumption with automatic re-attachment support via `getExec`.
- **Errors** propagate as structured `WireError` objects with codes like `ENOENT` and `EEXEC_BUSY`, wrapped locally as `WorkspaceError`.
- **No runtime versioning** exists; protocol changes require coordinated deployment of Durable Object code and computerd images.

## Frequently Asked Questions

### What transport protocol does the CapnWeb RPC interface use?

The interface primarily uses a single long-lived **WebSocket** connection to the `/ws` endpoint, with an HTTP batch fallback at `/api` for environments unable to maintain WebSocket connections. The wire format strictly uses JSON-text frames, and binary WebSocket frames are not supported.

### How does error handling work across the Durable Object to computerd boundary?

Errors serialize as structured **`WireError`** objects containing a machine-readable `code` field (such as `ENOENT`, `EUNKNOWN_HASH`, or `EEXEC_BUSY`). The client adapter re-throws these as `WorkspaceError` exceptions, preserving the original error code for programmatic handling without requiring string parsing.

### What happens when the WebSocket connection drops?

The Durable Object side **synchronously aborts** the current session when the socket closes or errors. The next RPC call automatically triggers re-establishment of a fresh connection. For shell executions, the DO can use `getExec()` to re-attach to running processes after reconnection.

### Is there runtime version negotiation in the CapnWeb RPC protocol?

No. The RPC contract intentionally lacks runtime version negotiation. Any modification to request or response shapes constitutes a **hard wire break** that requires coordinated rollout of both the Durable Object implementation and the computerd container image to prevent serialization mismatches.