Skip to content

feat(agents): experimental ContainerHarness: Claude Code or Codex in a Container, driven from a Durable Object - #2514

Merged
mattzcarey merged 4 commits into
mainfrom
feat/container-harness-pi-shape
Oct 7, 2026
Merged

mattzcarey merged 4 commits into
mainfrom
feat/container-harness-pi-shape

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Adds experimental agents/harness/container: ContainerHarness runs an agent CLI (Claude Code or Codex) in a Cloudflare Container and drives it from a Durable Object. Its API matches PiHarness.

import { ContainerHarness, claudeCode, codex } from "agents/harness/container";

// Adds credentials outside the container; export it from the main module.
export { ContainerEgress } from "agents/harness/container";

export class CodingAgent extends DurableObject<Env> {
  readonly harness = new ContainerHarness({
    container: this.ctx.container,
    egress: this.ctx.exports.ContainerEgress,
    agent: claudeCode({                                 // or codex({ ... })
      baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`,
      apiKey: this.env.AI_GATEWAY_TOKEN
    })
  });
  readonly lifecycle = Lifecycle.install(this).use(this.harness);
}

const { text } = await this.harness.prompt("fix the failing test");
await this.harness.sessions.fork("root");
"containers": [{ "class_name": "CodingAgent", "scheduling_policy": "durable_object" }]

You don't build an image or write a Dockerfile, and the container never holds a credential.

How it works

flowchart LR
  subgraph DO[Durable Object]
    H[ContainerHarness] --- S[(HarnessStore<br/>sessions · operations<br/>transcript · CLI session files)]
  end
  subgraph W[Worker]
    E[ContainerEgress]
  end
  subgraph C[Container: cloudflare/debian-trixie]
    D[daemon] -->|one process per turn, as uid 10001| CLI[claude -p / codex exec]
  end
  H <-->|WebSocket per session via getTcpPort| D
  CLI -->|http://anthropic.harness.internal<br/>placeholder key| E
  E -->|real key| GW[AI Gateway / provider]
Loading
  • No image. On first start the harness:

    • starts cloudflare/debian-trixie;
    • runs the preset's setup steps with exec() (an unprivileged user, npm install -g of the CLI, your own steps);
    • writes in the daemon (23 KiB, bundled into agents at build time);
    • snapshots the filesystem.

    Later starts restore a snapshot. Changing the setup sets up again.

  • Customise with setup only. A step with user: "agent" runs in the agent's home, and every session home starts as a copy of it. Installing plugins and mods, settings, skills or a fork works as it does on a laptop:

    setup: [{ name: "my mods", user: "agent", command: ["sh", "-c", "claude plugin marketplace add you/mods && claude plugin install mine@mods"] }]
  • Credentials stay outside the container. The CLI calls a placeholder host over plain HTTP with a placeholder key. interceptOutboundHttp hands that request to ContainerEgress, which swaps in apiKey / headers and forwards to baseUrl (https only).

  • The daemon (agents/harness/container/runtime, no dependencies):

    • numbers frames and replays from the object's cursor;
    • queues turns;
    • runs the CLI and turns its JSON lines into messages and events;
    • mirrors the CLI's session folder (~/.claude/projects, ~/.codex/sessions) to the object.

    For any other CLI, cliAdapter({ command, parser, stateDirs }) is the extension point.

  • Durable, like PiHarness.

    • Each session has one Lifecycle wake job (singleflight, recoveryLoop, a heartbeat) that starts or reattaches, reconciles and waits.
    • Operations, the transcript, cursors and the start-failure count all live in SQLite or in the job.
  • Session store (agents/harness/store): sessions, operations with idempotency keys, and logs of opaque JSON. It doesn't depend on any one harness.

Recovery

Lost What happens
Container idle After idleTimeoutMs (5 min) the harness snapshots the container, workspace included, and stops it. The next prompt starts from that snapshot; the CLI resumes its own session (--resume, codex exec resume).
Container dies mid-run The run settles container_lost (or reruns with onContainerLost: "retry"). The next container starts from the last snapshot plus the CLI's session files, and the session continues.
Object evicted mid-run The container keeps going. The wake job reattaches and replays missed frames from its cursor.
Snapshot unusable One failed start retries the same snapshot. After two in a row it is skipped for the next source (workspace → setup snapshot → fresh setup). If a fallback starts, the skipped snapshot is probed once more: it is kept if it starts, dropped if it fails. Snapshots too old to restore (30 days since last use) are skipped.

Example

examples/next/harnesses/container runs Claude Code and Codex side by side: two Durable Objects that differ only in their preset.

All new entries are marked @experimental.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b28edcd

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

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think 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
Contributor

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 7 potential issues.

Devin Review

Comment thread packages/agents/src/harness/container/harness.ts Outdated
Comment thread examples/next/harnesses/container/src/index.ts
Comment thread packages/agents/src/harness/container/harness.ts
Comment thread examples/next/harnesses/container/src/index.ts
Comment thread packages/agents/src/harness/container/egress.ts
Comment thread packages/agents/src/harness/container/harness.ts
Comment thread examples/next/harnesses/container/wrangler.jsonc
@agent-think

agent-think Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 4 entry points changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/harness/container 9 new — 19.2 KiB
🆕 agents/harness/container/daemon 14 new — 2.6 KiB
🆕 agents/harness/container/runtime 5 new — 4.5 KiB
🆕 agents/harness/store 2 new — 2.4 KiB
Changed exports (30)
Import Gzip change Size now
🆕 agents/harness/container#ContainerHarness — 19.2 KiB
🆕 agents/harness/container/runtime#serveFromEnv — 4.5 KiB
🆕 agents/harness/container/runtime#serve — 4.3 KiB
🆕 agents/harness/container/runtime#cliAdapter — 2.7 KiB
🆕 agents/harness/container/daemon#ContainerDaemon — 2.6 KiB
🆕 agents/harness/store#openHarnessStore — 2.4 KiB
🆕 agents/harness/store#HarnessStore — 2.3 KiB
🆕 agents/harness/container/runtime#claudeCodeCli — 2.2 KiB
🆕 agents/harness/container/runtime#codexCli — 1.6 KiB
🆕 agents/harness/container#claudeCode — 1.1 KiB
🆕 agents/harness/container#ContainerSessions — 1 KiB
🆕 agents/harness/container#ContainerSession — 1 KiB
🆕 agents/harness/container#codex — 1 KiB
🆕 agents/harness/container#ContainerEgress — 977 B
🆕 agents/harness/container#containerAgent — 755 B
🆕 agents/harness/container/daemon#parseDaemonMessage — 747 B
🆕 agents/harness/container#echoAgent — 708 B
🆕 agents/harness/container#ROOT_SESSION — 656 B
🆕 agents/harness/container/daemon#echoAdapter — 644 B
🆕 agents/harness/container/daemon#parseHostMessage — 639 B
🆕 agents/harness/container/daemon#chunkEntries — 176 B
🆕 agents/harness/container/daemon#inputText — 132 B
🆕 agents/harness/container/daemon#CONTAINER_ENV — 124 B
🆕 agents/harness/container/daemon#CONTAINER_TOKEN_HEADER — 85 B
🆕 agents/harness/container/daemon#CONTAINER_SESSION_PATH — 77 B
🆕 agents/harness/container/daemon#CONTAINER_HEALTH_PATH — 75 B
🆕 agents/harness/container/daemon#CLOSE_REPLACED — 69 B
🆕 agents/harness/container/daemon#CONTAINER_PROTOCOL_VERSION — 66 B
🆕 agents/harness/container/daemon#CLOSE_UNAUTHORIZED — 65 B
🆕 agents/harness/container/daemon#MAX_CHUNK_CHARS — 58 B
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared bda70b52 → b28edcd7 · workflow run · reported by agent-think[bot]

@pkg-pr-new

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

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2514

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2514

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2514

hono-agents

npm i https://pkg.pr.new/hono-agents@2514

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2514

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2514

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2514

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2514

commit: b28edcd

devin-ai-integration[bot]

This comment was marked as resolved.

@mattzcarey
mattzcarey force-pushed the feat/container-harness-pi-shape branch from 0be9dae to cd23a9d Compare October 7, 2026 04:16
The platform does not say why a start failed, so a failure no longer
deletes the snapshot it came from. Snapshots are skipped once they are
too old to restore (their 30-day lifetime runs from the last restore),
and otherwise forgotten only after failing three times over at least ten
minutes. Failures after the container started (inactivity timeout, egress
intercepts) say nothing about the snapshot and are not counted.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

Devin Review

Comment thread packages/agents/src/harness/container/harness.ts Outdated
A snapshot that fails two starts in a row is skipped (not deleted) in
favour of the next source, so a prompt's five attempts run workspace,
workspace, setup snapshot, setup snapshot, fresh setup. The fallback's
result decides who was at fault: if it starts, the skipped snapshot is
broken and dropped; if it fails too, the snapshot is not to blame and its
count is cleared. A wake that gives up clears every count, so the next
prompt tries the workspace first again.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Devin Review

Comment thread packages/agents/src/harness/container/harness.ts Outdated
Comment thread packages/agents/src/harness/container/harness.ts
…dropping it

A fallback that starts proves only that the platform can start
containers again, not that the skipped snapshot is broken. The harness
now stops that container and tries the skipped snapshot once more: if it
starts, the workspace is kept; if it fails, it is broken and dropped, and
the fallback starts again. The probe does not count against the start
budget.
@mattzcarey
mattzcarey merged commit 1203bdd into main Oct 7, 2026
18 checks passed
@mattzcarey
mattzcarey deleted the feat/container-harness-pi-shape branch October 7, 2026 05:58
@github-actions github-actions Bot mentioned this pull request Oct 6, 2026
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