diff --git a/src/content/docs/agents/harnesses/claude-code.mdx b/src/content/docs/agents/harnesses/claude-code.mdx new file mode 100644 index 00000000000..e83d26ff5bf --- /dev/null +++ b/src/content/docs/agents/harnesses/claude-code.mdx @@ -0,0 +1,171 @@ +--- +title: Claude Code +pcx_content_type: get-started +description: Run Claude Code on Cloudflare. The CLI runs in a Container, and a Durable Object keeps its sessions, its credentials, and the record of every run. +sidebar: + order: 12 + badge: + text: Experimental +products: + - agents +--- + +import { + LinkCard, + PackageManagers, + Render, + TypeScriptExample, + WranglerConfig, +} from "~/components"; + +Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) on Cloudflare, with its own tools, plugins, and settings, the way it runs on your laptop. Claude Code runs in a [Container](/containers/). A [Durable Object](/durable-objects/) drives it, keeps every session and the CLI's session files, and holds your model credentials outside the container. + +This page uses the [Container harness](/agents/harnesses/container/) with its `claudeCode()` preset. For how the harness works, sessions, events, and recovery, refer to that page. + + + +## Install + + + +Add a SQLite-backed Durable Object and a container for the same class. The container must use the [`durable_object` scheduling policy](/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy), so the object can start it from snapshots: + + + +```jsonc +{ + "name": "claude-code-agent", + "main": "src/index.ts", + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [{ "name": "ClaudeCodeAgent", "class_name": "ClaudeCodeAgent" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ClaudeCodeAgent"] }], + "containers": [ + { "class_name": "ClaudeCodeAgent", "scheduling_policy": "durable_object" }, + ], + "vars": { + "AI_GATEWAY_URL": "https://gateway.ai.cloudflare.com/v1//", + }, +} +``` + + + + + +## Create the agent + +Create the harness with the `claudeCode()` preset, and add it to the [Lifecycle](/agents/runtime/lifecycle/). Export `ContainerEgress` from your Worker's main module. It adds your credentials to Claude Code's model requests after they leave the container: + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { routeAgentRequest } from "agents"; +import { ContainerHarness, claudeCode } from "agents/harness/container"; +import { Lifecycle } from "agents/lifecycle"; + +export { ContainerEgress } from "agents/harness/container"; + +export class ClaudeCodeAgent extends DurableObject { + readonly harness = new ContainerHarness({ + container: containerOf(this.ctx), + egress: this.ctx.exports.ContainerEgress, + agent: claudeCode({ + baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`, + apiKey: this.env.AI_GATEWAY_TOKEN, + }), + }); + + readonly lifecycle = Lifecycle.install(this).use(this.harness); + + async onRequest(request: Request): Promise { + if (request.method !== "POST") { + return Response.json(await this.harness.messages()); + } + const { text } = await request.json<{ text: string }>(); + const result = await this.harness.prompt(text); + return Response.json({ status: result.status, text: result.text }); + } +} + +function containerOf(ctx: DurableObjectState): Container { + if (!ctx.container) { + throw new Error("Add a container for this class in wrangler.jsonc"); + } + return ctx.container; +} + +export default { + async fetch(request: Request, env: Env) { + return ( + (await routeAgentRequest(request, env)) ?? + new Response("Not found", { status: 404 }) + ); + }, +} satisfies ExportedHandler; +``` + + + + + +## Configure Claude Code + +`claudeCode()` takes Claude Code's own options: + +| Option | Description | +| -------------------- | --------------------------------------------------------------- | +| `model` | The default model for new sessions, such as `claude-opus-5-5`. | +| `appendSystemPrompt` | Passed to `--append-system-prompt`. | +| `allowedTools` | Passed to `--allowedTools`. | +| `disallowedTools` | Passed to `--disallowedTools`. | +| `maxTurns` | Passed to `--max-turns`. | +| `version` | The `@anthropic-ai/claude-code` version to install. | +| `setup` | Commands that run once and are kept in the setup snapshot. | + +Use `setup` to install plugins, `CLAUDE.md`, settings, or skills. A step with `user: "agent"` runs as the user Claude Code runs as, so files in its home work the way they do on your laptop: + + + +```ts +import { claudeCode } from "agents/harness/container"; + +claudeCode({ + baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`, + apiKey: this.env.AI_GATEWAY_TOKEN, + model: "claude-opus-5-5", + appendSystemPrompt: "Run the tests before you report a change as done.", + setup: [ + { + name: "install review plugin", + user: "agent", + command: [ + "sh", + "-c", + "claude plugin marketplace add my-org/plugins && claude plugin install review@plugins", + ], + }, + ], +}); +``` + + + +Claude Code runs with `--dangerously-skip-permissions`, so it does not stop to ask before it runs a command. The container is the sandbox. Use `allowedTools` and `disallowedTools` to limit what it can do. + +## Next steps + + + + diff --git a/src/content/docs/agents/harnesses/codex.mdx b/src/content/docs/agents/harnesses/codex.mdx new file mode 100644 index 00000000000..5275546cc42 --- /dev/null +++ b/src/content/docs/agents/harnesses/codex.mdx @@ -0,0 +1,168 @@ +--- +title: OpenAI Codex +pcx_content_type: get-started +description: Run OpenAI Codex on Cloudflare. The CLI runs in a Container, and a Durable Object keeps its sessions, its credentials, and the record of every run. +sidebar: + order: 13 + badge: + text: Experimental +products: + - agents +--- + +import { + LinkCard, + PackageManagers, + Render, + TypeScriptExample, + WranglerConfig, +} from "~/components"; + +Run [OpenAI Codex](https://developers.openai.com/codex/cli) on Cloudflare, with its own tools, plugins, and settings, the way it runs on your laptop. Codex runs in a [Container](/containers/). A [Durable Object](/durable-objects/) drives it, keeps every session and the CLI's session files, and holds your model credentials outside the container. + +This page uses the [Container harness](/agents/harnesses/container/) with its `codex()` preset. For how the harness works, sessions, events, and recovery, refer to that page. + + + +## Install + + + +Add a SQLite-backed Durable Object and a container for the same class. The container must use the [`durable_object` scheduling policy](/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy), so the object can start it from snapshots: + + + +```jsonc +{ + "name": "codex-agent", + "main": "src/index.ts", + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [{ "name": "CodexAgent", "class_name": "CodexAgent" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["CodexAgent"] }], + "containers": [ + { "class_name": "CodexAgent", "scheduling_policy": "durable_object" }, + ], + "vars": { + "AI_GATEWAY_URL": "https://gateway.ai.cloudflare.com/v1//", + }, +} +``` + + + + + +## Create the agent + +Create the harness with the `codex()` preset, and add it to the [Lifecycle](/agents/runtime/lifecycle/). Export `ContainerEgress` from your Worker's main module. It adds your credentials to Codex's model requests after they leave the container: + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { routeAgentRequest } from "agents"; +import { ContainerHarness, codex } from "agents/harness/container"; +import { Lifecycle } from "agents/lifecycle"; + +export { ContainerEgress } from "agents/harness/container"; + +export class CodexAgent extends DurableObject { + readonly harness = new ContainerHarness({ + container: containerOf(this.ctx), + egress: this.ctx.exports.ContainerEgress, + agent: codex({ + baseUrl: `${this.env.AI_GATEWAY_URL}/openai`, + apiKey: this.env.AI_GATEWAY_TOKEN, + }), + }); + + readonly lifecycle = Lifecycle.install(this).use(this.harness); + + async onRequest(request: Request): Promise { + if (request.method !== "POST") { + return Response.json(await this.harness.messages()); + } + const { text } = await request.json<{ text: string }>(); + const result = await this.harness.prompt(text); + return Response.json({ status: result.status, text: result.text }); + } +} + +function containerOf(ctx: DurableObjectState): Container { + if (!ctx.container) { + throw new Error("Add a container for this class in wrangler.jsonc"); + } + return ctx.container; +} + +export default { + async fetch(request: Request, env: Env) { + return ( + (await routeAgentRequest(request, env)) ?? + new Response("Not found", { status: 404 }) + ); + }, +} satisfies ExportedHandler; +``` + + + + + +## Configure Codex + +`codex()` takes Codex's own options: + +| Option | Description | +| --------- | ------------------------------------------------------------ | +| `model` | The default model for new sessions, such as `gpt-5.5`. | +| `config` | Extra `-c key=value` configuration overrides. | +| `version` | The `@openai/codex` version to install. | +| `setup` | Commands that run once and are kept in the setup snapshot. | + +Use `setup` to add `~/.codex/config.toml`, `~/.codex/AGENTS.md`, or other files. A step with `user: "agent"` runs as the user Codex runs as, so files in its home work the way they do on your laptop: + + + +```ts +import { codex } from "agents/harness/container"; + +codex({ + baseUrl: `${this.env.AI_GATEWAY_URL}/openai`, + apiKey: this.env.AI_GATEWAY_TOKEN, + model: "gpt-5.5", + config: ["model_reasoning_effort=high"], + setup: [ + { + name: "add global instructions", + user: "agent", + command: [ + "sh", + "-c", + "mkdir -p ~/.codex && echo 'Run the tests before you report a change as done.' > ~/.codex/AGENTS.md", + ], + }, + ], +}); +``` + + + +Codex runs with `--dangerously-bypass-approvals-and-sandbox`, so it does not stop to ask before it runs a command. The container is the sandbox. + +## Next steps + + + + diff --git a/src/content/docs/agents/harnesses/container/api.mdx b/src/content/docs/agents/harnesses/container/api.mdx new file mode 100644 index 00000000000..9a7323cec9b --- /dev/null +++ b/src/content/docs/agents/harnesses/container/api.mdx @@ -0,0 +1,320 @@ +--- +title: API reference +pcx_content_type: reference +description: Options, methods, presets, events, and types for ContainerHarness in agents/harness/container. +sidebar: + order: 1 +products: + - agents +--- + +import { TypeScriptExample } from "~/components"; + +This page lists the API of `agents/harness/container`. For setup and concepts, refer to [Container harness](/agents/harnesses/container/). + +:::caution[Experimental] +`agents/harness/container` is experimental. Its API may change in any release. +::: + +The package has three entry points: + +| Entry point | Use it for | +| ---------------------------------- | -------------------------------------------------------------------------------------------- | +| `agents/harness/container` | `ContainerHarness`, presets, `ContainerEgress`, and types. Import it in your Worker. | +| `agents/harness/container/runtime` | The daemon's Node.js runtime and `cliAdapter()`, for a custom image that brings its own CLI. | +| `agents/harness/container/daemon` | The daemon core without a runtime, for adapters that do not run on Node.js. | + +## `ContainerHarness` + +`ContainerHarness` is a [Lifecycle capability](/agents/runtime/lifecycle/). Create one per Durable Object, and install it with `Lifecycle.install(this).use(harness)`. + + + +```ts +import { ContainerHarness, claudeCode } from "agents/harness/container"; + +const harness = new ContainerHarness({ + container: this.ctx.container, + egress: this.ctx.exports.ContainerEgress, + agent: claudeCode({ apiKey: this.env.ANTHROPIC_API_KEY }), +}); +``` + + + +### Options + +`new ContainerHarness(options)` takes these options: + +| Option | Type | Default | Description | +| ----------------- | -------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `container` | `Container` | — | Required. The object's container, `ctx.container`. | +| `agent` | `ContainerAgent` | — | Required. What runs in the container. Build it with [`claudeCode()`](#claudecode), [`codex()`](#codex), or [`containerAgent()`](#containeragent). | +| `egress` | `ContainerEgressBinding` | — | `ctx.exports.ContainerEgress`. Required when the agent has egress routes, which every preset does. The constructor throws without it. | +| `instance` | `ContainerInstance` | `"standard-1"` | The instance size: `"lite"`, `"standard-1"` to `"standard-4"`, or `{ vcpu, memoryMib, diskMb }`. | +| `enableInternet` | `boolean` | `true` | Whether the container can reach hosts other than its egress routes. Managed images need it to install the CLI. | +| `idleTimeoutMs` | `number` | `300000` | How long the container runs after its last work. The harness then snapshots and stops it. The maximum is six hours less two minutes. | +| `startTimeoutMs` | `number` | `60000` | How long a new container has to answer before the start counts as failed. | +| `defaults` | `ContainerSessionDefaults` | The agent's values | What a new session starts with: `model`, the CLI's model name, and `options`, passed to the adapter. | +| `onContainerLost` | `"fail"` or `"retry"` | `"fail"` | What happens to a running operation when its container stops. `"fail"` ends it with `container_lost`. `"retry"` runs it again from the start in a new container. | +| `port` | `number` | `8080` | The port the daemon listens on in the container. | +| `storePrefix` | `string` | `"container_"` | The table prefix for the session store in the object's SQLite database. | + +The constructor throws when `idleTimeoutMs` is out of range, when an egress route's upstream is not an `https:` URL, or when the agent has egress routes and `egress` is missing. + +### Methods + +Methods that take a `session` option use the root session when you leave it out. + +| Method | Returns | Description | +| ----------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `prompt(input, options?)` | `Promise` | Submit a prompt and wait for it to settle. Returns the result and the session's transcript. | +| `submit(input, options?)` | `Promise` | Durably submit a prompt. Resolves once it is stored, before the CLI runs. | +| `wait(operationId, options?)` | `Promise` | Wait for an operation to settle. Options: `session`, and `signal`, which stops only the wait. | +| `abort(options?)` | `Promise` | Abort one operation with `operationId`, or every open operation in the session without it. | +| `messages(options?)` | `Promise` | The session's transcript since its latest reset. | +| `pending(options?)` | `Promise` | Queued and running operations, oldest first. Without `session`, lists every session. | +| `session(id?)` | `ContainerSession` | A handle to an existing session. Does no I/O. | +| `container()` | `Promise<{ running, runtimeId }>` | Whether the container runs, and the ID the harness started it with. | +| `stop()` | `Promise` | Snapshot and stop the container now. Sessions survive. Running operations are reconciled as if the container had died. | + +`harness.sessions` is a [`ContainerSessions`](#containersessions) object for every session in the Durable Object. + +### Submit options + +`prompt()` and `submit()` take these options: + +| Option | Type | Description | +| ------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `session` | `string` | The session. Default: `"root"`. | +| `operationId` | `string` | An idempotency key. Submitting the same ID twice in one session returns the first receipt, with `accepted: false`. Default: a random UUID. | +| `whenBusy` | `"followUp"` or `"steer"` | What a submission does while the session runs. `"followUp"`, the default, runs after the current run. `"steer"` joins the running turn when the adapter supports it, and otherwise queues as a follow-up. | + +The Claude Code and Codex adapters do not support steering. + +## `ContainerSession` + +A handle to one session. Get one from `harness.session(id)`, `harness.sessions.get(id)`, `sessions.create()`, or `sessions.fork()`. + +| Member | Returns | Description | +| ---------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | `string` | The session ID. | +| `submit(input, options?)` | `Promise` | Durably submit a prompt. | +| `prompt(input, options?)` | `Promise` | Submit, wait, and return the result and transcript. | +| `steer(input, options?)` | `Promise` | Submit with `whenBusy: "steer"`. | +| `wait(operationId, signal?)` | `Promise` | Wait for an operation. Aborting `signal` stops only the wait. | +| `abort(operationId?)` | `Promise` | Abort one operation, or all of the session's open operations. Returns `false` when the given operation was not open. | +| `reset(handoff?)` | `Promise` | Start a new CLI session without the old history. `handoff` is added to the start of the next prompt. Throws while the session is busy. | +| `setModel(model)` | `Promise` | Change the session's model, by the CLI's model name. | +| `messages()` | `Promise` | The transcript since the latest reset. | +| `events()` | `Promise` | A snapshot of the session, then live events. Refer to [Events](#events). | +| `busy()` | `Promise` | Whether the session has queued or running operations. | + +Every method except `id` throws for a session that was never created. + +## `ContainerSessions` + +Every session in the Durable Object, from `harness.sessions`. + +| Method | Returns | Description | +| ------------ | --------------------------------- | --------------------------------------------------------------------------------------------------- | +| `get(id)` | `ContainerSession` | A handle to a session. | +| `create()` | `Promise` | A new top-level session with the harness defaults, and a random UUID as its ID. | +| `fork(from)` | `Promise` | A new session that continues `from`'s transcript and CLI session on a branch of its own. | +| `list()` | `Promise` | Every session, oldest first, with its `id`, its `parent` if it is a fork, and whether it is `busy`. | + +The root session's ID is exported as `ROOT_SESSION`. + +## Presets + +A preset returns a `ContainerAgent`: the adapter the daemon runs, how the container is built, the environment the CLI sees, and the egress routes that carry credentials. Pass it to the harness as `agent`. + +Every preset that calls a model provider takes these connection options: + +| Option | Type | Description | +| --------- | ------------------------ | -------------------------------------------------------------------------------------------------------------- | +| `apiKey` | `string` | Sent the way the provider expects: `x-api-key` for Anthropic, `Authorization: Bearer` for OpenAI. | +| `baseUrl` | `string` | The upstream for model requests, such as an AI Gateway URL. Must be `https:`. Default: the provider's own API. | +| `headers` | `Record` | More headers on every model request, such as `cf-aig-authorization` or gateway metadata. | + +These values stay in the Durable Object. `ContainerEgress` adds them to each request after it leaves the container. + +### `claudeCode()` + +Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) from npm on the managed image. + +| Option | Type | Default | Description | +| -------------------- | ------------- | --------------------- | --------------------------------------------------------------------------------- | +| `model` | `string` | `"claude-sonnet-4-5"` | The default model for new sessions. | +| `version` | `string` | A pinned release | The `@anthropic-ai/claude-code` version to install. | +| `appendSystemPrompt` | `string` | — | Passed to `--append-system-prompt`. | +| `allowedTools` | `string[]` | — | Passed to `--allowedTools`. | +| `disallowedTools` | `string[]` | — | Passed to `--disallowedTools`. | +| `maxTurns` | `number` | — | Passed to `--max-turns`. | +| `setup` | `SetupStep[]` | — | More [setup steps](#setup-steps), run after the install and kept in the snapshot. | + +### `codex()` + +Installs [Codex](https://developers.openai.com/codex/cli) from npm on the managed image. + +| Option | Type | Default | Description | +| --------- | ------------- | ---------------- | --------------------------------------------------------------------------------- | +| `model` | `string` | `"gpt-5.1"` | The default model for new sessions. | +| `version` | `string` | A pinned release | The `@openai/codex` version to install. | +| `config` | `string[]` | — | Extra `-c key=value` configuration overrides. | +| `setup` | `SetupStep[]` | — | More [setup steps](#setup-steps), run after the install and kept in the snapshot. | + +### `containerAgent()` + +Runs an image you built, whose entrypoint serves the daemon with [`serveFromEnv()`](#runtime). + +| Option | Type | Description | +| --------- | ------------------------ | ------------------------------------------------------------------------------------------------ | +| `image` | `string` | Required. The image, such as `ctx.container.images.agent`. | +| `adapter` | `string` | Required. The ID of the adapter the daemon runs. | +| `env` | `Record` | Environment for the daemon and the CLI. Do not put secrets here. | +| `egress` | `EgressRoute[]` | Hosts to intercept and forward with credentials. Refer to [`ContainerEgress`](#containeregress). | +| `model` | `string` | The default model for new sessions. | +| `options` | JSON value | Adapter options for new sessions. | + +### `echoAgent()` + +Runs an adapter that echoes each prompt, with no CLI, model, or credentials. Use it to test the path from the Durable Object to the container. + +### Setup steps + +A `SetupStep` is one command, run once in a fresh managed container and kept in its snapshot: + +| Field | Type | Description | +| --------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | What the step does, for error messages. | +| `command` | `string[]` | The command and its arguments. | +| `user` | `"root"` or `"agent"` | Who runs it. Default `"root"`. `"agent"` runs it as the CLI's user with `HOME=/home/agent`, which every session home copies. | + +The setup snapshot is keyed by the image, the steps, and the daemon build. Changing any of them sets up a fresh container on the next start. + +## `ContainerEgress` + +A `WorkerEntrypoint` that forwards the container's model requests with real credentials. Export it from your Worker's main module, and pass `ctx.exports.ContainerEgress` to the harness: + +```ts +export { ContainerEgress } from "agents/harness/container"; +``` + +For each intercepted request, `ContainerEgress` finds the route for the request's host, removes the headers in `strip`, sets the headers in `headers`, and forwards the request to `upstream` with the original path and query. It does not follow redirects. A host without a route gets a `403` response. + +An `EgressRoute` has these fields: + +| Field | Type | Description | +| ---------- | ------------------------ | ----------------------------------------------------------------------------- | +| `host` | `string` | The placeholder hostname the CLI calls, such as `anthropic.harness.internal`. | +| `upstream` | `string` | The real base URL. Must be `https:`. | +| `headers` | `Record` | Headers to set on the forwarded request, such as credentials. | +| `strip` | `string[]` | Headers to remove from the CLI's request, such as its placeholder key. | + +## Results + +### `ContainerReceipt` + +Returned by `submit()` once the prompt is stored: + +| Field | Type | Description | +| ------------- | --------- | ----------------------------------------------------- | +| `operationId` | `string` | The operation ID. | +| `session` | `string` | The session ID. | +| `accepted` | `boolean` | `false` when this operation ID was already submitted. | + +### `ContainerOperationResult` + +Returned by `wait()`. `prompt()` returns the same fields plus `messages`, the session's transcript after the operation. + +| Field | Type | Description | +| ------------- | -------------------------- | -------------------------------------- | +| `operationId` | `string` | The operation ID. | +| `session` | `string` | The session ID. | +| `status` | `"done"` or `"unanswered"` | Whether the CLI answered. | +| `text` | `string` | The final assistant text, when `done`. | +| `reason` | `string` | Why an operation ended `unanswered`. | + +An unanswered operation has one of these reasons, or a reason from the adapter, such as `exit 1`: + +| Reason | Meaning | +| ----------------------- | ---------------------------------------------------------------------- | +| `aborted` | `abort()` stopped it. | +| `container_lost` | Its container stopped while it ran, and `onContainerLost` is `"fail"`. | +| `container_unavailable` | No container could be started after five attempts. | +| `not_found` | No operation with this ID exists in the session. | + +### `ContainerMessage` + +A transcript message. Every adapter produces the same format: + +| Field | Type | Description | +| ------------- | ------------------------- | ----------------------------------------------------- | +| `id` | `string` | The message ID. An updated message keeps its ID. | +| `role` | `"user"` or `"assistant"` | Who wrote it. | +| `parts` | `ContainerPart[]` | The content, in order. | +| `operationId` | `string` | The operation the message belongs to. | +| `createdAt` | `number` | When it was created, in milliseconds since the epoch. | + +A part is one of these types: + +| Type | Fields | +| ------------- | ------------------------------------------------------------------------------------------ | +| `"text"` | `text` | +| `"reasoning"` | `text` | +| `"image"` | `mediaType`, `data` | +| `"tool"` | `toolCallId`, `toolName`, `input`, `state` (`"running"`, `"done"`, or `"error"`), `output` | + +## Events + +`session.events()` returns a `ContainerEventStream`: + +| Member | Description | +| ----------------- | ------------------------------------------------------------------------------------------------ | +| `snapshot` | The session when the stream opened: `session`, `messages`, `pending`, and `busy`. | +| `start(listener)` | Start delivering events in batches, including any that arrived after the snapshot. Call it once. | +| `stop()` | Stop delivering events. Call it when you are done, whether or not you called `start()`. | + +Each event has a `type`: + +| Type | Fields | Description | +| ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | +| `"text-delta"` | `messageId`, `delta`, `operationId` | Live text for an assistant message that is still being written. | +| `"reasoning-delta"` | `messageId`, `delta`, `operationId` | Live reasoning for an assistant message. | +| `"message"` | `message`, `operationId` | A finished or updated transcript message. Stored in the transcript. | +| `"usage"` | `inputTokens`, `outputTokens`, `costUsd`, `operationId` | Token and cost figures, as the CLI reports them. | +| `"log"` | `level`, `message`, `operationId` | A log line from the adapter. | +| `"raw"` | `value`, `operationId` | An adapter event with no mapping. | +| `"operation-start"` | `operationId` | An operation started running in the container. | +| `"operation-end"` | `result` | An operation settled, with the result `wait()` returns. | +| `"container"` | `status` | The container is `"starting"`, `"ready"`, `"lost"`, or `"stopped"`. | + +An event can repeat what the snapshot already shows. Apply messages by ID. Events reach listeners only in the isolate that runs the harness, so serve them to clients from the Durable Object. + +## Runtime + +`agents/harness/container/runtime` runs the daemon in a custom image. It is Node.js only. + +| Export | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------ | +| `serveFromEnv(adapter)` | Serve the daemon with the token, runtime ID, and port the harness passes in the container environment. | +| `serve(options)` | Serve the daemon with explicit options. | +| `cliAdapter(options)` | An adapter for a CLI that runs one turn per process and prints JSON lines. | +| `claudeCodeCli()` | The Claude Code adapter the preset uses. | +| `codexCli()` | The Codex adapter the preset uses. | + +`cliAdapter()` takes these options: + +| Option | Type | Description | +| ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `id` | `string` | Required. The adapter ID, matched by `containerAgent({ adapter })`. | +| `command` | `(turn) => CliCommand` | Required. The `argv`, optional `stdin`, and optional `env` for one turn. | +| `parser` | `(turn) => CliParser` | Required. Turns each stdout line into `events`, `state`, and an `outcome`, and gives the outcome when the process exits without one. | +| `stateDirs` | `string[]` | Directories under `$HOME` where the CLI keeps its sessions. Persisted to the Durable Object and restored in a new container. | +| `cwd` | `string` | The working directory. Default `/workspace`. | +| `skeleton` | `string` | A home directory that every session home starts as a copy of. | +| `user` | `{ uid, gid }` | Run the CLI as this user. Default: the daemon's user. | +| `syncIntervalMs` | `number` | How often a running turn's state is persisted. Default five seconds. | + +A `CliTurn` gives `command` and `parser` the `operationId`, the prompt text as `prompt`, the session's `settings`, and the `state` values earlier turns reported, such as the CLI's own session ID. diff --git a/src/content/docs/agents/harnesses/container/index.mdx b/src/content/docs/agents/harnesses/container/index.mdx new file mode 100644 index 00000000000..52963d355f5 --- /dev/null +++ b/src/content/docs/agents/harnesses/container/index.mdx @@ -0,0 +1,440 @@ +--- +title: Container harness +pcx_content_type: overview +description: Run Claude Code or Codex in a Cloudflare Container, driven from a Durable Object that keeps the session, the credentials, and the record of every run. +sidebar: + order: 14 + badge: + text: Experimental + group: + hideIndex: false +products: + - agents +--- + +import { + CopyPromptButton, + LinkCard, + PackageManagers, + TypeScriptExample, + WranglerConfig, +} from "~/components"; + +`ContainerHarness` runs an agent command-line interface (CLI), such as Claude Code or Codex, in a [Cloudflare Container](/containers/), and drives it from a [Durable Object](/durable-objects/). The CLI runs its own loop and tools in the container. The Durable Object keeps the session, holds the credentials, and decides when the container runs. + +To get started with a specific CLI, refer to [Claude Code](/agents/harnesses/claude-code/) or [OpenAI Codex](/agents/harnesses/codex/). This page covers how the harness works for every CLI. + +:::caution[Experimental] +`agents/harness/container` is experimental. Its API may change in any release. +::: + + + +The [Container harness example](https://github.com/cloudflare/agents/tree/main/examples/next/harnesses/container) runs Claude Code and Codex side by side, with routes to prompt, stream events, and stop the container. + +## When to use it + +Use the Container harness when you want a specific CLI agent with its own tools, plugins, and settings, and you want it to behave the way it does on your laptop. Most CLI agents are binaries that need a Linux shell, so they cannot run in a Durable Object. + +If you control the agent loop, use a harness that runs in the Durable Object, such as [Pi](/agents/harnesses/pi/), and give it a [sandbox](/agents/tools/sandbox/) as a tool. For the trade-offs between the two, refer to [Harness](/agents/concepts/harness/#where-the-pieces-live). + +## How it works + +`ContainerHarness` is a [Lifecycle capability](/agents/runtime/lifecycle/). You install it on a plain Durable Object, and it uses the object's attached container through `ctx.container`. + +```mermaid +flowchart LR + client["Application or Worker"] -->|HTTP, WebSockets, or RPC| object + subgraph object["Durable Object"] + harness["ContainerHarness"] <--> sqlite[("SQLite: sessions, operations,
transcript, CLI session files")] + lifecycle["Lifecycle jobs"] --> harness + end + harness <-->|WebSocket| daemon + subgraph container["Container"] + daemon["Harness daemon"] --> cli["Claude Code or Codex"] + end + cli -->|placeholder key| egress["ContainerEgress
(your Worker)"] + egress -->|real credentials| upstream["AI Gateway or provider"] +``` + +- **The object keeps the record.** Sessions, queued and running prompts, the transcript, and the CLI's own session files are stored in the object's SQLite database. The table names start with `container_`. +- **The container is disposable.** The harness starts it on the first prompt and stops it five minutes after its last work. Before it stops, the harness snapshots the container's file system. The next prompt starts a new container from that snapshot, and the CLI resumes its own session. +- **There is no image to build.** On first use, the harness starts the `cloudflare/debian-trixie` managed image, installs the CLI and a small daemon, and snapshots the result. Later starts restore the snapshot. +- **Credentials stay outside.** The CLI calls a placeholder host with a placeholder key. `ContainerEgress`, a Worker entrypoint in your Worker, swaps in the real credentials and forwards the request. +- **Runs survive an eviction.** Each session with open work has one Lifecycle job. If the object is evicted mid-run, the job's alarm restarts the object, which reconnects to the container and replays the output it missed. + +Each prompt runs one CLI process as an unprivileged `agent` user, such as `claude -p --output-format stream-json` or `codex exec --json`. The daemon turns the CLI's output into transcript messages and events, and streams them to the object. + +## Install + +Install the Agents SDK: + + + +`ContainerHarness` is available in `agents` 0.27.0 and later. + +Add a SQLite-backed Durable Object and a container for the same class. The container must use the [`durable_object` scheduling policy](/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy), so the object can start it from snapshots: + + + +```jsonc +{ + "name": "claude-code-agent", + "main": "src/index.ts", + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [{ "name": "ClaudeCodeAgent", "class_name": "ClaudeCodeAgent" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ClaudeCodeAgent"] }], + "containers": [ + { "class_name": "ClaudeCodeAgent", "scheduling_policy": "durable_object" }, + ], + "vars": { + "AI_GATEWAY_URL": "https://gateway.ai.cloudflare.com/v1//", + }, +} +``` + + + +There is no `image` in the container configuration. The harness starts the managed image itself. + +Store your AI Gateway token as a secret: + +```sh +npx wrangler secret put AI_GATEWAY_TOKEN +``` + +To run containers locally, you need Docker and Wrangler 4.141.0 or later. For more information, refer to [Local development](/containers/guides/local-dev/). + +## Create the agent + +Create the harness with a preset, install a Lifecycle on the Durable Object, and add the harness to it. Export `ContainerEgress` from your Worker's main module, so `ctx.exports.ContainerEgress` exists: + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { routeAgentRequest } from "agents"; +import { ContainerHarness, claudeCode } from "agents/harness/container"; +import { Lifecycle } from "agents/lifecycle"; + +// Adds credentials to the CLI's model requests, outside the container. +export { ContainerEgress } from "agents/harness/container"; + +export class ClaudeCodeAgent extends DurableObject { + readonly harness = new ContainerHarness({ + container: containerOf(this.ctx), + egress: this.ctx.exports.ContainerEgress, + agent: claudeCode({ + baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`, + apiKey: this.env.AI_GATEWAY_TOKEN, + }), + }); + + readonly lifecycle = Lifecycle.install(this).use(this.harness); + + async onRequest(request: Request): Promise { + if (request.method !== "POST") { + return Response.json(await this.harness.messages()); + } + const { text } = await request.json<{ text: string }>(); + const result = await this.harness.prompt(text); + return Response.json({ status: result.status, text: result.text }); + } +} + +function containerOf(ctx: DurableObjectState): Container { + if (!ctx.container) { + throw new Error("Add a container for this class in wrangler.jsonc"); + } + return ctx.container; +} + +export default { + async fetch(request: Request, env: Env) { + return ( + (await routeAgentRequest(request, env)) ?? + new Response("Not found", { status: 404 }) + ); + }, +} satisfies ExportedHandler; +``` + + + +`routeAgentRequest()` sends `/agents/claude-code-agent/` to the object with that name. Each object has its own container, workspace, and sessions. In an application, choose the object name from an authenticated user or project. + +The first prompt to a new object takes longer, because the harness installs the CLI and takes the setup snapshot. Later containers start from a snapshot. + +:::caution +These routes have no authentication. Each prompt runs commands and spends model credit. Add your own authentication before you deploy anywhere public. +::: + +## Choose the CLI + +A preset says what runs in the container. Switching between [Claude Code](/agents/harnesses/claude-code/) and [OpenAI Codex](/agents/harnesses/codex/) changes only the preset: + + + +```ts +import { claudeCode, codex } from "agents/harness/container"; + +claudeCode({ + baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`, + apiKey: this.env.AI_GATEWAY_TOKEN, + model: "claude-opus-5-5", +}); + +codex({ + baseUrl: `${this.env.AI_GATEWAY_URL}/openai`, + apiKey: this.env.AI_GATEWAY_TOKEN, + model: "gpt-5.5", +}); +``` + + + +Each preset pins a CLI version. Pass `version` to choose another one. For every option, refer to the [API reference](/agents/harnesses/container/api/#presets). + +## Connect to a model provider + +Every preset takes the same connection options: `apiKey`, `baseUrl`, and `headers`. They describe where the CLI's model requests go, and which credentials they carry: + +| Connection | Options | +| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Straight to the provider | `apiKey` set to the provider key. Leave out `baseUrl`. | +| [AI Gateway with Unified Billing](/ai-gateway/features/unified-billing/) | `baseUrl` set to your gateway URL plus `/anthropic` or `/openai`. `apiKey` set to the gateway token. | +| [AI Gateway with your own provider keys](/ai-gateway/configuration/bring-your-own-keys/) | `baseUrl` set to the gateway URL. `headers` set to `{ "cf-aig-authorization": "Bearer " }`. | + +With Unified Billing, the gateway token is the only credential, and it never leaves the Durable Object. + +## Keep credentials out of the container + +The CLI never sees a real credential. The harness points it at a placeholder host, such as `http://anthropic.harness.internal`, with a placeholder key. `env` in the container shows only the placeholder. + +When the container starts, the harness intercepts each placeholder host with [`interceptOutboundHttp()`](/containers/configuration/outbound-traffic/). `ContainerEgress` receives each intercepted request in your Worker. It removes the placeholder credentials, adds the real ones, and forwards the request over HTTPS to `baseUrl`. It does not follow redirects, so credentials are never sent to another host. + +The container can still reach the Internet by default, because the managed image installs the CLI with `npm` during setup. Commands the agent runs can reach any host, but no host receives your model credentials. To allow only the placeholder hosts, set `enableInternet: false` and use a [custom image](#run-another-cli) that already has the CLI installed. + +## Customize the CLI + +Add `setup` steps to a preset to install plugins, settings, or a different build of the CLI. The steps run once, on the first start, and their result is kept in the setup snapshot. + +A step runs as `root` by default. A step with `user: "agent"` runs as the user the CLI runs as, in its home. Every session's home starts as a copy of that home, so files such as `CLAUDE.md`, `~/.claude/settings.json`, skills, and `~/.codex/config.toml` work the way they do on your laptop: + + + +```ts +claudeCode({ + baseUrl: `${this.env.AI_GATEWAY_URL}/anthropic`, + apiKey: this.env.AI_GATEWAY_TOKEN, + appendSystemPrompt: "Run the tests before you report a change as done.", + setup: [ + { + name: "install review plugin", + user: "agent", + command: [ + "sh", + "-c", + "claude plugin marketplace add my-org/plugins && claude plugin install review@plugins", + ], + }, + ], +}); +``` + + + +Changing `setup`, the CLI version, or the Agents SDK version takes effect when a container next starts. A container that runs when you deploy keeps its old setup until it stops. The next container sets up from scratch, and starts without the previous workspace snapshot. + +:::caution +A token that a setup step uses, for example to clone a private repository, stays in the snapshot. The agent can read it. Prefer public sources or short-lived URLs. +::: + +## Run another CLI + +To run a CLI that has no preset, build an image whose entrypoint serves the harness daemon, and pass it to `containerAgent()`. Describe the CLI with `cliAdapter()` from `agents/harness/container/runtime`: + + + +```ts +import { cliAdapter, serveFromEnv } from "agents/harness/container/runtime"; + +await serveFromEnv( + cliAdapter({ + id: "my-agent", + // Where the CLI keeps its sessions, relative to $HOME. + stateDirs: [".my-agent/sessions"], + command: (turn) => ({ + argv: [ + "my-agent", + "--json", + ...(turn.state.id ? ["--resume", turn.state.id] : []), + ], + stdin: turn.prompt, + }), + parser: () => ({ + // Turn one line of output into events, state, and an outcome. + line: (text) => parseMyAgentLine(JSON.parse(text)), + end: ({ code }) => ({ status: "unanswered", reason: `exit ${code}` }), + }), + }), +); +``` + + + +Declare the image in Wrangler under `images`, and select it with `ctx.container.images`: + + + +```ts +import { containerAgent } from "agents/harness/container"; + +containerAgent({ + image: this.ctx.container.images.agent, + adapter: "my-agent", +}); +``` + + + +The adapter persists everything the CLI writes under `stateDirs`, and every `state` value the parser reports, to the Durable Object while a turn runs. A new container restores both before its first turn. + +## Send prompts + +`harness.prompt()` submits to the root session and waits for the answer. It returns `status`, the final `text` or a `reason`, and the session's transcript as `messages`. `status` is `"done"` when the CLI answered, and `"unanswered"` when the run failed, was aborted, or was lost with its container. + +Coding tasks often take minutes. To return before the CLI finishes, submit and wait separately: + + + +```ts +const receipt = await this.harness.submit("Add a test for parse()", { + operationId: "issue-42", +}); + +// In a later request to the same Durable Object: +const result = await this.harness.wait(receipt.operationId, { + session: receipt.session, +}); +``` + + + +`submit()` resolves once the prompt is stored, before the CLI starts. An `operationId` makes submissions idempotent within a session: reusing one returns the first receipt with `accepted: false`. + +A prompt submitted while the session runs waits for the current run, then runs as its own turn. Claude Code and Codex run one process per turn, so they cannot join a running turn. `whenBusy: "steer"` and `session.steer()` queue the prompt as a follow-up for these CLIs. + +To stop work, call `harness.abort()`. Aborted operations end `unanswered` with the reason `aborted`. + +## Work with sessions + +The harness creates a root session with the ID `"root"`. Each session is a separate CLI session with its own transcript. Create and fork sessions through `harness.sessions`: + + + +```ts +const session = await this.harness.sessions.create(); +await session.prompt("Draft a migration plan"); + +// Continue the same history on a branch. +const fork = await this.harness.sessions.fork(session.id); +await fork.prompt("Now try it without a schema change"); + +const all = await this.harness.sessions.list(); +``` + + + +`harness.session(id)` returns a handle to a session that already exists. Prompting a session that was never created throws. + +Each session has its own `$HOME`, so sessions never see each other's CLI state. All sessions in one object share the `/workspace` working directory and the same container. + +`session.reset(handoff)` starts a new CLI session without the old history. The optional `handoff` text is added to the start of the next prompt. `session.setModel(model)` changes the model for the next turn. + +## Stream events to clients + +`session.events()` returns a stream. Its `snapshot` holds the transcript, pending operations, and whether the session is busy. Call `start()` to receive every later event, in batches: + + + +```ts +const stream = await this.harness.session().events(); +socket.send(JSON.stringify({ type: "snapshot", ...stream.snapshot })); + +stream.start((events) => { + for (const event of events) socket.send(JSON.stringify(event)); +}); + +// When the client disconnects: +await stream.stop(); +``` + + + +Events include live `text-delta` and `reasoning-delta` output, finished `message` entries, `usage` figures, `operation-start` and `operation-end`, and `container` status changes. An event can repeat what the snapshot already shows, so key messages by ID. Events reach listeners only in the Durable Object that runs the harness. For the full list, refer to [Events](/agents/harnesses/container/api/#events). + +The [example](https://github.com/cloudflare/agents/tree/main/examples/next/harnesses/container) serves the same stream over Server-Sent Events. + +## Container lifecycle + +The harness starts the container on demand, and stops it after `idleTimeoutMs`, five minutes by default. Before it stops the container, it snapshots the whole file system, including `/workspace`. The next prompt starts a new container from that snapshot. + +To stop the container now, call `harness.stop()`. Sessions survive, and the next prompt resumes them in a new container. Snapshots expire 30 days after they were last used. Without a usable snapshot, the harness sets up a fresh container. + +`harness.container()` reports whether the container is running. + +## Recovery + +The harness recovers differently depending on what was lost: + +| What was lost | What happens | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| The Durable Object, after an eviction or deploy | The container keeps running. The Lifecycle job restarts the object, which reconnects and replays the output it missed. | +| The WebSocket to the container | The harness reconnects on the next heartbeat and replays the missed output. | +| The container, after a crash or platform restart | A new container starts from the last snapshot. Queued prompts run there. A prompt that was running ends `unanswered` with the reason `container_lost`, and the CLI resumes its session on the next prompt. | + +A container that dies mid-run loses workspace edits made since the last snapshot. The CLI's session files are saved as each turn runs, so the conversation is not lost. To run an interrupted prompt again from the start, set `onContainerLost: "retry"`. Only do this when the agent's work is safe to repeat. + +If a container fails to start five times in a row, the session's open prompts end `unanswered` with the reason `container_unavailable`. The next prompt tries again. + +## Security + +The container is the sandbox. Claude Code runs with `--dangerously-skip-permissions`, and Codex runs with `--dangerously-bypass-approvals-and-sandbox`, so neither CLI stops to ask before it runs a command. Treat everything in the container as readable by the agent, and keep credentials out of it. + +- Put authentication in front of every route that reaches the harness. +- Use `allowedTools` and `disallowedTools` on `claudeCode()` to limit Claude Code's tools. +- Set `enableInternet: false` with a custom image to stop commands reaching the Internet. + +## Limits + +- One container runs per Durable Object, shared by every session in that object. +- Image parts in a prompt are not passed to the CLI. Only text reaches it. +- Events reach listeners only in the Durable Object that runs the harness. +- For instance sizes and container limits, refer to [Containers limits](/containers/platform/limits/). + +## Related resources + + + + + + diff --git a/src/content/docs/agents/harnesses/index.mdx b/src/content/docs/agents/harnesses/index.mdx index 57f72e1a164..78de54b3720 100644 --- a/src/content/docs/agents/harnesses/index.mdx +++ b/src/content/docs/agents/harnesses/index.mdx @@ -12,7 +12,7 @@ A harness is the loop that makes an agent behave like an agent instead of a sing It is responsible for the turn-by-turn work around the model: building the prompt, loading memory, selecting tools, handling tool results, streaming responses, persisting messages, and deciding whether the agent should continue or stop. -To build a new agent, start with the [Pi harness](/agents/harnesses/pi/), which runs Pi Durable in a Durable Object. You can also run [OpenCode](/agents/harnesses/opencode/), or build the loop yourself on top of the [Agents SDK runtime](/agents/runtime/). +To build a new agent, start with the [Pi harness](/agents/harnesses/pi/), which runs Pi Durable in a Durable Object. You can also run [OpenCode](/agents/harnesses/opencode/), run [Claude Code](/agents/harnesses/claude-code/) or [OpenAI Codex](/agents/harnesses/codex/) with the [Container harness](/agents/harnesses/container/), or build the loop yourself on top of the [Agents SDK runtime](/agents/runtime/). ## How harnesses fit @@ -23,7 +23,7 @@ Harnesses sit on top of the Agents SDK runtime: The runtime answers “where does this agent live and how does it stay durable?” The harness answers “what does this agent do on each turn?” -Conversation storage depends on the harness. Pi and OpenCode manage their own transcripts. Think and `AIChatAgent` install the SDK's `Sessions` capability. To find the APIs relevant to your agent, refer to [Choose capabilities](/agents/runtime/capabilities/#choose-capabilities). +Conversation storage depends on the harness. Pi and OpenCode manage their own transcripts. The Container harness stores the CLI's transcript and session files in the Durable Object. Think and `AIChatAgent` install the SDK's `Sessions` capability. To find the APIs relevant to your agent, refer to [Choose capabilities](/agents/runtime/capabilities/#choose-capabilities). ## Choose an approach @@ -33,6 +33,8 @@ Use a build-your-own harness when you need full control over the model call, mes Use OpenCode (experimental) when you want OpenCode 2.0's sessions, tools, and plugin system. `OpenCodeHarness` runs [OpenCode's Workerd SDK](https://opencode.ai/v2/docs/build/sdk/cloudflare/) inside a plain Durable Object. +Use the Container harness (experimental) when you want a CLI agent, such as Claude Code or Codex, with its own tools and plugins. The CLI runs in a [Container](/containers/). `ContainerHarness` drives it from a Durable Object, which keeps the session and the credentials. For how this differs from a harness that runs in the Durable Object, refer to [Harness](/agents/concepts/harness/#where-the-pieces-live). + ## Current harnesses + + + + + + ## What a harness usually owns A harness usually owns: diff --git a/src/content/docs/agents/tools/sandbox.mdx b/src/content/docs/agents/tools/sandbox.mdx index 95cd36a48e8..1c797886b31 100644 --- a/src/content/docs/agents/tools/sandbox.mdx +++ b/src/content/docs/agents/tools/sandbox.mdx @@ -23,6 +23,8 @@ Use a sandbox when an agent needs to: If the code that the model writes only calls methods that you define, such as methods that read or update your data, use [Code Mode](/agents/tools/codemode/) instead. Code Mode runs the code in a Dynamic Worker, which starts faster than a container. For more information, refer to [Choose a sandbox environment](/sandbox/concepts/). ::: +This page keeps the agent loop in the Durable Object and uses the container as a tool. To run a CLI agent such as Claude Code or Codex inside the container instead, use the [Container harness](/agents/harnesses/container/). For the trade-offs, refer to [Harness](/agents/concepts/harness/#where-the-pieces-live). + ## How it works Attaching a container to the [Durable Object](/durable-objects/) of an agent gives the agent a sandbox through `this.ctx.container`. Agents with different names get separate sandboxes, and every turn sent to the same agent reaches the same sandbox. diff --git a/src/content/partials/agents/container-harness-run.mdx b/src/content/partials/agents/container-harness-run.mdx new file mode 100644 index 00000000000..da80712c899 --- /dev/null +++ b/src/content/partials/agents/container-harness-run.mdx @@ -0,0 +1,11 @@ +--- +{} +--- + +`routeAgentRequest()` sends `/agents//` to the Durable Object with that name. Each object has its own container, workspace, and sessions. In an application, choose the object name from an authenticated user or project. + +The first prompt to a new object takes longer, because the harness installs the CLI and takes the setup snapshot. Later containers start from a snapshot, and the container stops five minutes after its last work. + +:::caution +These routes have no authentication. Each prompt runs commands and spends model credit. Add [authentication](/agents/runtime/routing/authentication/) before you deploy anywhere public. +::: diff --git a/src/content/partials/agents/container-harness-setup.mdx b/src/content/partials/agents/container-harness-setup.mdx new file mode 100644 index 00000000000..d002bbe6474 --- /dev/null +++ b/src/content/partials/agents/container-harness-setup.mdx @@ -0,0 +1,13 @@ +--- +{} +--- + +There is no `image` in the container configuration. The harness starts the `cloudflare/debian-trixie` managed image, installs the CLI on first use, and snapshots the result. + +Store your AI Gateway token as a secret: + +```sh +npx wrangler secret put AI_GATEWAY_TOKEN +``` + +To run containers locally, you need Docker and Wrangler 4.141.0 or later. For more information, refer to [Local development](/containers/guides/local-dev/).