A new CodexAgent has been added to coder_eval that integrates OpenAI's Codex SDK. The implementation mirrors the structure of ClaudeCodeAgent and provides seamless integration with the evaluation framework.
Install coder-eval with the codex extra:
pip install 'coder-eval[codex]'This installs:
openai-codex- The official Codex Python SDK (from PyPI)openai-codex-cli-bin- Platform-specific Codex CLI binaries (pulled in transitively)
Codex requires authentication. Options:
# Option 1: API Key (direct)
await codex_client.login_api_key("your-api-key")
# Option 2: ChatGPT (interactive)
await codex_client.login_chatgpt()
# Option 3: Device Code Flow
await codex_client.login_chatgpt_device_code()CodexAgent.start() calls login_api_key automatically when CODEX_API_KEY is present in the environment. Without a key it falls back to any existing ChatGPT login. (Only CODEX_API_KEY is read — not OPENAI_API_KEY/AZURE_OPENAI_API_KEY; point CODEX_API_KEY at whichever endpoint's key you use.)
| Env var | Purpose |
|---|---|
CODEX_API_KEY |
Auth key/token for the selected endpoint (required for headless runs). |
CODEX_BASE_URL |
Route to a custom endpoint. Unset → standard OpenAI platform (api.openai.com). Set → custom provider (gateway or Azure). |
CODEX_MODEL |
Fallback model when agent.model is unset. On Azure this is the deployment name. |
CODEX_API_VERSION |
Azure only: the required api-version query param. Leave unset for OpenAI/gateways. |
Standard OpenAI: leave CODEX_BASE_URL unset, set CODEX_API_KEY to an OpenAI sk-… key and CODEX_MODEL to a Codex/Responses-capable model.
Azure OpenAI:
CODEX_BASE_URL=https://<your-resource>.openai.azure.com/openai
CODEX_API_VERSION=2025-04-01-preview # required by Azure
CODEX_MODEL=<your-deployment-name> # deployment, not the base model id
CODEX_API_KEY=<azure-openai-key>This registers a custom Codex model provider (base_url + env_key=CODEX_API_KEY + query_params={api-version} + wire_api=responses). The Codex CLI only supports the Responses wire API (it rejects wire_api=chat as "no longer supported"), so the protocol is fixed. If your Azure deployment requires the key in an api-key header rather than Authorization: Bearer, that needs an additional provider http_headers/env_http_headers entry — open an issue if you hit that.
Run a task with Codex agent:
coder-eval run tasks/agents/codex_hello_world.yaml --type codexOr override agent type for all tasks in an experiment:
coder-eval run experiments/example.yaml --type codexSpecify Codex in task YAML:
agent:
type: codex
permission_mode: acceptEdits
allowed_tools:
- Bash
- Read
- Write
disallowed_tools:
- Edit
plugins:
- type: local
path: "$PLUGIN_PATH"
success_criteria:
- type: file_exists
path: "src/solution.py"
description: "Solution file must exist"Valid permission_mode values:
default- Standard access, requires approval on failureacceptEdits- Automatically accept file edits, no filesystem restrictionsplan- Read-only sandbox, approval required for any changesbypassPermissions- Full access, no approvals needed
CodexAgent supports SKILL.md files following the Agent Skills open standard. Skills are discovered from:
- config.plugins - Local plugins with
type: localandpathpointing to a skills directory - plugin_tools_dir parameter - Runtime plugin directory passed to
start()
Skills are symlinked (or copied) to .agents/skills/ where the Codex CLI auto-discovers them. Environment variables in plugin paths ($VAR, ${VAR}) are expanded at runtime.
Example with environment variable:
agent:
type: codex
plugins:
- type: local
path: "$SKILLS_PLUGIN_PATH"Set environment variable:
export SKILLS_PLUGIN_PATH=~/uipath/uipath-claude-plugins/plugins/uipath-coded-agents
coder-eval run tasks/my_task.yamlAgent (ABC)
└── CodexAgent
├── Codex SDK Client (openai_codex.Codex)
├── Thread Management (thread_start, turn.stream)
└── Streaming telemetry (commands, token usage, agent text)
start(working_directory)- Initialize Codex client and set working directorycommunicate(user_input, timeout, stream_callback)- Execute one turn with Codexstop()- Clean up resourcesget_state()- Return current agent statediscard_pending_turn()- Rollback on failure
Each turn returns a TurnRecord with:
iteration- Turn numberuser_input- The prompt sentagent_output- assembled from the streamedagentMessagedeltascommands-CommandTelemetryfor each shell command (Bash) and apply_patch file change (Write)timestamp- When the turn completedduration_seconds- Wall-clock execution timetoken_usage- input/output/cache-read token counts (from the SDK token-usage stream)model_used- the pinnedagent.model, when set
The agent uses a ThreadedWatchdog to enforce wall-clock timeouts. If a turn exceeds the deadline, a TurnTimeoutError is raised with a partial TurnRecord preserved in pending_turn.
On failure, the agent:
- Sets
pending_turnto acrashed=TrueTurnRecord with captured telemetry - Raises
AgentCrashErrororTurnTimeoutError - The orchestrator reads
pending_turnand callsdiscard_pending_turn()to roll back state
The agent maps permission_mode to the Codex SDK's Sandbox. The approval mode is uniformly deny_all for every mode — the trust boundary is the sandbox, which does vary by mode:
permission_mode |
sandbox |
approval_mode |
|---|---|---|
bypassPermissions |
full-access |
deny_all |
acceptEdits |
workspace-write |
deny_all |
default |
workspace-write |
deny_all |
plan |
read-only |
deny_all |
deny_all means run autonomously, never prompt, no server-side reviewer: in-sandbox operations execute directly and only escalations beyond the sandbox are refused. coder_eval uses it for every mode because the alternative (auto_review) adds a server-side reviewer that can spuriously return declined under gateway load.
allowed_tools / disallowed_tools are normalized (Bash → shell, Write/Edit → apply_patch, etc.) and passed as enabled_tools / disabled_tools in the thread config. Note: the Codex SDK does not currently enforce disabled_tools; do not rely on it as a security boundary (the agent logs a warning when it is set).
The agent sets up SKILL.md files (Agent Skills open standard) in .agents/skills/ directory:
- Scans
config.pluginsfor local plugins withpathfield - Checks
plugin_tools_dirparameter passed tostart() - Expands environment variables in paths (
$PLUGIN_PATH,${PLUGIN_PATH}) - Symlinks skill directories (falls back to copying if symlink fails)
- Codex CLI auto-discovers skills in
.agents/skills/
The Codex SDK is synchronous. The agent uses _run_async() helper to detect and await coroutines, preserving the async interface.
| Feature | Claude Code | Codex |
|---|---|---|
| SDK Type | Subprocess (CLI via JSON generator) | Sync client (app-server subprocess) |
| Command Tracking | Full telemetry (tool name, params, duration) | Streamed telemetry: shell → Bash, apply_patch → Write |
| Model Selection | Direct via --model or config |
agent.model pinned into thread_start |
| Session Resume | --resume {session_id} |
Via thread ID |
| Permissions | permission_mode + allowed_tools |
permission_mode → sandbox/approval + allowed_tools/disallowed_tools → thread config |
| Tool Enforcement | Not enforced by coder_eval wrapper | enabled_tools honored; disabled_tools NOT enforced by the SDK |
- Tool-name collapse - Codex reports shell tools (
Read/Grep/Bash) all as shell commands, surfaced asBashtelemetry; name-keyed criteria that distinguish these tools aren't meaningful across agents. skill_triggeredcriterion - Codex has no distinctSkilltool (it engages a skill by reading its files via shell), so the criterion detects Codex engagement from that file-read signal (a command referencingskills/<name>/) instead of aSkilltool call. The file-read signal is weaker than Claude's explicit invocation.disallowed_tools- passed to the SDK but not enforced; not a security boundary.- Authentication - Requires
CODEX_API_KEYin the environment (point it at whichever endpoint's key you use — OpenAI, gateway, or Azure); the agent callslogin_api_keywhen a key is present.OPENAI_API_KEY/AZURE_OPENAI_API_KEYare NOT read. - Model field -
TurnRecord.model_usedreflects the pinnedagent.model; the CodexTurnpayload itself doesn't carry the resolved model. - Skills with Windows paths - Symlink creation may fail on Windows; agent falls back to copying (slower).
- Implement session-based resume (thread ID tracking)
- Strengthen the Codex
skill_triggeredsignal — it currently infers engagement from a file read, weaker than Claude'sSkilltool call - Capture the resolved model from the SDK (vs. the pinned config value)
Run the included test tasks:
# Basic functionality test
coder-eval run tasks/agents/codex_hello_world.yaml
# Tool restriction test (verifies disallowed_tools enforcement)
coder-eval run tasks/agents/codex_disallowed_tools_test.yaml
# Skills discovery test (requires PLUGIN_PATH environment variable)
export PLUGIN_PATH=~/path/to/skills
coder-eval run tasks/agents/codex_skills_test.yamlExample unit test to verify agent setup:
import pytest
from coder_eval.models import AgentKind, AgentConfig
from coder_eval.agents.codex_agent import CodexAgent
from coder_eval.agent import AgentState
def test_codex_agent_initialization():
"""Verify CodexAgent can be instantiated with valid config."""
config = AgentConfig(
type=AgentKind.CODEX,
permission_mode="acceptEdits",
allowed_tools=["Bash", "Read", "Write"],
)
agent = CodexAgent(config)
assert agent.get_state() == AgentState.WORKING
assert agent.config.type == AgentKind.CODEX
def test_tool_name_mapping():
"""Verify Claude Code tool names map to Codex SDK names."""
from coder_eval.agents.codex_agent import _CLAUDE_TO_CODEX_TOOL_MAP
assert _CLAUDE_TO_CODEX_TOOL_MAP["Bash"] == "shell"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Write"] == "apply_patch"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Edit"] == "apply_patch"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Read"] == "shell"
def test_permission_mode_mapping():
"""Verify permission_mode maps to a sandbox; approval is uniformly deny_all."""
from coder_eval.agents.codex_agent import (
_CODEX_APPROVAL_MODE,
_PERMISSION_MODE_TO_SANDBOX,
)
assert _PERMISSION_MODE_TO_SANDBOX["acceptEdits"] == "workspace-write"
assert _PERMISSION_MODE_TO_SANDBOX["plan"] == "read-only"
# Approval is the same for every permission mode — no per-mode mapping.
assert _CODEX_APPROVAL_MODE == "deny_all"