Skip to content

Run Codex in a sandbox

Last updated View as MarkdownAgent setup

Run the Codex CLI ↗︎ in a Linux sandbox to edit a GitHub repository from a prompt, then read its changes as a diff. Codex calls OpenAI models through AI Gateway. Your Worker adds the gateway token to those requests, and the sandbox can reach only your gateway and github.com.

Prerequisites

  • The sandbox-coding-agent project from Build a coding agent runner. This page replaces the image, the model, and the parts of the Durable Object that are specific to the agent.
  • OpenAI credentials in AI Gateway, either Unified Billing credits or an OpenAI API key stored as a provider key.

Switch the Worker to Codex

  1. Replace the Dockerfile. Only the highlighted line differs from the Dockerfile in the tutorial:

    Dockerfiledockerfile
    FROM node:24-trixie-slim
    
    RUN apt-get update \
    	&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
    	&& rm -rf /var/lib/apt/lists/*
    
    RUN npm install --global --ignore-scripts @openai/codex@0.156.1
    
    COPY --from=docker.io/cloudflare/sandbox:1.0.0 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim
    
    WORKDIR /workspace
    CMD ["sleep", "infinity"]

    The Codex npm package ships its native binaries as dependencies, so it installs without running install scripts. The image pins the version, because the flags and event format of Codex can change between releases.

  2. In wrangler.jsonc, set MODEL to an OpenAI model ID that your gateway can serve:

    {
    	"vars": {
    		"AI_GATEWAY_ACCOUNT_ID": "<ACCOUNT_ID>",
    		"AI_GATEWAY_ID": "default",
    		"MODEL": "gpt-6-sol",
    	},
    }
    [vars]
    AI_GATEWAY_ACCOUNT_ID = "<ACCOUNT_ID>"
    AI_GATEWAY_ID = "default"
    MODEL = "gpt-6-sol"

    Codex sends requests in the OpenAI Responses format, so it works only with OpenAI models that support the Responses API.

  3. In src/sandbox.ts, replace the ResultEvent constant with a path for the final message from Codex and a schema for its failure events:

    src/sandbox.tsts
    const lastMessagePath = `${taskDirectory}/last-message.txt`;
    
    const FailureEvent = z.union([
    	z.object({
    		type: z.literal("turn.failed"),
    		error: z.object({ message: z.string() }),
    	}),
    	z.object({ type: z.literal("error"), message: z.string() }),
    ]);
  4. Replace the agentCommand() and agentEnv() methods:

    src/sandbox.tsts
    export class AgentSandbox extends DurableObject<Env> {
    	// ...
    
    	private agentCommand(prompt: string): string[] {
    		const baseUrl = `https://gateway.ai.cloudflare.com/v1/${this.env.AI_GATEWAY_ACCOUNT_ID}/${this.env.AI_GATEWAY_ID}/openai`;
    
    		return [
    			"codex",
    			// Run one task without an interactive terminal
    			"exec",
    			// Write one JSON event per line to the events file
    			"--json",
    			// Skip the session files that Codex would otherwise save
    			"--ephemeral",
    			"--dangerously-bypass-approvals-and-sandbox",
    			// Write the final message to a file outside the repository
    			"--output-last-message",
    			lastMessagePath,
    			"--model",
    			this.env.MODEL,
    			// Add a custom provider that sends requests to the OpenAI endpoint of your gateway
    			"--config",
    			'model_provider="cloudflare-ai-gateway"',
    			"--config",
    			`model_providers.cloudflare-ai-gateway={ name = "Cloudflare AI Gateway", base_url = ${JSON.stringify(baseUrl)}, wire_api = "responses" }`,
    			// Stop the analytics and update requests from Codex
    			"--config",
    			"analytics.enabled=false",
    			"--config",
    			"check_for_update_on_startup=false",
    			// Stop Codex from syncing its plugin marketplace, which clones a
    			// repository from `github.com` on every run
    			"--config",
    			"features.plugins=false",
    			"--",
    			prompt,
    		];
    	}
    
    	private agentEnv(): Record<string, string> {
    		return {};
    	}
    }

    --dangerously-bypass-approvals-and-sandbox turns off the approval prompts and the built-in command sandbox of Codex. The container limits what commands from Codex can reach instead.

    A custom provider sends no API key, and the Outbound entrypoint adds the gateway token.

    Codex reads the certificate for intercepted HTTPS from SSL_CERT_FILE, which trustEnv in the runner sets.

  5. Replace the outcome() method:

    src/sandbox.tsts
    export class AgentSandbox extends DurableObject<Env> {
    	// ...
    
    	private async outcome(exitCode: number): Promise<TaskStatus> {
    		if (exitCode === 0) {
    			return {
    				state: "succeeded",
    				result: (await this.readOptionalText(lastMessagePath)) ?? "",
    			};
    		}
    
    		let failure: string | undefined;
    		const events = await this.files.readFile(eventsPath);
    
    		for await (const line of readLines(events.body)) {
    			const parsed = FailureEvent.safeParse(parseJson(line));
    
    			if (parsed.success) {
    				failure =
    					parsed.data.type === "error"
    						? parsed.data.message
    						: parsed.data.error.message;
    			}
    		}
    
    		if (failure === undefined) {
    			const stderr = (await this.readOptionalText(stderrPath)) ?? "";
    			return {
    				state: "failed",
    				error: `codex exited with ${exitCode}: ${stderr.slice(-2000)}`,
    			};
    		}
    
    		return { state: "failed", error: failure };
    	}
    }

    Codex exits with a nonzero code when its turn fails, so the exit code decides the outcome. A failed task returns the message from the last turn.failed or error event. Codex retries a failing model request five times, and the task fails after about 20 seconds.

  6. Deploy your Worker:

    npx wrangler deploy

    The AI_GATEWAY_TOKEN secret from the tutorial stays in place. A running container keeps the image it started with, so use a new sandbox name for Codex.

Run a task

  1. Clone a repository into a sandbox named codex-1:

    curl "$WORKER_URL/sandboxes/codex-1/repository" \
    	--json '{"url": "https://github.com/octocat/Hello-World"}'
  2. Start a task:

    curl "$WORKER_URL/sandboxes/codex-1/task" \
    	--data "Add a file NOTES.md with a one-sentence summary of this repository. Do not commit."
  3. Check the task until its state is succeeded or failed:

    curl "$WORKER_URL/sandboxes/codex-1/task"

    A succeeded task includes the final message that Codex wrote.

  4. Read the changes:

    curl "$WORKER_URL/sandboxes/codex-1/diff"

    The diff shows the new NOTES.md file.

Was this helpful?