diff --git a/.github/workflows/docker-smoke-test-unified.yml b/.github/workflows/docker-smoke-test-unified.yml index a20899028..90d49200c 100644 --- a/.github/workflows/docker-smoke-test-unified.yml +++ b/.github/workflows/docker-smoke-test-unified.yml @@ -84,3 +84,41 @@ jobs: - name: Verify agent CLI exists run: docker run --rm --entrypoint which openab-unified-${{ matrix.target.name }}:test ${{ matrix.target.agent }} + + - name: Verify Codex runtime versions + if: matrix.target.name == 'codex' + run: | + # Expected versions come from the Dockerfile ARG defaults (single + # source of truth) so a version bump only touches the Dockerfiles. + CODEX_ACP_VERSION=$(awk -F= '$1 == "ARG CODEX_ACP_VERSION" {print $2; exit}' Dockerfile.unified) + CODEX_VERSION=$(awk -F= '$1 == "ARG CODEX_VERSION" {print $2; exit}' Dockerfile.unified) + test -n "$CODEX_ACP_VERSION" && test -n "$CODEX_VERSION" + docker run --rm --entrypoint codex-acp openab-unified-codex:test --version \ + | grep -F "@agentclientprotocol/codex-acp ${CODEX_ACP_VERSION}" + docker run --rm --entrypoint codex openab-unified-codex:test --version \ + | grep -F "codex-cli ${CODEX_VERSION}" + + - name: Verify Codex agent command is a single token + if: matrix.target.name == 'codex' + run: | + # Regression guard: OPENAB_AGENT_COMMAND must stay a plain binary. + # Packing env-var prefixes (e.g. "/usr/bin/env K=V codex-acp") breaks + # configs that override [agent].args only — OAB keeps token 1 as the + # command and replaces the rest, yielding "/usr/bin/env ". + docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' openab-unified-codex:test \ + | grep -Fx 'OPENAB_AGENT_COMMAND=codex-acp' + + - name: Verify Codex ACP handshake + if: matrix.target.name == 'codex' + run: | + INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{},"clientInfo":{"name":"ci-test","version":"0.0.1"}}}' + CID=$(docker run -d -i --entrypoint codex-acp openab-unified-codex:test) + echo "$INIT" | docker attach --no-stdin=false "$CID" & + sleep 5 + RESPONSE=$(docker logs "$CID" 2>/dev/null | grep -m1 '^{' || true) + docker rm -f "$CID" >/dev/null 2>&1 + + echo "Response: $RESPONSE" + echo "$RESPONSE" | jq -e \ + '.result.agentInfo.name == "@agentclientprotocol/codex-acp"' \ + >/dev/null diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index 35a50fd1c..a1e3d2ac8 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -54,9 +54,36 @@ jobs: - name: Verify agent CLI exists run: docker run --rm --entrypoint which openab-test${{ matrix.variant.suffix }} ${{ matrix.variant.agent }} + - name: Verify Codex runtime versions + if: matrix.variant.suffix == '-codex' + run: | + # Expected versions come from the Dockerfile ARG defaults (single + # source of truth) so a version bump only touches the Dockerfiles. + CODEX_ACP_VERSION=$(awk -F= '$1 == "ARG CODEX_ACP_VERSION" {print $2; exit}' Dockerfile.codex) + CODEX_VERSION=$(awk -F= '$1 == "ARG CODEX_VERSION" {print $2; exit}' Dockerfile.codex) + test -n "$CODEX_ACP_VERSION" && test -n "$CODEX_VERSION" + docker run --rm --entrypoint codex-acp openab-test-codex --version \ + | grep -F "@agentclientprotocol/codex-acp ${CODEX_ACP_VERSION}" + docker run --rm --entrypoint codex openab-test-codex --version \ + | grep -F "codex-cli ${CODEX_VERSION}" + + - name: Verify Codex agent command is a single token + if: matrix.variant.suffix == '-codex' + run: | + # Regression guard: OPENAB_AGENT_COMMAND must stay a plain binary. + # Packing env-var prefixes (e.g. "/usr/bin/env K=V codex-acp") breaks + # configs that override [agent].args only — OAB keeps token 1 as the + # command and replaces the rest, yielding "/usr/bin/env ". + docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' openab-test-codex \ + | grep -Fx 'OPENAB_AGENT_COMMAND=codex-acp' + - name: Verify agent responds run: | - INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientCapabilities":{},"clientInfo":{"name":"ci-test","version":"0.0.1"}}}' + if [ "${{ matrix.variant.suffix }}" = "-codex" ]; then + INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{},"clientInfo":{"name":"ci-test","version":"0.0.1"}}}' + else + INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientCapabilities":{},"clientInfo":{"name":"ci-test","version":"0.0.1"}}}' + fi # Start agent in background, send init, capture output with timeout CID=$(docker run -d -i --entrypoint sh openab-test${{ matrix.variant.suffix }} -c 'exec ${{ matrix.variant.agent }} ${{ matrix.variant.agent_args }} 2>/dev/null') @@ -70,6 +97,9 @@ jobs: if [ -n "$RESPONSE" ] && echo "$RESPONSE" | jq -e '.result.agentInfo.name' > /dev/null 2>&1; then AGENT_NAME=$(echo "$RESPONSE" | jq -r '.result.agentInfo.name') echo "✅ ACP handshake ok — agent=$AGENT_NAME" + elif [ "${{ matrix.variant.suffix }}" = "-codex" ]; then + echo "❌ Codex ACP handshake failed" + exit 1 else echo "⚠️ ACP handshake returned no response — falling back to CLI check" docker run --rm --entrypoint ${{ matrix.variant.agent }} \ diff --git a/Dockerfile.codex b/Dockerfile.codex index 28c8c3908..54c2f63e3 100644 --- a/Dockerfile.codex +++ b/Dockerfile.codex @@ -19,9 +19,9 @@ FROM node:22-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini bubblewrap unzip && rm -rf /var/lib/apt/lists/* # Pre-install codex-acp and codex CLI globally -ARG CODEX_ACP_VERSION=0.16.0 -ARG CODEX_VERSION=0.141.0 -RUN npm install -g @zed-industries/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --retry 3 +ARG CODEX_ACP_VERSION=1.1.2 +ARG CODEX_VERSION=0.144.1 +RUN npm install -g @agentclientprotocol/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --fetch-retries=3 # Install gh CLI RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ diff --git a/Dockerfile.package b/Dockerfile.package index f28f3ddda..44a4bd655 100644 --- a/Dockerfile.package +++ b/Dockerfile.package @@ -91,9 +91,9 @@ CMD ["openab", "run", "-c", "/etc/openab/config.toml"] # Target: codex # ============================================================================= FROM base-node AS codex -ARG CODEX_ACP_VERSION=0.16.0 -ARG CODEX_VERSION=0.141.0 -RUN npm install -g @zed-industries/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --retry 3 +ARG CODEX_ACP_VERSION=1.1.2 +ARG CODEX_VERSION=0.144.1 +RUN npm install -g @agentclientprotocol/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --fetch-retries=3 ENV HOME=/home/node WORKDIR /home/node COPY --from=bins --chown=node:node /openab /usr/local/bin/openab diff --git a/Dockerfile.unified b/Dockerfile.unified index c533b3dc7..4e579a5d6 100644 --- a/Dockerfile.unified +++ b/Dockerfile.unified @@ -138,9 +138,9 @@ CMD ["openab", "run", "-c", "/etc/openab/config.toml"] # Target: codex # ============================================================================= FROM base-node AS codex -ARG CODEX_ACP_VERSION=0.16.0 -ARG CODEX_VERSION=0.141.0 -RUN npm install -g @zed-industries/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --retry 3 +ARG CODEX_ACP_VERSION=1.1.2 +ARG CODEX_VERSION=0.144.1 +RUN npm install -g @agentclientprotocol/codex-acp@${CODEX_ACP_VERSION} @openai/codex@${CODEX_VERSION} --fetch-retries=3 ENV HOME=/home/node WORKDIR /home/node COPY --from=builder --chown=node:node /build/target/release/openab /usr/local/bin/openab diff --git a/README.md b/README.md index c927e9f2f..56603e61e 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ The bot creates a thread. After that, just type in the thread — no @mention ne |-------|-----|-------------|-------| | Kiro (default) | `kiro-cli acp` | Native | [docs/kiro.md](docs/kiro.md) | | Claude Code | `claude-agent-acp` | [@agentclientprotocol/claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) | [docs/claude-code.md](docs/claude-code.md) | -| Codex | `codex-acp` | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | [docs/codex.md](docs/codex.md) | +| Codex | `codex-acp` | [@agentclientprotocol/codex-acp](https://github.com/agentclientprotocol/codex-acp) | [docs/codex.md](docs/codex.md) | | Gemini | `gemini --acp` | Native | [docs/gemini.md](docs/gemini.md) | | OpenCode | `opencode acp` | Native | [docs/opencode.md](docs/opencode.md) | | MiMo-Code | `mimo acp` | Native | [docs/mimocode.md](docs/mimocode.md) | diff --git a/README.zh-TW.md b/README.zh-TW.md index 1db0ed09b..2315e0fe8 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -167,7 +167,7 @@ bot 會建立一個討論串。之後只要直接在討論串中輸入即可, |-------|-----|-------------|------| | Kiro(預設) | `kiro-cli acp` | Native | [docs/kiro.md](docs/kiro.md) | | Claude Code | `claude-agent-acp` | [@agentclientprotocol/claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) | [docs/claude-code.md](docs/claude-code.md) | -| Codex | `codex-acp` | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | [docs/codex.md](docs/codex.md) | +| Codex | `codex-acp` | [@agentclientprotocol/codex-acp](https://github.com/agentclientprotocol/codex-acp) | [docs/codex.md](docs/codex.md) | | Gemini | `gemini --acp` | Native | [docs/gemini.md](docs/gemini.md) | | OpenCode | `opencode acp` | Native | [docs/opencode.md](docs/opencode.md) | | MiMo-Code | `mimo acp` | Native | [docs/mimocode.md](docs/mimocode.md) | diff --git a/config.toml.example b/config.toml.example index 28b8bb70a..953d6519b 100644 --- a/config.toml.example +++ b/config.toml.example @@ -119,10 +119,12 @@ working_dir = "/home/agent" # inherit_env = ["API_BASE_URL", "MODEL_NAME"] # [agent] -# command = "codex" -# args = ["--acp"] +# command = "codex-acp" # working_dir = "/home/node" # env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } +# Recommended for containerized deployments (outer container = security boundary): +# [pool] +# default_config_options = { mode = "agent-full-access" } # [agent] # command = "gemini" diff --git a/crates/openab-core/src/setup/wizard.rs b/crates/openab-core/src/setup/wizard.rs index f5a789609..67d4acb07 100644 --- a/crates/openab-core/src/setup/wizard.rs +++ b/crates/openab-core/src/setup/wizard.rs @@ -381,7 +381,7 @@ fn section_agent() -> (String, String, bool) { "", "claude: npm install -g @anthropic-ai/claude-code", "kiro: npm install -g @koryhutchison/kiro-cli", - "codex: npm install -g openai-codex (requires OpenAI API key)", + "codex: npm install -g @openai/codex @agentclientprotocol/codex-acp", "gemini: npm install -g @google/gemini-cli", "", "Make sure the agent is in your PATH before continuing.", @@ -518,7 +518,7 @@ fn print_next_steps(agent: &str, output_path: &Path, is_local: bool) { } "codex" => { cprintln!(C.cyan, " 1. Install Codex CLI + ACP adapter:"); - println!(" npm install -g @openai/codex @zed-industries/codex-acp"); + println!(" npm install -g @openai/codex @agentclientprotocol/codex-acp"); cprintln!(C.cyan, " 2. Authenticate:"); println!(" codex login --device-auth"); } diff --git a/docs/codex.md b/docs/codex.md index 832d5d4f1..56d71a762 100644 --- a/docs/codex.md +++ b/docs/codex.md @@ -1,6 +1,6 @@ # Codex -Codex uses the [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) adapter for ACP support. +Codex uses the [@agentclientprotocol/codex-acp](https://github.com/agentclientprotocol/codex-acp) adapter for ACP support. The recommended working directory for the Codex image is `/home/node`; this is also the container `HOME`, so Codex auth, sessions, generated images, and skills live under `/home/node/.codex/`. @@ -11,7 +11,14 @@ live under `/home/node/.codex/`. docker build -f Dockerfile.codex -t openab-codex:latest . ``` -The image installs `@zed-industries/codex-acp` and `@openai/codex` globally via npm. +The image installs `@agentclientprotocol/codex-acp` and `@openai/codex` +globally in the same npm transaction. The global Codex CLI keeps +`codex login --device-auth` available, while npm deduplicates the adapter's +compatible Codex dependency to the pinned CLI version. + +For containerized deployments where the outer container or VM is the security +boundary, set `[pool] default_config_options = { mode = "agent-full-access" }` +— see [ACP Modes and Migration](#acp-modes-and-migration). ## Helm Install @@ -21,7 +28,6 @@ helm install openab openab/openab \ --set agents.codex.discord.enabled=true \ --set agents.codex.discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string 'agents.codex.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ - --set agents.codex.command=codex-acp \ --set agents.codex.workingDir=/home/node \ --set image.tag=beta ``` @@ -51,7 +57,7 @@ To override a single agent's image instead of the global tag: ```toml [agent] -# command defaults from OPENAB_AGENT_COMMAND="codex" +# command defaults from the image's OPENAB_AGENT_COMMAND # Only override if you need non-default behavior ``` @@ -67,6 +73,78 @@ Follow the device code flow in your browser, then restart the pod: kubectl rollout restart deployment/openab-codex ``` +## ACP Modes and Migration + +The adapter exposes three ACP modes. The selected mode controls the sandbox +and approval policy for each ACP turn: + +| Mode | Sandbox | Approval policy | Network | +|------|---------|-----------------|---------| +| `read-only` | read-only | on-request | disabled | +| `agent` (adapter default) | workspace-write | on-request | disabled | +| `agent-full-access` (recommended for OpenAB deployments) | danger-full-access | never | enabled | + +The adapter defaults to `agent`. For OpenAB deployments the outer container or +VM is normally the intended security boundary, and Codex's inner sandbox needs +`bubblewrap` (user namespaces) that containers typically don't grant — so the +**recommended deployment default** is `agent-full-access`, set through the +standard ACP config option mechanism: + +```toml +[pool] +default_config_options = { mode = "agent-full-access" } +``` + +OpenAB sends this after `session/new` on the ACP session, so it is explicit, +visible in config, and overridable per deployment — nothing is baked into the +image. `agent-full-access` removes Codex's inner sandbox and approval prompts; +it can read or modify mounted files and use the container's network. Use it +only with a dedicated outer isolation boundary. Avoid host filesystem and +Docker socket mounts, and scope mounted credentials, persistent volumes, +service accounts, and network access to the agent's actual needs. Select +`agent` or `read-only` when those conditions are not met. + +> The adapter also honors an `INITIAL_AGENT_MODE` environment variable, but +> OpenAB spawns agents with a cleared environment, and packing it into +> `OPENAB_AGENT_COMMAND` via `/usr/bin/env` breaks configs that override +> `[agent].args` only. Prefer the `[pool]` mechanism above; if you need the +> env route, deliver it with `[agent] env = { INITIAL_AGENT_MODE = "…" }`. + +> [!WARNING] +> **Breaking change — mode IDs renamed.** The previous Zed adapter used `auto` +> and `full-access`; this adapter uses `agent` and `agent-full-access`. OpenAB +> does **not** translate the old values: a `[pool].default_config_options` +> entry like `mode = "full-access"` fails to apply on upgrade (OpenAB logs +> `failed to set default config option` and the session stays on the adapter's +> conservative `agent` default — it fails safe, never escalates). Update your +> config to the new IDs: +> +> ```text +> auto -> agent +> full-access -> agent-full-access +> ``` + +> [!WARNING] +> **Breaking change — `-c` CLI overrides are silently ignored.** The Zed +> adapter accepted Codex-style `-c key=value` arguments (e.g. +> `[agent] args = ["-c", "model=\"gpt-5.5\""]` to pin a model). This adapter's +> CLI only recognizes `--version`, `login`, and `cli` — anything else is +> ignored without error, so a carried-over model pin silently stops applying. +> Pin the model through the ACP config option instead: +> +> ```toml +> [pool] +> default_config_options = { mode = "agent-full-access", model = "gpt-5.5" } +> ``` + +Custom ACP clients that call `session/set_config_option` directly must also send +the new mode IDs. If canary validation finds a regression, roll back to the +previous OpenAB Codex image tag; existing Codex credentials and session data +remain under `/home/node/.codex/`. + +For preview-image validation, mode-matrix checks, evidence reporting, and +post-merge rollout, follow [Canary Testing Pull Requests](canary-tests.md). + ### Persisted Paths (PVC) | Path | Contents | @@ -166,7 +244,7 @@ itself, explicitly expose an upload token to the agent: ```toml [agent] -# command defaults from OPENAB_AGENT_COMMAND="codex" +# command defaults from the image's OPENAB_AGENT_COMMAND # Only override if you need non-default behavior env = { DISCORD_FILE_BOT_TOKEN = "${DISCORD_FILE_BOT_TOKEN}" } ``` @@ -204,7 +282,12 @@ Example user prompt after creating such a skill: Use $discord-imagegen-deliver to generate a warm hand-painted sky with birds and send it back to this Discord thread. ``` -## Approval Policy & Auto-review +## Direct Codex CLI Approval Policy & Auto-review + +The settings in this section apply to direct Codex CLI commands such as +`codex exec`. For OpenAB ACP turns, select an ACP mode as described in +[ACP Modes and Migration](#acp-modes-and-migration); the adapter supplies the +turn's sandbox and approval policy. Codex separates **when** to ask for approval (`approval_policy`) from **who** reviews the request (`approvals_reviewer`): @@ -214,9 +297,10 @@ reviews the request (`approvals_reviewer`): | `approval_policy` | `untrusted`, `on-failure` (deprecated), `on-request`, `granular`, `never` | When Codex must request approval before acting | | `approvals_reviewer` | `"user"` (default), `"auto_review"` | Who handles the approval — human or GPT-5.4 Thinking reviewer | -For OpenAB deployments, **Auto-review is the recommended mode**. OpenAB agents -run as long-lived background processes with no human watching the terminal, so -manual approval is impractical and `"never"` removes all guardrails. +For unattended direct CLI commands, **Auto-review is the recommended mode**. +OpenAB agents run as long-lived background processes with no human watching the +terminal, so manual approval is impractical and `"never"` removes all +guardrails. Enable Auto-review in `/home/node/.codex/config.toml`: @@ -290,8 +374,12 @@ This commonly happens in OpenAB deployments where Codex already runs inside an isolated container or VM — the outer runtime provides the desired isolation, so the inner sandbox is redundant. -**Solution — Disable Codex's inner sandbox** (recommended when the outer OpenAB -runtime already provides isolation): +**For ACP sessions**, set `agent-full-access` through +`[pool].default_config_options` as described in +[ACP Modes and Migration](#acp-modes-and-migration). + +**For direct Codex CLI commands**, disable Codex's inner sandbox when the outer +OpenAB runtime already provides isolation: ```toml # /home/node/.codex/config.toml @@ -313,7 +401,7 @@ approvals_reviewer = "auto_review" > and OpenAB agents have no terminal attached — every tool call hangs in > `in_progress` until openab's 1800 s hard timeout fires. Use > `approvals_reviewer = "auto_review"` (recommended, see -> [§Approval Policy](#approval-policy--auto-review)) or +> [§Direct Codex CLI Approval Policy](#direct-codex-cli-approval-policy--auto-review)) or > `approval_policy = "never"` for trusted and already-isolated pods (`"never"` > removes all per-call guardrails — the outer pod isolation is the only > remaining boundary). @@ -324,7 +412,7 @@ Or launch with: codex --sandbox danger-full-access ``` -Or seed via `kubectl cp` (see [above](#approval-policy--auto-review) for why +Or seed via `kubectl cp` (see [above](#direct-codex-cli-approval-policy--auto-review) for why ConfigMap mounts should not be used for `.codex/config.toml`): ```bash diff --git a/docs/config-reference.md b/docs/config-reference.md index 938c9fe3d..5657eab8e 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -207,6 +207,12 @@ command = "codex-acp" working_dir = "/home/node" env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } +# Recommended for containerized OpenAB deployments: the outer container is the +# security boundary; Codex's inner sandbox needs user namespaces containers +# typically don't grant. See docs/codex.md §ACP Modes and Migration. +[pool] +default_config_options = { mode = "agent-full-access" } + # Gemini CLI [agent] command = "gemini"