# codex

> OpenAI Codex as an iii worker; codex::run/start/stop/status/sessions::list spawn the codex CLI for headless turns, deliver raw thread events on the codex::raw-event trigger type and AgentEvent frames on codex::agent-event.

| field | value |
|-------|-------|
| version | 0.1.20 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=codex@0.1.20
```

## configuration

```yaml
- base_url: 
  codex_executable: 
  defaults:
    approval_policy: never
    cwd: 
    model: 
    reasoning_effort: 
    sandbox_mode: workspace-write
    skip_git_repo_check: true
  iii_context: true
```

## readme

# codex

OpenAI Codex as an iii worker: the Codex API exposed as functions and trigger types on the iii bus, nothing else. The worker spawns the same `codex` binary the user runs in their terminal, with the same login (ChatGPT or API key), the same filesystem, and the same sandbox. `codex::run` executes one headless turn and returns the result; the raw Codex thread events are delivered verbatim on the `codex::raw-event` trigger type, and a translated AgentEvent view on `codex::agent-event`, so the acp worker and any sibling worker observe a Codex run exactly like a native harness turn.

## Install

```bash
iii trigger compose::add worker=codex
```

Requires the [Codex CLI](https://github.com/openai/codex) on the host (`npm i -g @openai/codex`) and either an existing `codex login` or `OPENAI_API_KEY` in the worker environment.

## Skills

Install the `codex` agent skill for Claude Code, Cursor, and 30+ other agents:

```bash
npx skills add iii-hq/workers --skill codex
```

## Quickstart

From zero to a Codex turn over the bus:

```bash
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii trigger compose::add worker=codex
iii   # starts the engine + worker
```

Then talk to it like any other function: from `iii trigger codex::run`, or from any SDK:

```ts
import { registerWorker } from 'iii-sdk';

const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' });

const res = await iii.trigger({
  function_id: 'codex::run',
  payload: {
    prompt: 'Add a /health endpoint to server.ts and run the tests',
    cwd: '/path/to/repo',
    sandbox_mode: 'workspace-write',
  },
  timeout_ms: 600_000,
});
// { session_id, codex_thread_id, result, stop_reason, usage }
```

Or straight from the terminal with the `iii trigger` CLI:

```bash
# one full turn (raise the timeout; the default 30s is too short for agent turns)
iii trigger codex::run --timeout-ms 600000 \
  --json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}'

# quick reads use key=value syntax
iii trigger codex::sessions::list
iii trigger codex::status session_id=<session_id>

# background turn + control
iii trigger codex::start --json '{"prompt":"...","cwd":"/path/to/repo"}'
iii trigger codex::stop session_id=<session_id>

# ask the running engine for a function's description and parameter table
iii trigger codex::run --help
```

A turn from the CLI and the published schema:

![iii trigger codex::run returning the result with usage and reasoning tokens](https://raw.githubusercontent.com/iii-hq/workers/main/codex/assets/cli-run.png)

![iii trigger codex::run --help printing the published request schema as a parameter table](https://raw.githubusercontent.com/iii-hq/workers/main/codex/assets/cli-help.png)

Call `codex::run` again with the returned `session_id` to continue the same conversation: the worker maps iii session ids to Codex thread ids in engine state and resumes automatically (threads persist in `~/.codex/sessions`).

Two ids come back from every run. `session_id` is the iii session id: the key for `codex::status`, `codex::stop`, resume, and the stream group. `codex_thread_id` is Codex's internal thread id (what the worker passes to `resumeThread` under the hood) — returned for reference, not a lookup key.

## Functions

| Function | Purpose |
| --- | --- |
| `codex::run` | Run one turn, wait, return the final result |
| `codex::start` | Fire-and-forget turn; progress arrives on the streams |
| `codex::stop` | Interrupt a live run |
| `codex::status` | Session state, live flag, usage |
| `codex::sessions::list` | All sessions this worker has run |

`codex::run` accepts either a bare `prompt` string or a `messages` array (`[{ role: 'user', content: [{ type: 'text', text }] }]`) — the same input contract as the claude-code worker and `run::start_and_wait`, so the acp worker drives Codex with `--brain-fn codex::run` — plus `model`, `cwd`, `sandbox_mode`, `approval_policy`, `reasoning_effort`, `skip_git_repo_check`, `output_schema`, `images` (local image paths), `additional_directories` (extra writable roots), and `codex_config`.

### Raw API pass-through

The worker drives the `codex exec --json` CLI, so anything the CLI exposes as a `config.toml` override goes through the `codex_config` field untouched — MCP servers, model providers, profiles, network access, web search, and the rest. Each key/value becomes a `--config key=value` on the spawn:

```jsonc
{
  "prompt": "...",
  "codex_config": {
    "sandbox_workspace_write": { "network_access": true },
    "web_search": "live",
    "mcp_servers": { "github": { "command": "gh-mcp" } }
  }
}
```

Extra writable directories use the named `additional_directories` field (the CLI's `--add-dir`).

And the full output side is available raw: every event Codex emits (`thread.started`, `turn.started`, `item.started/updated/completed` for commands, patches, MCP tool calls, web searches, reasoning, agent messages, `turn.completed` with usage, `turn.failed`) is delivered verbatim on the `codex::raw-event` trigger type. Consumers that want the exact Codex wire format bind `codex::raw-event`; consumers that want harness-shaped frames bind `codex::agent-event`. Same turn, two views.

## Event feeds

The worker owns two trigger types. Bind one to a function of your own with `{ session_id }` to receive one session's frames:

| Trigger type | Carries |
| --- | --- |
| `codex::agent-event` | translated AgentEvent frames (`function_execution_start` / `function_execution_end`, `message_complete`, `turn_end`, `agent_end`), what the acp worker renders |
| `codex::raw-event` | every Codex thread event, verbatim |

Binding config: `{ "session_id": "<iii session id>", "metadata": { } }`. `session_id` is required (non-empty, at most 512 characters); `metadata` is optional and is handed to your function (it wins over the binding's own metadata). Any other key rejects the binding, and each trigger type accepts at most 256 bindings.

Your function gets one fire-and-forget call per frame:

```json
{
  "session_id": "sess-1",
  "event_id": "sess-1-<epoch>-00000007",
  "seq": 7,
  "epoch": "<uuid>",
  "source": "codex",
  "event": { "type": "message_complete", "message": { } }
}
```

`seq` counts from 0 per feed and session within one `epoch` (a uuid minted per worker process, so a restart starts a new epoch); the two feeds count independently. Deliveries may arrive out of order: order by `(epoch, seq)` and dedup by `event_id`. The feeds are ephemeral: nothing is stored or replayed, and a binding made mid-turn only sees later frames. History lives in the session record (`codex::status`, `codex::sessions::list`) and in the `codex::run` result. A failing consumer never fails the turn. These trigger types replace the former `agent::events` / `codex::events` streams (see the guide "Migrate from iii-stream and pubsub").

## Configuration

```yaml
engine_url: ws://127.0.0.1:49134

defaults:
  model: ""                       # empty = Codex default
  sandbox_mode: workspace-write   # read-only | workspace-write | danger-full-access
  approval_policy: never          # never | on-request | on-failure | untrusted
  reasoning_effort: ""            # empty = Codex default
  cwd: ""                         # default working directory for runs
  skip_git_repo_check: true

codex_executable: ""               # path to the codex CLI; empty = PATH resolution
```

Sandboxing is Codex's own: `read-only` blocks writes, `workspace-write` allows edits inside `cwd`, `danger-full-access` disables the sandbox. `approval_policy` is forwarded to Codex per turn from the payload or the configured default; headless callers leave it at `never` so a command the sandbox blocks fails instead of prompting, but any other value is passed through and respected.

## The agent on the bus

By default every turn carries the iii runtime context, delivered through Codex's native `developer_instructions` config (a developer-role message in the turn context, the channel Codex itself uses for standing instructions): the same engine-grounded rules as the harness identity prompts, retargeted to the `iii` CLI the agent reaches through its sandboxed shell. The agent discovers capabilities from the live engine instead of memory — `iii trigger engine::functions::list` to find function ids, `iii trigger <fn> --help` as the contract before every first call, the registry flow (`directory::registry::workers::list/info`, `worker::add`) when nothing registered fits — plus the calling rules and error-handling discipline that go with them. Local file edits stay on Codex's native tools; backend actions go through registered functions.

```bash
# the agent answers this by querying the live engine itself
iii trigger codex::run --timeout-ms 300000 \
  --json '{"prompt":"List every worker connected to this engine and what each one does.","cwd":"/tmp"}'
```

The block rides the turn context on every call, resumes included, without touching your prompt or Codex's base instructions. Turn it off per call with `"iii_context": false` or globally in `config.yaml`; a caller-supplied `developer_instructions` in `codex_config` wins over it.

## Plan-then-execute with sandbox modes

Codex has no named plan mode; the equivalent is a planning prompt under the `read-only` sandbox. The guarantee is OS-level (Seatbelt on macOS, Landlock on Linux), so writes physically fail rather than being policy-declined. Because the worker resumes threads, plan-then-execute is two calls against the same `session_id`:

```bash
# 1. plan (OS-enforced read-only)
iii trigger codex::run --timeout-ms 600000 \
  --json '{"prompt":"Plan how to add rate limiting to the REST API. Do not implement.","cwd":"/path/to/repo","sandbox_mode":"read-only"}'

# 2. execute the plan with full context, same thread
iii trigger codex::run --timeout-ms 600000 \
  --json '{"session_id":"<from-step-1>","prompt":"Implement the plan.","sandbox_mode":"workspace-write","cwd":"/path/to/repo"}'
```

The approval step is whatever sits between the two calls — a human reading the plan, another worker, or a trigger.

## Observability

Every `codex::run` is an ordinary traced invocation on the engine: the trace carries the full input payload and the output (result, usage) as span events, with per-function p50/p95/p99 in the console's trace explorer — no extra instrumentation in the worker.

![codex::run invocations in the iii console trace explorer, with input and output payloads](https://raw.githubusercontent.com/iii-hq/workers/main/codex/assets/console-traces.png)

## How it maps

| Codex | iii |
| --- | --- |
| SDK `runStreamed()` turn | `codex::run` invocation |
| every thread event, verbatim | `codex::raw-event` delivery |
| agent_message / reasoning item | `message_complete` frame on `codex::agent-event` |
| command_execution / file_change / mcp_tool_call / web_search | `function_execution_start` / `function_execution_end` frames |
| turn end | `turn_end` + `agent_end` frames, function return value |
| thread resume | engine state scope `codex_sessions`, keyed by iii session_id |
| extra capability | another iii worker on the bus (`shell`, `database`, `storage`, ...) |

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/codex?tab=skills&skill=SKILL.md&version=0.1.20)

### SKILL.md

# codex

The codex worker exposes the OpenAI Codex API as iii functions. One
`codex::run` call executes one headless Codex turn — the same agent the user
runs in their terminal, with the same login, filesystem, and sandbox — in a
chosen working directory, and returns the final result and token usage. The
worker is a pure pass-through: named payload fields cover the common path,
the `options` field forwards any Codex SDK ThreadOption verbatim, and every
thread event is delivered untouched on the `codex::raw-event` trigger type. A
translated AgentEvent view is delivered on `codex::agent-event`, which is what
the acp worker renders.

Requires the `codex` CLI on the host with an existing `codex login` or
`OPENAI_API_KEY` in the worker environment. When a turn needs a capability
beyond Codex itself, add another iii worker to the bus instead of bolting
anything onto this one.

## When to Use

- Delegate a whole coding task ("add an endpoint and run the tests") in one
  call, instead of orchestrating individual `coder::*` / `shell::*` calls
  yourself: `codex::run` with `prompt` and `cwd`.
- Continue a conversation across calls: pass the same `session_id` again and
  the worker resumes the underlying Codex thread with full context.
- Run long jobs without holding the call open: `codex::start` returns
  `{session_id, started}` immediately; bind `codex::raw-event` with
  `{ session_id }` for raw progress or `codex::agent-event` for the rendered
  view (or poll `codex::status`); interrupt with `codex::stop`.
- Act on the whole backend: turns carry the iii runtime context by default
  (delivered as Codex `developer_instructions`), so the agent discovers and
  calls any registered function through the iii CLI
  (engine::functions::list, `iii trigger <fn> --help`); disable per turn
  with `iii_context: false`.
- Plan before touching anything: run the planning prompt with
  `sandbox_mode: read-only` (writes physically fail), read the plan, then
  send "implement the plan" on the same `session_id` with
  `sandbox_mode: workspace-write`.
- Get structured final output: pass `output_schema` (JSON schema) and the
  final agent message is JSON matching it.
- Attach screenshots or diagrams: `images: ["/path/a.png"]` adds local
  images to the prompt.
- Wire MCP servers or model providers into one turn: `codex_config`
  forwards any `config.toml` override, e.g.
  `{"codex_config": {"mcp_servers": {"github": {"command": "gh-mcp"}}}}`.
- Reach past the named payload fields: anything the SDK ThreadOptions
  accept goes through `options` unchanged, e.g.
  `{"options": {"networkAccessEnabled": true, "webSearchMode": "live"}}`.

## Boundaries

- Spawns the host `codex` CLI per turn — needs Codex installed and
  authenticated; not available inside a bare container without it.
- Execution safety is Codex's own sandbox (`sandbox_mode`), not the
  engine's: `read-only` blocks writes, `workspace-write` allows edits in
  `cwd`, `danger-full-access` disables the sandbox. Headless turns run
  `approval_policy: never`, so blocked commands fail instead of prompting.
- One turn per session at a time: check `codex::status` (`live: true`)
  before sending another `codex::run` for the same `session_id`; parallel
  runs against one session race on the underlying thread resume.
- `codex::agent-event` carries whole-message frames; per-item progress detail
  (command output as it accumulates, todo lists) exists only on
  `codex::raw-event`. Both feeds are ephemeral (not stored or replayed);
  order by `(epoch, seq)`.

## Functions

- `codex::run` — run one Codex turn and wait; accepts `prompt` (or a
  `messages` array whose last user entry becomes the prompt), plus `model`,
  `cwd`, `sandbox_mode`, `approval_policy`, `reasoning_effort`,
  `skip_git_repo_check`, `output_schema`, and raw `options`; returns
  `{session_id, codex_thread_id, result, stop_reason, usage}`.
- `codex::start` — same payload, returns `{session_id, started}`
  immediately; progress arrives on the event feeds.
- `codex::stop` — interrupt the live run for a session.
- `codex::status` — point-in-time session view: live flag, status, turns,
  usage.
- `codex::sessions::list` — every session this worker has run.

## api reference

```json
{
  "functions": [
    {
      "description": "Internal: reload codex configuration when it changes.",
      "metadata": {
        "internal": true
      },
      "name": "codex::on-config-change",
      "request_schema": {
        "properties": {},
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "type": "object"
      }
    },
    {
      "description": "Run a Codex coding-agent turn and wait for its result. Accepts `prompt` or a `messages` array plus a `codex_config` pass-through; delivers raw Codex events on codex::raw-event and AgentEvent frames on codex::agent-event, and returns {session_id, result, usage}.",
      "metadata": {},
      "name": "codex::run",
      "request_schema": {
        "definitions": {
          "Message": {
            "properties": {
              "content": {
                "description": "Either a plain string or an array of content blocks."
              },
              "role": {
                "type": "string"
              }
            },
            "required": [
              "content",
              "role"
            ],
            "type": "object"
          }
        },
        "properties": {
          "additional_directories": {
            "default": null,
            "description": "Additional writable directories alongside the working root (--add-dir).",
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "approval_policy": {
            "default": null,
            "description": "Codex approval policy; headless callers leave it at never.",
            "type": [
              "string",
              "null"
            ]
          },
          "codex_config": {
            "default": null,
            "description": "Codex config.toml overrides for this turn (e.g. mcp_servers, model_providers, profiles)."
          },
          "cwd": {
            "default": null,
            "description": "Working directory the turn runs in.",
            "type": [
              "string",
              "null"
            ]
          },
          "iii_context": {
            "default": null,
            "description": "Inject the iii runtime discovery prompt as developer_instructions (default true via config).",
            "type": [
              "boolean",
              "null"
            ]
          },
          "images": {
            "default": null,
            "description": "Paths to local images attached to the prompt.",
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "messages": {
            "description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt.",
            "items": {
              "$ref": "#/definitions/Message"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "model": {
            "default": null,
            "description": "Model id; empty = Codex default.",
            "type": [
              "string",
              "null"
            ]
          },
          "output_schema": {
            "default": null,
            "description": "JSON schema for structured final output."
          },
          "prompt": {
            "default": null,
            "description": "The user prompt for this turn.",
            "type": [
              "string",
              "null"
            ]
          },
          "reasoning_effort": {
            "default": null,
            "description": "Model reasoning effort: minimal | low | medium | high | xhigh.",
            "type": [
              "string",
              "null"
            ]
          },
          "sandbox_mode": {
            "default": null,
            "description": "Codex sandbox mode: read-only | workspace-write | danger-full-access.",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "default": null,
            "description": "iii session id; reuse to resume the same Codex thread.",
            "type": [
              "string",
              "null"
            ]
          },
          "skip_git_repo_check": {
            "default": null,
            "description": "Allow running outside a git repository.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "timeout_ms": {
            "default": null,
            "description": "Reserved for callers; not forwarded.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "title": "RunRequest",
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "busy": {
            "type": "boolean"
          },
          "codex_thread_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_error": {
            "type": "boolean"
          },
          "num_turns": {
            "type": "integer"
          },
          "reason": {
            "type": "string"
          },
          "result": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          },
          "stop_reason": {
            "type": "string"
          },
          "usage": {
            "type": [
              "object",
              "null"
            ]
          }
        },
        "type": "object"
      }
    },
    {
      "description": "List every Codex session this worker has run.",
      "metadata": {},
      "name": "codex::sessions::list",
      "request_schema": {
        "properties": {},
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "sessions": {
            "items": {
              "type": "object"
            },
            "type": "array"
          }
        },
        "type": "object"
      }
    },
    {
      "description": "Start a Codex turn and return immediately; bind codex::agent-event (or codex::raw-event) with { session_id } for progress and turn_end, or poll codex::status.",
      "metadata": {},
      "name": "codex::start",
      "request_schema": {
        "definitions": {
          "Message": {
            "properties": {
              "content": {
                "description": "Either a plain string or an array of content blocks."
              },
              "role": {
                "type": "string"
              }
            },
            "required": [
              "content",
              "role"
            ],
            "type": "object"
          }
        },
        "properties": {
          "additional_directories": {
            "default": null,
            "description": "Additional writable directories alongside the working root (--add-dir).",
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "approval_policy": {
            "default": null,
            "description": "Codex approval policy; headless callers leave it at never.",
            "type": [
              "string",
              "null"
            ]
          },
          "codex_config": {
            "default": null,
            "description": "Codex config.toml overrides for this turn (e.g. mcp_servers, model_providers, profiles)."
          },
          "cwd": {
            "default": null,
            "description": "Working directory the turn runs in.",
            "type": [
              "string",
              "null"
            ]
          },
          "iii_context": {
            "default": null,
            "description": "Inject the iii runtime discovery prompt as developer_instructions (default true via config).",
            "type": [
              "boolean",
              "null"
            ]
          },
          "images": {
            "default": null,
            "description": "Paths to local images attached to the prompt.",
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "messages": {
            "description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt.",
            "items": {
              "$ref": "#/definitions/Message"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "model": {
            "default": null,
            "description": "Model id; empty = Codex default.",
            "type": [
              "string",
              "null"
            ]
          },
          "output_schema": {
            "default": null,
            "description": "JSON schema for structured final output."
          },
          "prompt": {
            "default": null,
            "description": "The user prompt for this turn.",
            "type": [
              "string",
              "null"
            ]
          },
          "reasoning_effort": {
            "default": null,
            "description": "Model reasoning effort: minimal | low | medium | high | xhigh.",
            "type": [
              "string",
              "null"
            ]
          },
          "sandbox_mode": {
            "default": null,
            "description": "Codex sandbox mode: read-only | workspace-write | danger-full-access.",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "default": null,
            "description": "iii session id; reuse to resume the same Codex thread.",
            "type": [
              "string",
              "null"
            ]
          },
          "skip_git_repo_check": {
            "default": null,
            "description": "Allow running outside a git repository.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "timeout_ms": {
            "default": null,
            "description": "Reserved for callers; not forwarded.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "title": "RunRequest",
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "session_id": {
            "type": "string"
          },
          "started": {
            "type": "boolean"
          }
        },
        "type": "object"
      }
    },
    {
      "description": "Check the status of one Codex session.",
      "metadata": {},
      "name": "codex::status",
      "request_schema": {
        "properties": {
          "session_id": {
            "description": "iii session id returned by codex::run / codex::start.",
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "title": "SessionIdRequest",
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "live": {
            "type": "boolean"
          },
          "record": {
            "type": [
              "object",
              "null"
            ]
          },
          "session_id": {
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    {
      "description": "Interrupt a live Codex run for a session.",
      "metadata": {},
      "name": "codex::stop",
      "request_schema": {
        "properties": {
          "session_id": {
            "description": "iii session id returned by codex::run / codex::start.",
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "title": "SessionIdRequest",
        "type": "object"
      },
      "response_schema": {
        "properties": {
          "reason": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          },
          "stopped": {
            "type": "boolean"
          }
        },
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Translated AgentEvent frames of one codex session. Config: { session_id, metadata? }. Payload: { session_id, event_id, seq, epoch, source, event }. Ephemeral (not stored, not replayed); order by (epoch, seq).",
      "invocation_schema": {},
      "metadata": {},
      "name": "codex::agent-event",
      "return_schema": {}
    },
    {
      "description": "Raw Codex thread events (codex exec --json lines, verbatim) of one codex session. Config: { session_id, metadata? }. Payload: { session_id, event_id, seq, epoch, source, event }. Ephemeral (not stored, not replayed); order by (epoch, seq).",
      "invocation_schema": {},
      "metadata": {},
      "name": "codex::raw-event",
      "return_schema": {}
    }
  ]
}
```
