Skip to content

Latest commit

 

History

History
218 lines (164 loc) · 13.5 KB

File metadata and controls

218 lines (164 loc) · 13.5 KB

Design: OpenAI Codex CLI (codex) adapter

Problem

OpenAI ships an agentic coding CLI (codex, package @openai/codex). cuekit needs an adapter to spawn it as a delegation child, with the same UX guarantees the other pane adapters give:

  • parents can attach to the live child pane via the multiplexer backend;
  • parents can steer interactive children with steer_task;
  • parents can opt into a batch run mode for short single-shot prompts;
  • child output is captured in the normal cuekit transcript path and exit-code sentinel;
  • terminal status inference works the same way as claude-code / antigravity;
  • permission-bypass behaves consistently with the existing dangerously_skip_permissions semantics.

The adapter ships alongside the existing claude-code / opencode / gemini / antigravity adapters; users pick agent_kind: codex per task or set it via project config / agent profile.

Relevant codex shape

Probed against codex-cli 0.133.0 (codex --help + codex exec --help):

Usage: codex [OPTIONS] [PROMPT]
       codex [OPTIONS] <COMMAND> [ARGS]

Commands:
  exec  Run Codex non-interactively (also accepts a [PROMPT] positional)
  ... (mcp, doctor, sandbox, plugin, ...)

Options:
  -m, --model <MODEL>
  -s, --sandbox <SANDBOX_MODE>            read-only | workspace-write | danger-full-access
  -a, --ask-for-approval <APPROVAL_POLICY> untrusted | on-failure | on-request | never
  --dangerously-bypass-approvals-and-sandbox
  --dangerously-bypass-hook-trust
  -C, --cd <DIR>
  --add-dir <DIR>
  -i, --image <FILE>
  -c, --config <key=value>
  -p, --profile <CONFIG_PROFILE>

Compared to antigravity (agy) and gemini:

capability gemini flag agy flag codex flag delta vs antigravity
batch (one-shot) -p '<prompt>' -p '<prompt>' exec '<prompt>' (SUBCOMMAND) subcommand, not flag
interactive (REPL alive) positional '<prompt>' -i '<prompt>' positional '<prompt>' positional, like gemini
permission bypass -y --dangerously-skip-permissions --dangerously-bypass-approvals-and-sandbox longer flag name
restricted / sandbox --approval-mode plan --sandbox (boolean) -s <mode> with value takes a value, 3 modes
model selection -m '<model>' -m '<model>' present (antigravity lacks it)

Decision

Implement createCodexAdapter as a normal createPaneAdapter consumer, mirroring the antigravity adapter's structure but with codex's flag set.

Run mode Launch shape
interactive (default) codex [-s <mode> -a <policy> | -a <policy> | --dangerously-bypass-approvals-and-sandbox] [-m '<model>'] '<rendered prompt>'
batch codex exec [...same flags...] '<rendered prompt>'

Codex separates the SANDBOX axis (what the agent can do) from the APPROVAL axis (when codex pauses to ask the human). The single --dangerously-bypass-approvals-and-sandbox flag disables BOTH; setting -s alone disables only the sandbox part and leaves codex's default approval policy active — which can stall an unattended cuekit pane on a "please approve this command" prompt. The adapter therefore chooses between three exclusive flag groups:

  • adapter_options.sandbox set → emit -s <mode> AND -a <ask_for_approval ?? "never">, skip the bypass flag. Default -a never keeps the unattended pane unblocked; callers can override with adapter_options.ask_for_approval.
  • adapter_options.ask_for_approval set without sandbox → emit -a <policy> alone, skip the bypass flag. Codex's default sandbox stays.
  • Otherwise, shouldDangerouslySkipPermissions(spec) true (default) → emit --dangerously-bypass-approvals-and-sandbox (bypasses both axes). Callers opt out with adapter_options.dangerously_skip_permissions: false.

The launch command still goes through wrapLaunchCommandWithExitCode so terminal status inference and the exit-code sentinel work the same way as claude-code.

The launch command still goes through wrapLaunchCommandWithExitCode so terminal status inference and the exit-code sentinel work the same way as claude-code.

Capabilities

{
  agent_kind: "codex",
  supports_steering: true,
  supports_attach: true,
  supports_model_selection: true,   // ← divergence from antigravity
  supports_artifacts: true,
  supports_live_progress: false,
  default_mode: "interactive",
  supported_modes: ["interactive", "batch"],
}

supports_model_selection: true is the load-bearing divergence from antigravity. Codex has -m <MODEL>, so model selection is real and we must advertise it correctly — the MCP list({ kind: "adapters" }) surface uses this to drive UX (model dropdowns, profile resolution, defaults attribution).

Launch command details

A pure builder lives in packages/adapters/src/codex-adapter.ts:

const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"] as const;
const CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"] as const;

// `readSandboxMode` and `readApprovalPolicy` follow the same shape: parse the
// string value, validate against the enum, return undefined otherwise.
// Invalid input never produces a malformed launch command.

export function buildCodexLaunchCommand(spec: TaskSpec, options = {}): string {
  const bin = options.codexBin ?? "codex";
  const parts: string[] = [bin];
  if (adapterRunModeFor(spec) === "batch") parts.push("exec");
  const sandbox = readSandboxMode(spec);
  const explicitApproval = readApprovalPolicy(spec);
  if (sandbox) {
    parts.push("-s", sandbox);
    parts.push("-a", explicitApproval ?? "never");
  } else if (explicitApproval) {
    parts.push("-a", explicitApproval);
  } else if (shouldDangerouslySkipPermissions(spec)) {
    parts.push("--dangerously-bypass-approvals-and-sandbox");
  }
  if (spec.model) parts.push("-m", shellQuote(spec.model));
  parts.push(shellQuote(renderTaskSpecPrompt(spec)));
  return parts.join(" ");
}

Notes:

  • Batch mode is a subcommand (codex exec), not a flag. The subcommand appears immediately after the binary name and BEFORE the options. Copy-pasting from antigravity (which uses -p flag for batch) will silently produce a broken command.
  • adapter_options.sandbox accepts a STRING value (read-only / workspace-write / danger-full-access), not a boolean. Invalid values are silently ignored — the adapter falls back to default-on bypass. This matches the soft-fail semantics adapter_options uses elsewhere (e.g. antigravity's === true check).
  • The bypass flag is --dangerously-bypass-approvals-and-sandbox — longer than claude-code's --dangerously-skip-permissions and antigravity's --dangerously-skip-permissions. Rename will silently fail (codex rejects unknown flags). The adapter literally inlines the flag string to make grep-driven refactors safe.

Steering, attach, and lifecycle

These follow the shared pane adapter contract — nothing codex-specific. The smoke test must verify in a real runtime that:

  1. tmux send-keys reliably appends a message to a running interactive codex REPL prompt;
  2. codex does not eat or echo the trailing newline in a way that breaks subsequent steering;
  3. transitioning between codex's busy state and prompt-ready state does not require a special pre-keystroke before send-keys.

If (3) turns out to be necessary, follow the pattern opencode uses (a small pre-key sequence before the message), not a codex-specific bespoke path.

Auth and setup

codex requires an authenticated OpenAI account. The adapter assumes the binary is already authenticated (via codex login). cuekit doctor probes codex --version and surfaces a clear hint when missing; it does not attempt to write or refresh credentials.

For users registering cuekit as an MCP server in codex itself, codex ships its own codex mcp add <name> -- <command> CLI that manages ~/.codex/config.toml directly:

codex mcp add cuekit -- cuekit --mcp

The cuekit dispatcher does NOT wrap this — codex's own CLI handles the TOML config correctly, and there's no hyphenated-path pitfall like antigravity has.

Files to change in implementation

  • Create packages/adapters/src/codex-adapter.ts.
  • Export createCodexAdapter and buildCodexLaunchCommand from packages/adapters/src/index.ts.
  • Register the adapter in packages/adapters/src/build-registry.ts alongside the other six.
  • Add packages/adapters/__tests__/codex-adapter.test.ts covering build/launch and capability shape.
  • Update cuekit doctor ADAPTER_EXECUTABLES table to include { kind: "codex", command: "codex" }.
  • Update MCP / stdio integration tests that assert adapter list (add codex to the expected set).
  • Update README and AGENTS.md adapter lists.
  • Update cuekit-adapter-run-modes-design.md command matrix.
  • Update cuekit-adapter-permission-bypass-design.md — codex inherits the same default-on bypass with -s <mode> as the opt-in restriction lever.
  • Update packages/project-config/src/init.ts to render a codex: block in the generated config.
  • Add drift-prevention tests in packages/project-config/__tests__/apply.test.ts mirroring the antigravity pattern.
  • Add .cuekit.example.yaml block for adapters.codex.permissions.

Test plan

Adapter-level tests:

  • createCodexAdapter(...).capabilities() returns agent_kind: "codex", supports_steering: true, supports_attach: true, supports_model_selection: true, default_mode: "interactive", supported_modes: ["interactive", "batch"].
  • buildCodexLaunchCommand({ agent_kind: "codex", objective: "x" }) defaults to codex --dangerously-bypass-approvals-and-sandbox '<prompt>'.
  • adapter_options.mode: "batch" prepends exec as a subcommand before flags.
  • adapter_options.dangerously_skip_permissions: false omits the bypass flag entirely (no replacement).
  • adapter_options.sandbox: "workspace-write" emits -s workspace-write and drops the bypass flag (sandbox wins).
  • All three sandbox modes (read-only / workspace-write / danger-full-access) round-trip.
  • Invalid sandbox values silently fall back to the default bypass.
  • model: "gpt-5" emits -m 'gpt-5'.
  • Prompts containing single quotes are shell-quoted POSIX-style.
  • Custom codexBin path is respected.

MCP-level tests:

  • list({ kind: "adapters" }) includes codex and reports supports_model_selection: true.
  • The stdio integration adapter count / expected agent kinds include codex.

Project-config drift-prevention:

  • shouldForceSafeAdapterOptions honors adapters.codex.permissions exactly (prompt forces, bypass does not).
  • A codex config does NOT affect a claude-code task (verbatim agent_kind match).

Validation:

bun test packages/adapters
bun test packages/mcp
bun run typecheck
bunx biome check
bun test

A real-runtime smoke test must cover:

  • submit a task with --agent_kind codex;
  • tmux attach-session -t cuekit-task-<id> shows the live REPL;
  • cuekit task steer ... reaches the running child;
  • transcript appears at <cwd>/.cuekit/tasks/<task_id>/transcript.txt;
  • cancel_tasks cleans up the pane;
  • batch mode (adapter_options: '{"mode":"batch"}') returns supports_steering: false in task status;
  • sandbox mode (adapter_options: '{"sandbox":"workspace-write"}') launches with -s workspace-write and without --dangerously-bypass-approvals-and-sandbox.

Safety notes

  • --dangerously-bypass-approvals-and-sandbox approves every tool call AND removes the sandbox. The default-on choice mirrors the existing v0 behavior across claude-code / antigravity. Project config keeps the right to force dangerously_skip_permissions: false for prompt-safe defaults — the same override applies here unchanged.
  • -s <mode> is the codex-native restriction lever. workspace-write is the most common opt-in choice (write to repo, but no arbitrary filesystem / network); read-only is for pure inspection; danger-full-access matches the bypass-flag effect but goes through codex's sandbox machinery rather than skipping it entirely (subtly different audit story).
  • Invalid adapter_options.sandbox values fall back to the default bypass silently. This matches the existing adapter_options handling (antigravity ignores non-true values for its boolean sandbox). The trade-off is "fail soft" → the user gets default behavior rather than a hard error; the test pins this so the contract doesn't drift.

Open questions

  1. -a, --ask-for-approval policy. Resolved: implemented post-initial-ship. Dogfood surfaced a pane stall — submitting a task with adapter_options.sandbox: "read-only" left codex's default approval policy active, and the unattended pane stalled on a "please approve" prompt. Fix: when sandbox is set the adapter now also emits -a <ask_for_approval ?? "never"> so the pane doesn't stall; callers wanting human-in-the-loop pass an explicit policy. adapter_options.ask_for_approval is also accepted without sandbox (drops the bypass flag, codex's default sandbox stays).
  2. -C, --cd and --add-dir. Multi-directory / explicit-cwd workflows. The pane backend already sets cwd; cuekit hasn't yet exposed multi-dir to children. Wire via adapter_options when needed.
  3. -c key=value config overrides. Codex supports arbitrary TOML overrides at the CLI. Powerful but rarely needed for child-agent delegation. Defer.
  4. Codex's own MCP server (codex mcp-server). Codex can serve as an MCP server (stdio) for other agents. Not relevant to cuekit's child-agent adapter contract, but worth noting in the migration / interop guide.