Remove codex-cli backend and migrate to Codex runtime

Remove the bundled codex-cli backend, migrate legacy codex-cli refs and runtime pins to the Codex app-server runtime, and update live/backend workflow coverage for the supported CLI lanes.
This commit is contained in:
Peter Steinberger 2026-05-14 10:07:18 +01:00 committed by GitHub
parent 66b98b7294
commit a0f35574d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 531 additions and 377 deletions

View file

@ -2154,27 +2154,11 @@ jobs:
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
# Keep the release-blocking CI lane on Codex API-key auth. The
# staged auth-file path remains supported for local maintainer
# reruns, but it can hang on stale subscription/session state in
# an otherwise healthy release run.
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
# Replace the staged config.toml with a minimal CI-safe config so
# the repo stays trusted for MCP/tool use without inheriting
# maintainer-local provider/profile overrides that do not exist
# inside CI.
# Codex's workspace-write sandbox relies on user namespaces that
# this Docker lane does not provide, so run Codex unsandboxed
# inside the already-isolated container to keep MCP cron/tool
# execution representative instead of failing on nested sandbox
# setup.
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
;;
live-codex-harness-docker)
# Keep CI on the API-key path for now. The staged Codex auth secret
@ -2395,14 +2379,11 @@ jobs:
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
;;
live-codex-harness-docker)
echo "OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key" >> "$GITHUB_ENV"

View file

@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
- Maintainer tooling: fail CI when pull requests add package patch files or pnpm patched dependencies, preserving the upstream-and-bump dependency workflow.
- Amazon Bedrock: externalize the Bedrock and Bedrock Mantle provider packages so core installs no longer pull AWS SDK dependencies unless those providers are installed.
- Plugins: externalize Slack, OpenShell sandbox, and Anthropic Vertex so their runtime dependency cones install only when those plugins are installed.
- Codex migration: remove the bundled `codex-cli` backend and repair legacy `codex-cli/*` model refs to the Codex app-server route on `openai/*`.
- Control UI/WebChat: add a persisted auto-scroll mode selector so users can keep the current near-bottom behavior, always follow streaming output, or turn automatic streaming scroll off and use the New messages button manually. Fixes #7648 and #81287. Thanks @BunsDev.
- ACP: add `acp.fallbacks` so ACP turns can try configured backup runtime backends when the primary backend is unavailable before any output is emitted. (#69542) Thanks @kaseonedge.
- Gateway/startup: add owner-level startup trace attribution for auth, plugin loading, lookup counts, and plugin sidecar services. (#81738) Thanks @samzong.

View file

@ -155,7 +155,7 @@ order and tells you what it chose:
- `OPENAI_API_KEY` -> `openai/gpt-5.5`
- `ANTHROPIC_API_KEY` -> `anthropic/claude-opus-4-7`
- Claude Code CLI -> `claude-cli/claude-opus-4-7`
- Codex CLI -> `codex-cli/gpt-5.5`
- Codex -> `openai/gpt-5.5` through the Codex app-server harness
If none are available, setup still writes the default workspace and leaves the
model unset. Install or log into Codex/Claude Code, or expose
@ -171,7 +171,6 @@ back to local runtimes already present on the machine:
- Claude Code CLI: `claude-cli/claude-opus-4-7`
- Codex app-server harness: `openai/gpt-5.5`
- Codex CLI: `codex-cli/gpt-5.5`
The model-assisted planner cannot mutate config directly. It must translate the
request into one of Crestodian's typed commands, then the normal approval and

View file

@ -97,6 +97,8 @@ This is the agent-facing decision tree:
`openai/<model>` with `openclaw doctor --fix`; doctor keeps the Codex auth
route by adding provider/model-scoped `agentRuntime.id: "codex"` where the
old model ref implied it.
Legacy **`codex-cli/*` model refs** repair to the same `openai/<model>` Codex
app-server route; OpenClaw no longer keeps a bundled Codex CLI backend.
5. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
ACP with `runtime: "acp"` and `agentId: "codex"`.
6. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
@ -180,6 +182,10 @@ Legacy refs such as `claude-cli/claude-opus-4-7` remain supported for
compatibility, but new config should keep the provider/model canonical and put
the execution backend in provider/model runtime policy.
Legacy `codex-cli/*` refs are different: doctor migrates them to `openai/*` so
they run through the Codex app-server harness instead of preserving a Codex CLI
backend.
`auto` mode is intentionally conservative for most providers. OpenAI agent
models are the exception: unset runtime and `auto` both resolve to the Codex
harness. Explicit PI runtime config remains an opt-in compatibility route for

View file

@ -41,9 +41,9 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
</Accordion>
<Accordion title="CLI runtimes">
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set provider/model runtime policy to `claude-cli`, `google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*` or `google/gemini-*`, then set provider/model runtime policy to `claude-cli` or `google-gemini-cli` when you want a local CLI backend.
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately.
Legacy `claude-cli/*` and `google-gemini-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately. Legacy `codex-cli/*` refs migrate to `openai/*` and use the Codex app-server route; OpenClaw no longer keeps a bundled Codex CLI backend.
</Accordion>
</AccordionGroup>

View file

@ -2,7 +2,7 @@
summary: "CLI backends: local AI CLI fallback with optional MCP tool bridge"
read_when:
- You want a reliable fallback when API providers fail
- You are running Codex CLI or other local AI CLIs and want to reuse them
- You are running local AI CLIs and want to reuse them
- You want to understand the MCP loopback bridge for CLI backend tool access
title: "CLI backends"
---
@ -31,11 +31,11 @@ thread/conversation binding, and persistent external coding sessions, use
## Beginner-friendly quick start
You can use Codex CLI **without any config** (the bundled OpenAI plugin
You can use Claude Code CLI **without any config** (the bundled Anthropic plugin
registers a default backend):
```bash
openclaw agent --message "hi" --model codex-cli/gpt-5.5
openclaw agent --message "hi" --model claude-cli/claude-sonnet-4-6
```
If your gateway runs under launchd/systemd and PATH is minimal, add just the
@ -46,8 +46,8 @@ command path:
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
},
},
@ -72,11 +72,11 @@ Add a CLI backend to your fallback list so it only runs when primary models fail
defaults: {
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["codex-cli/gpt-5.5"],
fallbacks: ["claude-cli/claude-sonnet-4-6"],
},
models: {
"anthropic/claude-opus-4-6": { alias: "Opus" },
"codex-cli/gpt-5.5": {},
"claude-cli/claude-sonnet-4-6": {},
},
},
},
@ -97,7 +97,7 @@ All CLI backends live under:
agents.defaults.cliBackends
```
Each entry is keyed by a **provider id** (e.g. `codex-cli`, `my-cli`).
Each entry is keyed by a **provider id** (e.g. `claude-cli`, `my-cli`).
The provider id becomes the left side of your model ref:
```
@ -111,9 +111,6 @@ The provider id becomes the left side of your model ref:
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
},
"my-cli": {
command: "my-cli",
args: ["--json"],
@ -149,7 +146,7 @@ The provider id becomes the left side of your model ref:
## How it works
1. **Selects a backend** based on the provider prefix (`codex-cli/...`).
1. **Selects a backend** based on the provider prefix (`claude-cli/...`).
2. **Builds a system prompt** using the same OpenClaw prompt + workspace context.
3. **Executes the CLI** with a session id (if supported) so history stays consistent.
The bundled `claude-cli` backend keeps a Claude stdio process alive per
@ -164,12 +161,6 @@ told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats
a new policy.
</Note>
The bundled OpenAI `codex-cli` backend passes OpenClaw's system prompt through
Codex's `model_instructions_file` config override (`-c
model_instructions_file="..."`). Codex does not expose a Claude-style
`--append-system-prompt` flag, so OpenClaw writes the assembled prompt to a
temporary file for each fresh Codex CLI session.
The bundled Anthropic `claude-cli` backend receives the OpenClaw skills snapshot
two ways: the compact OpenClaw skills catalog in the appended system prompt, and
a temporary Claude Code plugin passed with `--plugin-dir`. The plugin contains
@ -292,7 +283,7 @@ load local files from plain paths.
- `output: "json"` (default) tries to parse JSON and extract text + session id.
- For Gemini CLI JSON output, OpenClaw reads reply text from `response` and
usage from `stats` when `usage` is missing or empty.
- `output: "jsonl"` parses JSONL streams (for example Codex CLI `--json`) and extracts the final agent message plus session
- `output: "jsonl"` parses JSONL streams and extracts the final agent message plus session
identifiers when present.
- `output: "text"` treats stdout as the final response.
@ -304,16 +295,19 @@ Input modes:
## Defaults (plugin-owned)
The bundled OpenAI plugin also registers a default for `codex-cli`:
Bundled CLI backend defaults live with their owning plugin. For example,
Anthropic owns `claude-cli` and Google owns `google-gemini-cli`. OpenAI Codex
agent runs use the Codex app-server harness through `openai/*`; OpenClaw no
longer registers a bundled `codex-cli` backend.
- `command: "codex"`
- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","-c","sandbox_mode=\"workspace-write\"","--skip-git-repo-check"]`
The bundled Anthropic plugin registers a default for `claude-cli`:
- `command: "claude"`
- `args: ["-p","--output-format","stream-json","--include-partial-messages","--verbose", ...]`
- `output: "jsonl"`
- `resumeOutput: "text"`
- `input: "stdin"`
- `modelArg: "--model"`
- `imageArg: "--image"`
- `sessionMode: "existing"`
- `sessionMode: "always"`
The bundled Google plugin also registers a default for `google-gemini-cli`:
@ -383,9 +377,6 @@ opt into a generated MCP config overlay with `bundleMcp: true`.
Current bundled behavior:
- `claude-cli`: generated strict MCP config file
- `codex-cli`: inline config overrides for `mcp_servers`; the generated
OpenClaw loopback server is marked with Codex's per-server tool approval mode
so MCP calls cannot stall on local approval prompts
- `google-gemini-cli`: generated Gemini system settings file
When bundle MCP is enabled, OpenClaw:
@ -414,16 +405,13 @@ children and Streamable HTTP/SSE streams do not outlive the run.
- **Streaming is backend-specific.** Some backends stream JSONL; others buffer
until exit.
- **Structured outputs** depend on the CLI's JSON format.
- **Codex CLI sessions** resume via text output (no JSONL), which is less
structured than the initial `--json` run. OpenClaw sessions still work
normally.
## Troubleshooting
- **CLI not found**: set `command` to a full path.
- **Wrong model name**: use `modelAliases` to map `provider/model` → CLI model.
- **No session continuity**: ensure `sessionArg` is set and `sessionMode` is not
`none` (Codex CLI currently cannot resume with JSON output).
`none`.
- **Images ignored**: set `imageArg` (and verify CLI supports file paths).
## Related

View file

@ -461,8 +461,8 @@ Optional CLI backends for text-only fallback runs (no tool calls). Useful as a b
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
"my-cli": {
command: "my-cli",

View file

@ -133,7 +133,7 @@ openclaw models list --json
</Tip>
## Live: CLI backend smoke (Claude, Codex, Gemini, or other local CLIs)
## Live: CLI backend smoke (Claude, Gemini, or other local CLIs)
- Test: `src/gateway/gateway-cli-backend.live.test.ts`
- Goal: validate the Gateway + agent pipeline using a local CLI backend, without touching your default config.
@ -145,9 +145,9 @@ openclaw models list --json
- Default provider/model: `claude-cli/claude-sonnet-4-6`
- Command/args/image behavior come from the owning CLI backend plugin metadata.
- Overrides (optional):
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.5"`
- `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/codex"`
- `OPENCLAW_LIVE_CLI_BACKEND_ARGS='["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]'`
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-6"`
- `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"`
- `OPENCLAW_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json"]'`
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_PROBE=1` to send a real image attachment (paths are injected into the prompt). Docker recipes default this off unless explicitly requested.
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_ARG="--image"` to pass image file paths as CLI args instead of prompt injection.
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE="repeat"` (or `"list"`) to control how image args are passed when `IMAGE_ARG` is set.
@ -158,8 +158,8 @@ openclaw models list --json
Example:
```bash
OPENCLAW_LIVE_CLI_BACKEND=1 \
OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.5" \
OPENCLAW_LIVE_CLI_BACKEND=1 \
OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-6" \
pnpm test:live src/gateway/gateway-cli-backend.live.test.ts
```
@ -186,7 +186,6 @@ Single-provider Docker recipes:
```bash
pnpm test:docker:live-cli-backend:claude
pnpm test:docker:live-cli-backend:claude-subscription
pnpm test:docker:live-cli-backend:codex
pnpm test:docker:live-cli-backend:gemini
```
@ -194,9 +193,9 @@ Notes:
- The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`.
- It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user.
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code` or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- `pnpm test:docker:live-cli-backend:claude-subscription` requires portable Claude Code subscription OAuth through either `~/.claude/.credentials.json` with `claudeAiOauth.subscriptionType` or `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`. It first proves direct `claude -p` in Docker, then runs two Gateway CLI-backend turns without preserving Anthropic API-key env vars. This subscription lane disables the Claude MCP/tool and image probes by default because Claude currently routes third-party app usage through extra-usage billing instead of normal subscription plan limits.
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude, Codex, and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
- Claude's default smoke also patches the session from Sonnet to Opus and verifies the resumed session still remembers an earlier note.
## Live: APNs HTTP/2 proxy reachability

View file

@ -388,7 +388,7 @@ Prefer the narrowest metadata that already describes ownership. Use
when those fields express the relationship. Use `activation` for extra planner
hints that cannot be represented by those ownership fields.
Use top-level `cliBackends` for CLI runtime aliases such as `claude-cli`,
`codex-cli`, or `google-gemini-cli`; `activation.onAgentHarnesses` is only for
`my-cli`, or `google-gemini-cli`; `activation.onAgentHarnesses` is only for
embedded agent harness ids that do not already have an ownership field.
This block is metadata only. It does not register runtime behavior, and it does

View file

@ -320,9 +320,9 @@ descriptor-backed placeholders for parse-time lazy loading.
### CLI backend registration
`api.registerCliBackend(...)` lets a plugin own the default config for a local
AI CLI backend such as `codex-cli`.
AI CLI backend such as `claude-cli` or `my-cli`.
- The backend `id` becomes the provider prefix in model refs like `codex-cli/gpt-5`.
- The backend `id` becomes the provider prefix in model refs like `my-cli/gpt-5`.
- The backend `config` uses the same shape as `agents.defaults.cliBackends.<id>`.
- User config still wins. OpenClaw merges `agents.defaults.cliBackends.<id>` over the
plugin default before running the CLI.

View file

@ -75,7 +75,7 @@ PI runtime config remains available as an opt-in compatibility route. When PI is
explicitly selected with an `openai-codex` auth profile, OpenClaw keeps the
public model ref as `openai/*` and routes PI internally through the legacy
Codex-auth transport. Run `openclaw doctor --fix` to repair stale
`openai-codex/*` model refs or old PI session pins that do not come from
`openai-codex/*`, `codex-cli/*`, or old PI session pins that do not come from
explicit runtime config.
</Note>
@ -85,7 +85,7 @@ explicit runtime config.
| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Chat / Responses | `openai/<model>` model provider | Yes |
| Codex subscription models | `openai/<model>` with `openai-codex` OAuth | Yes |
| Legacy Codex model refs | `openai-codex/<model>` | Repaired by doctor to `openai/<model>` |
| Legacy Codex model refs | `openai-codex/<model>` or `codex-cli/<model>` | Repaired by doctor to `openai/<model>` |
| Codex app-server harness | `openai/<model>` with omitted runtime or provider/model `agentRuntime.id: codex` | Yes |
| Server-side web search | Native OpenAI Responses tool | Yes, when web search is enabled and no provider pinned |
| Images | `image_generate` | Yes |
@ -245,6 +245,7 @@ Choose your preferred auth method and follow the setup steps.
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or ordered `openai` auth profile |
| `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile |
| `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Existing `openai-codex` profile |
| `codex-cli/gpt-5.5` | repaired by doctor | Legacy CLI route rewritten to `openai/gpt-5.5` | Codex app-server auth |
<Warning>
Do not configure older `openai-codex/gpt-5.1*`, `openai-codex/gpt-5.2*`, or

View file

@ -43,7 +43,7 @@ title: "Tests"
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, state scenarios, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. If one lane exceeds the effective weight or resource cap on a low-parallelism host, it can still start from an empty pool and will run alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
- `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
- `pnpm test:docker:skill-install`: Installs the packed OpenClaw tarball in a bare Docker runner, disables `skills.install.allowUploadedArchives`, resolves a current skill slug from live ClawHub search, installs it through `openclaw skills install`, and verifies `SKILL.md`, `.clawhub/origin.json`, `.clawhub/lock.json`, and `skills info --json`.
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases.
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:claude`, `pnpm test:docker:live-cli-backend:claude:resume`, or `pnpm test:docker:live-cli-backend:claude:mcp`. Gemini has matching `:resume` and `:mcp` aliases.
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key, pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.

View file

@ -1,70 +0,0 @@
import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend";
import {
CLI_FRESH_WATCHDOG_DEFAULTS,
CLI_RESUME_WATCHDOG_DEFAULTS,
} from "openclaw/plugin-sdk/cli-backend";
const CODEX_CLI_DEFAULT_MODEL_REF = "codex-cli/gpt-5.5";
// Keep this in sync with MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION in the Codex plugin.
const CODEX_CLI_NPM_PACKAGE = "@openai/codex@0.130.0";
export function buildOpenAICodexCliBackend(): CliBackendPlugin {
return {
id: "codex-cli",
liveTest: {
defaultModelRef: CODEX_CLI_DEFAULT_MODEL_REF,
defaultImageProbe: true,
defaultMcpProbe: true,
docker: {
npmPackage: CODEX_CLI_NPM_PACKAGE,
binaryName: "codex",
},
},
bundleMcp: true,
bundleMcpMode: "codex-config-overrides",
nativeToolMode: "always-on",
config: {
command: "codex",
args: [
"exec",
"--json",
"--color",
"never",
"--sandbox",
"workspace-write",
"-c",
'service_tier="fast"',
"--skip-git-repo-check",
],
resumeArgs: [
"exec",
"resume",
"{sessionId}",
"-c",
'sandbox_mode="workspace-write"',
"-c",
'service_tier="fast"',
"--skip-git-repo-check",
],
output: "jsonl",
resumeOutput: "text",
input: "arg",
modelArg: "--model",
sessionIdFields: ["thread_id"],
sessionMode: "existing",
systemPromptFileConfigArg: "-c",
systemPromptFileConfigKey: "model_instructions_file",
systemPromptWhen: "first",
imageArg: "--image",
imageMode: "repeat",
imagePathScope: "workspace",
reliability: {
watchdog: {
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
},
},
serialize: true,
},
};
}

View file

@ -1,7 +1,6 @@
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { buildOpenAICodexCliBackend } from "./cli-backend.js";
import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
import {
openaiCodexMediaUnderstandingProvider,
@ -45,7 +44,6 @@ export default definePluginEntry({
});
},
});
api.registerCliBackend(buildOpenAICodexCliBackend());
api.registerProvider(buildProviderWithPromptContribution(buildOpenAIProvider()));
api.registerProvider(buildProviderWithPromptContribution(buildOpenAICodexProviderPlugin()));
api.registerMemoryEmbeddingProvider(openAiMemoryEmbeddingProviderAdapter);

View file

@ -756,7 +756,6 @@
}
]
},
"cliBackends": ["codex-cli"],
"providerAuthEnvVars": {
"openai": ["OPENAI_API_KEY"]
},

View file

@ -1,4 +1,3 @@
export { buildOpenAICodexCliBackend } from "./cli-backend.js";
export { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
export {
openaiCodexMediaUnderstandingProvider,

View file

@ -17,7 +17,6 @@ import {
OPENAI_CODEX_LOGIN_LABEL,
OPENAI_CODEX_WIZARD_GROUP,
} from "./auth-choice-copy.js";
import { buildOpenAICodexCliBackend } from "./cli-backend.js";
async function runOpenAIProviderAuthMethod(
methodId: string,
@ -159,6 +158,5 @@ export default definePluginEntry({
register(api) {
api.registerProvider(buildOpenAISetupProvider());
api.registerProvider(buildOpenAICodexSetupProvider());
api.registerCliBackend(buildOpenAICodexCliBackend());
},
});

View file

@ -1,4 +1,3 @@
export { buildOpenAICodexCliBackend } from "./cli-backend.js";
export { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
export {
openaiCodexMediaUnderstandingProvider,

View file

@ -1608,9 +1608,6 @@
"test:docker:live-cli-backend:claude-subscription": "OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 OPENCLAW_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG=1 OPENCLAW_LIVE_CLI_BACKEND_MODEL_SWITCH_PROBE=0 OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1 OPENCLAW_LIVE_CLI_BACKEND_IMAGE_PROBE=0 OPENCLAW_LIVE_CLI_BACKEND_MCP_PROBE=0 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:claude:mcp": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 OPENCLAW_LIVE_CLI_BACKEND_MCP_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:claude:resume": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:codex": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:codex:mcp": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4 OPENCLAW_LIVE_CLI_BACKEND_MCP_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:codex:resume": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4 OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:gemini": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3-flash-preview bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:gemini:mcp": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3-flash-preview OPENCLAW_LIVE_CLI_BACKEND_MCP_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
"test:docker:live-cli-backend:gemini:resume": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3-flash-preview OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1 bash scripts/test-live-cli-backend-docker.sh",
@ -1621,11 +1618,9 @@
"test:docker:live-subagent-announce": "bash scripts/test-live-subagent-announce-docker.sh",
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-gateway:claude": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=claude-cli OPENCLAW_LIVE_GATEWAY_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-gateway:codex": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=codex-cli OPENCLAW_LIVE_GATEWAY_MODELS=codex-cli/gpt-5.5 bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-gateway:gemini": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_GATEWAY_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-models": "bash scripts/test-live-models-docker.sh",
"test:docker:live-models:claude": "OPENCLAW_LIVE_PROVIDERS=claude-cli OPENCLAW_LIVE_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-models-docker.sh",
"test:docker:live-models:codex": "OPENCLAW_LIVE_PROVIDERS=codex-cli OPENCLAW_LIVE_MODELS=codex-cli/gpt-5.5 bash scripts/test-live-models-docker.sh",
"test:docker:live-models:gemini": "OPENCLAW_LIVE_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-models-docker.sh",
"test:docker:live:all": "OPENCLAW_DOCKER_ALL_LIVE_MODE=only node scripts/test-docker-all.mjs",
"test:docker:local:all": "OPENCLAW_DOCKER_ALL_LIVE_MODE=skip node scripts/test-docker-all.mjs",

View file

@ -161,7 +161,7 @@ function liveOpenAiChatToolsLane() {
export const mainLanes = [
liveLane("live-models", liveDockerScriptCommand("test-live-models-docker.sh"), {
providers: ["claude-cli", "codex-cli", "google-gemini-cli"],
providers: ["claude-cli", "google-gemini-cli"],
timeoutMs: LIVE_PROFILE_TIMEOUT_MS,
weight: 4,
}),
@ -169,11 +169,11 @@ export const mainLanes = [
"live-gateway",
liveDockerScriptCommand(
"test-live-gateway-models-docker.sh",
"OPENCLAW_IMAGE=openclaw:local-live-gateway OPENCLAW_DOCKER_BUILD_EXTENSIONS=matrix OPENCLAW_LIVE_GATEWAY_PROVIDERS=claude-cli,codex-cli,google-gemini-cli",
"OPENCLAW_IMAGE=openclaw:local-live-gateway OPENCLAW_DOCKER_BUILD_EXTENSIONS=matrix OPENCLAW_LIVE_GATEWAY_PROVIDERS=claude-cli,google-gemini-cli",
{ skipBuild: false },
),
{
providers: ["claude-cli", "codex-cli", "google-gemini-cli"],
providers: ["claude-cli", "google-gemini-cli"],
timeoutMs: LIVE_PROFILE_TIMEOUT_MS,
weight: 4,
},
@ -431,7 +431,7 @@ export const tailLanes = [
),
liveLane("live-codex-harness", liveDockerScriptCommand("test-live-codex-harness-docker.sh"), {
cacheKey: "codex-harness",
provider: "codex-cli",
provider: "openai",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
@ -455,7 +455,7 @@ export const tailLanes = [
),
{
cacheKey: "codex-harness",
provider: "codex-cli",
provider: "openai",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
@ -475,20 +475,6 @@ export const tailLanes = [
},
),
livePluginToolLane(),
liveLane(
"live-cli-backend-codex",
liveDockerScriptCommand(
"test-live-cli-backend-docker.sh",
"OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4",
),
{
cacheKey: "cli-backend-codex",
provider: "codex-cli",
resources: ["npm"],
timeoutMs: LIVE_CLI_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-claude",
liveDockerScriptCommand("test-live-acp-bind-docker.sh", "OPENCLAW_LIVE_ACP_BIND_AGENT=claude"),
@ -505,7 +491,7 @@ export const tailLanes = [
liveDockerScriptCommand("test-live-acp-bind-docker.sh", "OPENCLAW_LIVE_ACP_BIND_AGENT=codex"),
{
cacheKey: "acp-bind-codex",
provider: "codex-cli",
provider: "openai",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,

View file

@ -7,16 +7,28 @@ if (!provider) {
process.exit(1);
}
if (provider === "codex-cli") {
process.stdout.write(
JSON.stringify(
{
provider,
unsupported: true,
reason:
"codex-cli is no longer a bundled CLI backend. Use openai/* with the Codex app-server runtime instead.",
},
null,
2,
),
);
process.exit(0);
}
async function loadFallbackBackend(id: string) {
switch (id) {
case "claude-cli": {
const mod = await import("../extensions/anthropic/cli-backend.ts");
return mod.buildAnthropicCliBackend();
}
case "codex-cli": {
const mod = await import("../extensions/openai/cli-backend.ts");
return mod.buildOpenAICodexCliBackend();
}
case "google-gemini-cli": {
const mod = await import("../extensions/google/cli-backend.ts");
return mod.buildGoogleGeminiCliBackend();

View file

@ -33,15 +33,6 @@ DOCKER_TRUSTED_HARNESS_MOUNT=(-v "$TRUSTED_HARNESS_DIR":"$DOCKER_TRUSTED_HARNESS
if [[ -z "$CLI_PROVIDER" || "$CLI_PROVIDER" == "$CLI_MODEL" ]]; then
CLI_PROVIDER="$DEFAULT_PROVIDER"
fi
CLI_USE_CI_SAFE_CODEX_CONFIG="${OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG:-}"
if [[ -z "$CLI_USE_CI_SAFE_CODEX_CONFIG" ]]; then
if [[ "$CLI_PROVIDER" == "codex-cli" ]]; then
CLI_USE_CI_SAFE_CODEX_CONFIG="1"
else
CLI_USE_CI_SAFE_CODEX_CONFIG="0"
fi
fi
if [[ -f "$PROFILE_FILE" && -r "$PROFILE_FILE" ]]; then
set -a
# shellcheck disable=SC1090
@ -63,11 +54,9 @@ if [[ "$CLI_AUTH_MODE" == "subscription" && "$CLI_PROVIDER" != "claude-cli" ]];
exit 1
fi
if [[ "$CLI_AUTH_MODE" == "api-key" && "$CLI_PROVIDER" == "codex-cli" ]]; then
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "ERROR: OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key for codex-cli requires OPENAI_API_KEY." >&2
exit 1
fi
if [[ "$CLI_PROVIDER" == "codex-cli" ]]; then
echo "ERROR: codex-cli is no longer a bundled CLI backend. Use openai/* with the Codex app-server runtime instead." >&2
exit 1
fi
CLI_METADATA_JSON="$(node --import tsx "$ROOT_DIR/scripts/print-cli-backend-live-metadata.ts" "$CLI_PROVIDER")"
@ -187,9 +176,7 @@ fi
AUTH_DIRS=()
AUTH_FILES=()
if [[ "$CLI_AUTH_MODE" == "api-key" && "$CLI_PROVIDER" == "codex-cli" ]]; then
AUTH_FILES+=(".codex/config.toml")
elif [[ -n "${OPENCLAW_DOCKER_AUTH_DIRS:-}" ]]; then
if [[ -n "${OPENCLAW_DOCKER_AUTH_DIRS:-}" ]]; then
while IFS= read -r auth_dir; do
[[ -n "$auth_dir" ]] || continue
AUTH_DIRS+=("$auth_dir")
@ -285,10 +272,6 @@ provider="${OPENCLAW_DOCKER_CLI_BACKEND_PROVIDER:-claude-cli}"
default_command="${OPENCLAW_DOCKER_CLI_BACKEND_COMMAND_DEFAULT:-}"
docker_package="${OPENCLAW_DOCKER_CLI_BACKEND_NPM_PACKAGE:-}"
binary_name="${OPENCLAW_DOCKER_CLI_BACKEND_BINARY_NAME:-}"
if [ "$provider" = "codex-cli" ] && [ "${OPENCLAW_LIVE_CLI_BACKEND_AUTH:-auto}" != "api-key" ]; then
unset OPENAI_API_KEY
unset OPENAI_BASE_URL
fi
if [ -z "$binary_name" ] && [ -n "$default_command" ]; then
binary_name="$(basename "$default_command")"
fi
@ -310,13 +293,6 @@ if [ -n "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-}" ] && [ ! -x "${OPENCLAW_LIVE_CL
elif [ -n "$docker_package" ] && package_has_explicit_version "$docker_package"; then
run_setup_command npm install -g "$docker_package"
fi
if [ "$provider" = "codex-cli" ] && [ "${OPENCLAW_LIVE_CLI_BACKEND_AUTH:-auto}" = "api-key" ]; then
codex_login_command="${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-$NPM_CONFIG_PREFIX/bin/codex}"
if [ ! -x "$codex_login_command" ] && [ -x "$NPM_CONFIG_PREFIX/bin/codex" ]; then
codex_login_command="$NPM_CONFIG_PREFIX/bin/codex"
fi
printf '%s\n' "$OPENAI_API_KEY" | "$codex_login_command" login --with-api-key >/dev/null
fi
if [ -n "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-}" ] && [ -x "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}" ]; then
echo "==> CLI backend binary: ${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}"
"${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}" -V || "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}" --version || true
@ -403,42 +379,6 @@ openclaw_live_link_runtime_tree "$tmp_dir"
openclaw_live_stage_state_dir "$tmp_dir/.openclaw-state"
openclaw_live_prepare_staged_config
cd "$tmp_dir"
if [ "${OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG:-0}" = "1" ]; then
node --import tsx "$trusted_scripts_dir/prepare-codex-ci-config.ts" "$HOME/.codex/config.toml" "$tmp_dir"
fi
if [ "$provider" = "codex-cli" ] && [ "${OPENCLAW_LIVE_CLI_BACKEND_AUTH:-auto}" = "api-key" ]; then
codex_probe_model="${OPENCLAW_LIVE_CLI_BACKEND_MODEL#*/}"
codex_probe_token="OPENCLAW-CODEX-DIRECT-PROBE"
codex_probe_stdout="$tmp_dir/codex-direct-probe.stdout"
codex_probe_stderr="$tmp_dir/codex-direct-probe.stderr"
if ! timeout --foreground --kill-after=10s 180s \
"${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-codex}" \
exec \
--json \
--color \
never \
--sandbox \
danger-full-access \
-c \
'service_tier="fast"' \
--skip-git-repo-check \
--model \
"$codex_probe_model" \
"Reply exactly: $codex_probe_token" \
>"$codex_probe_stdout" 2>"$codex_probe_stderr" </dev/null; then
echo "ERROR: direct Codex CLI probe failed before OpenClaw gateway smoke." >&2
sed -n '1,120p' "$codex_probe_stdout" >&2 || true
sed -n '1,120p' "$codex_probe_stderr" >&2 || true
exit 1
fi
if ! grep -q "$codex_probe_token" "$codex_probe_stdout"; then
echo "ERROR: direct Codex CLI probe did not return expected token." >&2
sed -n '1,120p' "$codex_probe_stdout" >&2 || true
sed -n '1,120p' "$codex_probe_stderr" >&2 || true
exit 1
fi
echo "==> Direct Codex CLI probe ok"
fi
node scripts/test-live.mjs -- src/gateway/gateway-cli-backend.live.test.ts
EOF
@ -450,9 +390,6 @@ echo "==> Provider: $CLI_PROVIDER"
echo "==> Auth mode: $CLI_AUTH_MODE"
echo "==> Setup timeout: ${CLI_SETUP_TIMEOUT_SECONDS}s"
echo "==> Profile file: $PROFILE_STATUS"
if [[ "$CLI_PROVIDER" == "codex-cli" ]]; then
echo "==> CI-safe Codex config: $CLI_USE_CI_SAFE_CODEX_CONFIG"
fi
if [[ "$CLI_PROVIDER" == "claude-cli" && "$CLI_AUTH_MODE" == "subscription" ]]; then
echo "==> Claude subscription: $CLAUDE_SUBSCRIPTION_TYPE"
echo "==> Claude subscription source: $CLAUDE_SUBSCRIPTION_AUTH_SOURCE"
@ -462,18 +399,7 @@ echo "==> External auth files: ${AUTH_FILES_CSV:-none}"
DOCKER_AUTH_ENV=(
-e OPENCLAW_LIVE_CLI_BACKEND_AUTH="$CLI_AUTH_MODE"
)
if [[ "$CLI_PROVIDER" == "codex-cli" && "$CLI_AUTH_MODE" == "api-key" ]]; then
docker_env_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-cli-backend-env.XXXXXX")"
TEMP_DIRS+=("$docker_env_dir")
docker_env_file="$docker_env_dir/openai.env"
{
printf 'OPENAI_API_KEY=%s\n' "${OPENAI_API_KEY}"
if [[ -n "${OPENAI_BASE_URL:-}" ]]; then
printf 'OPENAI_BASE_URL=%s\n' "${OPENAI_BASE_URL}"
fi
} >"$docker_env_file"
DOCKER_EXTRA_ENV_FILES+=(--env-file "$docker_env_file")
elif [[ "$CLI_PROVIDER" == "claude-cli" && "$CLI_AUTH_MODE" == "subscription" ]]; then
if [[ "$CLI_PROVIDER" == "claude-cli" && "$CLI_AUTH_MODE" == "subscription" ]]; then
DOCKER_AUTH_ENV+=(
-e CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}"
-e OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV="$OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV"
@ -501,7 +427,6 @@ DOCKER_RUN_ARGS=(docker run --rm -t \
-e OPENCLAW_DOCKER_AUTH_FILES_RESOLVED="$AUTH_FILES_CSV" \
-e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts" \
-e OPENCLAW_LIVE_DOCKER_SOURCE_STAGE_MODE="${OPENCLAW_LIVE_DOCKER_SOURCE_STAGE_MODE:-copy}" \
-e OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG="$CLI_USE_CI_SAFE_CODEX_CONFIG" \
-e OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS="$CLI_SETUP_TIMEOUT_SECONDS" \
-e OPENCLAW_DOCKER_CLI_BACKEND_PROVIDER="$CLI_PROVIDER" \
-e OPENCLAW_DOCKER_CLI_BACKEND_COMMAND_DEFAULT="$CLI_DEFAULT_COMMAND" \

View file

@ -8,24 +8,53 @@ type LegacyRuntimeModelProviderAlias = {
legacyProvider: string;
/** Canonical provider id that should own model selection. */
provider: string;
/** Runtime/backend id that preserves the old execution behavior. */
/** Runtime/backend id selected for the migrated ref. */
runtime: string;
/** True when the runtime is a CLI backend rather than an embedded harness. */
cli: boolean;
/** True when doctor must write a runtime policy even if the target runtime is the default. */
requiresRuntimePolicy: boolean;
};
const LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES = [
{ legacyProvider: "codex", provider: "openai", runtime: "codex", cli: false },
{ legacyProvider: "codex-cli", provider: "openai", runtime: "codex-cli", cli: true },
{ legacyProvider: "claude-cli", provider: "anthropic", runtime: "claude-cli", cli: true },
{
legacyProvider: "codex",
provider: "openai",
runtime: "codex",
cli: false,
requiresRuntimePolicy: false,
},
{
legacyProvider: "codex-cli",
provider: "openai",
runtime: "codex",
cli: false,
requiresRuntimePolicy: true,
},
{
legacyProvider: "claude-cli",
provider: "anthropic",
runtime: "claude-cli",
cli: true,
requiresRuntimePolicy: true,
},
{
legacyProvider: "google-gemini-cli",
provider: "google",
runtime: "google-gemini-cli",
cli: true,
requiresRuntimePolicy: true,
},
] as const satisfies readonly LegacyRuntimeModelProviderAlias[];
export function legacyRuntimeModelAliasRequiresRuntimePolicy(provider: string): boolean {
return (
LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES.find(
(entry) => normalizeProviderId(entry.legacyProvider) === normalizeProviderId(provider),
)?.requiresRuntimePolicy === true
);
}
const LEGACY_ALIAS_BY_PROVIDER = new Map(
LEGACY_RUNTIME_MODEL_PROVIDER_ALIASES.map((entry) => [
normalizeProviderId(entry.legacyProvider),

View file

@ -241,10 +241,9 @@ describe("handleModelsCommand", () => {
expect(result?.reply?.text).not.toContain("- anthropic");
});
it("hides bare backwards-compat aliases but surfaces CLI runtime providers in /models lists", async () => {
it("hides bare backwards-compat aliases but surfaces supported CLI runtime providers in /models lists", async () => {
modelCatalogMocks.loadModelCatalog.mockResolvedValueOnce([
{ provider: "codex", id: "gpt-5.5", name: "GPT-5.5" },
{ provider: "codex-cli", id: "gpt-5.5", name: "GPT-5.5" },
{ provider: "claude-cli", id: "claude-opus-4-7", name: "Claude Opus" },
{ provider: "google-gemini-cli", id: "gemini-3.1-pro-preview", name: "Gemini Pro" },
{ provider: "anthropic", id: "claude-opus-4-7", name: "Claude Opus" },
@ -256,7 +255,6 @@ describe("handleModelsCommand", () => {
"google",
"openai",
"claude-cli",
"codex-cli",
"google-gemini-cli",
]);
@ -271,9 +269,9 @@ describe("handleModelsCommand", () => {
expect(result?.reply?.text).toContain("- google (1)");
expect(result?.reply?.text).toContain("- openai (1)");
expect(result?.reply?.text).toContain("- claude-cli (1)");
expect(result?.reply?.text).toContain("- codex-cli (1)");
expect(result?.reply?.text).toContain("- google-gemini-cli (1)");
expect(result?.reply?.text).not.toMatch(/^- codex \(/m);
expect(result?.reply?.text).not.toMatch(/^- codex-cli \(/m);
});
it("sources CLI runtime provider model lists from the catalog, not user agents.defaults.models", async () => {

View file

@ -204,6 +204,7 @@ vi.mock("../config/config.js", () => ({
}));
vi.mock("../config/paths.js", () => ({
resolveIsNixMode: () => false,
resolveStateDir: () => resolveStateDir(),
}));

View file

@ -629,7 +629,7 @@ describe("normalizeCompatibilityConfigValues", () => {
});
});
it("migrates legacy Codex CLI primary refs to OpenAI refs plus model runtime", () => {
it("migrates legacy Codex CLI primary refs to the Codex app-server route", () => {
const res = normalizeCompatibilityConfigValues({
agents: {
defaults: {
@ -652,16 +652,159 @@ describe("normalizeCompatibilityConfigValues", () => {
expect(res.config.agents?.defaults?.agentRuntime).toBeUndefined();
expect(res.config.agents?.defaults?.models).toEqual({
"codex-cli/gpt-5.5": { alias: "Codex CLI" },
"openai/gpt-5.5": {
alias: "OpenAI GPT",
agentRuntime: { id: "codex-cli" },
"openai/gpt-5.5": { alias: "OpenAI GPT", agentRuntime: { id: "codex" } },
"openai/gpt-5.4-mini": { agentRuntime: { id: "codex" } },
});
});
it("migrates legacy Codex CLI fallback refs when the primary is already canonical", () => {
const res = normalizeCompatibilityConfigValues({
agents: {
defaults: {
model: {
primary: "openai/gpt-5.5",
fallbacks: ["codex-cli/gpt-5.4"],
},
models: {
"codex-cli/gpt-5.4": { alias: "Legacy CLI fallback" },
},
},
},
"openai/gpt-5.4-mini": {
agentRuntime: { id: "codex-cli" },
} as unknown as OpenClawConfig);
expect(res.config.agents?.defaults?.model).toEqual({
primary: "openai/gpt-5.5",
fallbacks: ["openai/gpt-5.4"],
});
expect(res.config.agents?.defaults?.models).toEqual({
"codex-cli/gpt-5.4": { alias: "Legacy CLI fallback" },
"openai/gpt-5.4": {
alias: "Legacy CLI fallback",
agentRuntime: { id: "codex" },
},
});
});
it("migrates standalone legacy Codex CLI allowlist keys", () => {
const res = normalizeCompatibilityConfigValues({
agents: {
defaults: {
models: {
"codex-cli/gpt-5.4": { alias: "Legacy CLI fallback" },
},
},
},
} as unknown as OpenClawConfig);
expect(res.config.agents?.defaults?.models).toEqual({
"codex-cli/gpt-5.4": { alias: "Legacy CLI fallback" },
"openai/gpt-5.4": {
alias: "Legacy CLI fallback",
agentRuntime: { id: "codex" },
},
});
});
it("pins migrated Codex CLI refs to Codex when OpenAI uses a custom base URL", () => {
const res = normalizeCompatibilityConfigValues({
agents: {
defaults: {
model: "codex-cli/gpt-5.5",
},
},
models: {
providers: {
openai: {
baseUrl: "https://proxy.example/v1",
},
},
},
} as unknown as OpenClawConfig);
expect(res.config.agents?.defaults?.model).toBe("openai/gpt-5.5");
expect(res.config.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime).toEqual({
id: "codex",
});
});
it("migrates existing Codex CLI runtime pins to the Codex app-server runtime", () => {
const res = normalizeCompatibilityConfigValues({
agents: {
defaults: {
models: {
"openai/gpt-5.5": {
agentRuntime: { id: "codex-cli", mode: "strict" },
},
},
},
list: [
{
id: "reviewer",
models: {
"openai/gpt-5.4-mini": {
agentRuntime: { id: "codex-cli" },
},
},
},
],
},
models: {
providers: {
openai: {
agentRuntime: { id: "codex-cli" },
models: [
{
id: "gpt-5.5",
agentRuntime: { id: "codex-cli" },
},
],
},
},
},
} as unknown as OpenClawConfig);
expect(res.config.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime).toEqual({
id: "codex",
mode: "strict",
});
expect(res.config.agents?.list?.[0]?.models?.["openai/gpt-5.4-mini"]?.agentRuntime).toEqual({
id: "codex",
});
expect(res.config.models?.providers?.openai?.agentRuntime).toEqual({ id: "codex" });
expect(res.config.models?.providers?.openai?.models?.[0]?.agentRuntime).toEqual({
id: "codex",
});
expect(res.changes).toContain(
"Moved agents.defaults.models.openai/gpt-5.5 agentRuntime.id from codex-cli to codex.",
);
expect(res.changes).toContain(
"Moved agents.list.reviewer.models.openai/gpt-5.4-mini agentRuntime.id from codex-cli to codex.",
);
expect(res.changes).toContain(
"Moved models.providers.openai agentRuntime.id from codex-cli to codex.",
);
expect(res.changes).toContain(
"Moved models.providers.openai.models.gpt-5.5 agentRuntime.id from codex-cli to codex.",
);
});
it("migrates provider-scoped Codex CLI runtime pins without agents config", () => {
const res = normalizeCompatibilityConfigValues({
models: {
providers: {
openai: {
agentRuntime: { id: "codex-cli" },
},
},
},
} as unknown as OpenClawConfig);
expect(res.config.models?.providers?.openai?.agentRuntime).toEqual({ id: "codex" });
expect(res.changes).toContain(
"Moved models.providers.openai agentRuntime.id from codex-cli to codex.",
);
});
it("migrates legacy Gemini CLI primary refs to Google refs plus model runtime", () => {
const res = normalizeCompatibilityConfigValues({
agents: {

View file

@ -1,4 +1,7 @@
import { migrateLegacyRuntimeModelRef } from "../../../agents/model-runtime-aliases.js";
import {
legacyRuntimeModelAliasRequiresRuntimePolicy,
migrateLegacyRuntimeModelRef,
} from "../../../agents/model-runtime-aliases.js";
import { normalizeProviderId } from "../../../agents/provider-id.js";
import { resolveSingleAccountKeysToMove } from "../../../channels/plugins/setup-promotion-helpers.js";
import { resolveNormalizedProviderModelMaxTokens } from "../../../config/defaults.js";
@ -229,6 +232,18 @@ type ModelProviderEntry = Partial<
>;
type ModelsConfigPatch = Partial<NonNullable<OpenClawConfig["models"]>>;
type ModelDefinitionEntry = NonNullable<ModelProviderEntry["models"]>[number];
type SelectedRuntimeRef = {
ref: string;
runtime: string;
requiresRuntimePolicy: boolean;
};
const LEGACY_CODEX_CLI_RUNTIME_ID = "codex-cli";
const CODEX_APP_SERVER_RUNTIME_ID = "codex";
function migratedRuntimeRequiresPolicy(legacyProvider: string): boolean {
return legacyRuntimeModelAliasRequiresRuntimePolicy(legacyProvider);
}
function mergeModelEntry(legacyEntry: unknown, currentEntry: unknown): unknown {
if (!isRecord(legacyEntry) || !isRecord(currentEntry)) {
@ -237,11 +252,28 @@ function mergeModelEntry(legacyEntry: unknown, currentEntry: unknown): unknown {
return { ...legacyEntry, ...currentEntry };
}
function normalizeLegacyCodexCliAgentRuntimePolicy(raw: unknown): {
value?: unknown;
changed: boolean;
} {
if (!isRecord(raw)) {
return { value: raw, changed: false };
}
if (normalizeOptionalLowercaseString(raw.id) !== LEGACY_CODEX_CLI_RUNTIME_ID) {
return { value: raw, changed: false };
}
return {
value: { ...raw, id: CODEX_APP_SERVER_RUNTIME_ID },
changed: true,
};
}
function normalizeLegacyRuntimeAgentModelConfig(raw: unknown): {
value?: unknown;
changed: boolean;
selectedRuntime?: string;
selectedRefs: string[];
selectedRuntimeRequiresPolicy: boolean;
selectedRefs: SelectedRuntimeRef[];
} {
if (typeof raw === "string") {
const migrated = migrateLegacyRuntimeModelRef(raw);
@ -250,39 +282,72 @@ function normalizeLegacyRuntimeAgentModelConfig(raw: unknown): {
value: migrated.ref,
changed: true,
selectedRuntime: migrated.runtime,
selectedRefs: [migrated.ref],
selectedRuntimeRequiresPolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider),
selectedRefs: [
{
ref: migrated.ref,
runtime: migrated.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider),
},
],
}
: { value: raw, changed: false, selectedRefs: [] };
: { value: raw, changed: false, selectedRuntimeRequiresPolicy: false, selectedRefs: [] };
}
if (!isRecord(raw)) {
return { value: raw, changed: false, selectedRefs: [] };
return { value: raw, changed: false, selectedRuntimeRequiresPolicy: false, selectedRefs: [] };
}
const migratedPrimary =
typeof raw.primary === "string" ? migrateLegacyRuntimeModelRef(raw.primary) : null;
if (!migratedPrimary) {
return { value: raw, changed: false, selectedRefs: [] };
let changed = false;
const next: Record<string, unknown> = { ...raw };
const selectedRefs: SelectedRuntimeRef[] = [];
let selectedRuntime = migratedPrimary?.runtime;
let selectedRuntimeRequiresPolicy =
migratedPrimary !== null && migratedRuntimeRequiresPolicy(migratedPrimary.legacyProvider);
if (migratedPrimary) {
next.primary = migratedPrimary.ref;
selectedRefs.push({
ref: migratedPrimary.ref,
runtime: migratedPrimary.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migratedPrimary.legacyProvider),
});
changed = true;
}
const next: Record<string, unknown> = { ...raw, primary: migratedPrimary.ref };
const selectedRefs = [migratedPrimary.ref];
if (Array.isArray(raw.fallbacks)) {
next.fallbacks = raw.fallbacks.map((fallback) => {
if (typeof fallback !== "string") {
return fallback;
}
const migratedFallback = migrateLegacyRuntimeModelRef(fallback);
if (migratedFallback?.runtime === migratedPrimary.runtime) {
selectedRefs.push(migratedFallback.ref);
if (
migratedFallback &&
(migratedFallback.runtime === selectedRuntime ||
migratedFallback.legacyProvider === LEGACY_CODEX_CLI_RUNTIME_ID)
) {
selectedRuntime ??= migratedFallback.runtime;
selectedRuntimeRequiresPolicy ||= migratedRuntimeRequiresPolicy(
migratedFallback.legacyProvider,
);
selectedRefs.push({
ref: migratedFallback.ref,
runtime: migratedFallback.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migratedFallback.legacyProvider),
});
changed = true;
return migratedFallback.ref;
}
return fallback;
});
}
if (!changed) {
return { value: raw, changed: false, selectedRuntimeRequiresPolicy: false, selectedRefs: [] };
}
return {
value: next,
changed: true,
selectedRuntime: migratedPrimary.runtime,
selectedRuntime,
selectedRuntimeRequiresPolicy,
selectedRefs,
};
}
@ -309,56 +374,77 @@ function mergeModelEntryWithRuntimePolicy(
legacyEntry: unknown,
currentEntry: unknown,
runtime: string | undefined,
requiresRuntimePolicy = runtimeNeedsExplicitModelPolicy(runtime),
): unknown {
const merged = mergeModelEntry(legacyEntry, currentEntry);
return runtimeNeedsExplicitModelPolicy(runtime)
? modelEntryWithRuntimePolicy(merged, runtime)
: merged;
return runtime && requiresRuntimePolicy ? modelEntryWithRuntimePolicy(merged, runtime) : merged;
}
function normalizeLegacyRuntimeAllowlistModels(
rawModels: unknown,
selectedRuntime: string | undefined,
selectedRuntimeRequiresPolicy: boolean,
): {
value?: unknown;
changed: boolean;
} {
if (!selectedRuntime || !isRecord(rawModels)) {
if (!isRecord(rawModels)) {
return { value: rawModels, changed: false };
}
let changed = false;
const next: Record<string, unknown> = {};
const legacyEntries: Array<[string, unknown]> = [];
const legacyEntries: Array<{
migratedKey: string;
entry: unknown;
runtime: string;
requiresRuntimePolicy: boolean;
}> = [];
for (const [rawKey, entry] of Object.entries(rawModels)) {
const migrated = migrateLegacyRuntimeModelRef(rawKey);
if (migrated?.runtime === selectedRuntime) {
if (
migrated &&
(migrated.runtime === selectedRuntime ||
migrated.legacyProvider === LEGACY_CODEX_CLI_RUNTIME_ID)
) {
changed = true;
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
legacyEntries.push([migrated.ref, entry]);
legacyEntries.push({
migratedKey: migrated.ref,
entry,
runtime: migrated.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider),
});
continue;
}
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
}
for (const [migratedKey, entry] of legacyEntries) {
next[migratedKey] = mergeModelEntryWithRuntimePolicy(entry, next[migratedKey], selectedRuntime);
for (const { migratedKey, entry, runtime, requiresRuntimePolicy } of legacyEntries) {
next[migratedKey] = mergeModelEntryWithRuntimePolicy(
entry,
next[migratedKey],
runtime,
requiresRuntimePolicy || (runtime === selectedRuntime && selectedRuntimeRequiresPolicy),
);
}
return { value: next, changed };
}
function ensureSelectedModelRuntimePolicies(
rawModels: unknown,
selectedRefs: readonly string[],
selectedRuntime: string | undefined,
selectedRefs: readonly SelectedRuntimeRef[],
): { value?: unknown; changed: boolean } {
if (!runtimeNeedsExplicitModelPolicy(selectedRuntime) || selectedRefs.length === 0) {
if (selectedRefs.length === 0) {
return { value: rawModels, changed: false };
}
const next: Record<string, unknown> = isRecord(rawModels) ? { ...rawModels } : {};
let changed = false;
for (const ref of selectedRefs) {
for (const { ref, runtime, requiresRuntimePolicy } of selectedRefs) {
if (!requiresRuntimePolicy) {
continue;
}
const current = next[ref];
const updated = modelEntryWithRuntimePolicy(current, selectedRuntime);
const updated = modelEntryWithRuntimePolicy(current, runtime);
if (JSON.stringify(updated) !== JSON.stringify(current ?? {})) {
next[ref] = updated;
changed = true;
@ -367,6 +453,33 @@ function ensureSelectedModelRuntimePolicies(
return { value: next, changed };
}
function normalizeLegacyCodexCliRuntimePinsInModels(
rawModels: unknown,
path: string,
changes: string[],
): { value?: unknown; changed: boolean } {
if (!isRecord(rawModels)) {
return { value: rawModels, changed: false };
}
let changed = false;
const next: Record<string, unknown> = { ...rawModels };
for (const [modelRef, rawEntry] of Object.entries(rawModels)) {
if (!isRecord(rawEntry)) {
continue;
}
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(rawEntry.agentRuntime);
if (!runtime.changed) {
continue;
}
next[modelRef] = { ...rawEntry, agentRuntime: runtime.value };
changed = true;
changes.push(
`Moved ${path}.${sanitizeForLog(modelRef)} agentRuntime.id from codex-cli to codex.`,
);
}
return { value: next, changed };
}
function normalizeLegacyRuntimeAgentContainer(
raw: Record<string, unknown>,
path: string,
@ -387,7 +500,11 @@ function normalizeLegacyRuntimeAgentContainer(
);
}
const models = normalizeLegacyRuntimeAllowlistModels(raw.models, model.selectedRuntime);
const models = normalizeLegacyRuntimeAllowlistModels(
raw.models,
model.selectedRuntime,
model.selectedRuntimeRequiresPolicy,
);
if (models.changed) {
next.models = models.value;
changed = true;
@ -395,11 +512,7 @@ function normalizeLegacyRuntimeAgentContainer(
}
if (model.selectedRuntime) {
const modelRuntimes = ensureSelectedModelRuntimePolicies(
next.models,
model.selectedRefs,
model.selectedRuntime,
);
const modelRuntimes = ensureSelectedModelRuntimePolicies(next.models, model.selectedRefs);
if (modelRuntimes.changed) {
next.models = modelRuntimes.value;
changed = true;
@ -407,16 +520,95 @@ function normalizeLegacyRuntimeAgentContainer(
}
}
const codexCliRuntimePins = normalizeLegacyCodexCliRuntimePinsInModels(
next.models,
`${path}.models`,
changes,
);
if (codexCliRuntimePins.changed) {
next.models = codexCliRuntimePins.value;
changed = true;
}
return { value: next, changed };
}
function normalizeLegacyCodexCliProviderRuntimePins(
cfg: OpenClawConfig,
changes: string[],
): { config: OpenClawConfig; changed: boolean } {
const rawModels = cfg.models;
if (!isRecord(rawModels) || !isRecord(rawModels.providers)) {
return { config: cfg, changed: false };
}
let changed = false;
const nextProviders: Record<string, unknown> = { ...rawModels.providers };
for (const [providerId, rawProvider] of Object.entries(rawModels.providers)) {
if (!isRecord(rawProvider)) {
continue;
}
let providerChanged = false;
const nextProvider: Record<string, unknown> = { ...rawProvider };
const providerRuntime = normalizeLegacyCodexCliAgentRuntimePolicy(rawProvider.agentRuntime);
if (providerRuntime.changed) {
nextProvider.agentRuntime = providerRuntime.value;
providerChanged = true;
changes.push(
`Moved models.providers.${sanitizeForLog(providerId)} agentRuntime.id from codex-cli to codex.`,
);
}
if (Array.isArray(rawProvider.models)) {
const nextProviderModels = rawProvider.models.map((entry, index) => {
if (!isRecord(entry)) {
return entry;
}
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(entry.agentRuntime);
if (!runtime.changed) {
return entry;
}
providerChanged = true;
const modelId = normalizeOptionalString(entry.id) ?? `[${index}]`;
changes.push(
`Moved models.providers.${sanitizeForLog(providerId)}.models.${sanitizeForLog(modelId)} agentRuntime.id from codex-cli to codex.`,
);
return Object.assign({}, entry, { agentRuntime: runtime.value });
});
if (providerChanged) {
nextProvider.models = nextProviderModels;
}
}
if (providerChanged) {
nextProviders[providerId] = nextProvider;
changed = true;
}
}
return changed
? {
config: {
...cfg,
models: {
...rawModels,
providers: nextProviders as NonNullable<OpenClawConfig["models"]>["providers"],
},
},
changed: true,
}
: { config: cfg, changed: false };
}
export function normalizeLegacyRuntimeModelRefs(
cfg: OpenClawConfig,
changes: string[],
): OpenClawConfig {
const rawAgents = cfg.agents;
const providerPinned = normalizeLegacyCodexCliProviderRuntimePins(cfg, changes);
const cfgWithProviders = providerPinned.config;
const rawAgents = cfgWithProviders.agents;
if (!isRecord(rawAgents)) {
return cfg;
return cfgWithProviders;
}
let changed = false;
@ -452,12 +644,13 @@ export function normalizeLegacyRuntimeModelRefs(
}
}
return changed
const nextCfg = changed
? {
...cfg,
...cfgWithProviders,
agents: nextAgents as OpenClawConfig["agents"],
}
: cfg;
: cfgWithProviders;
return nextCfg;
}
export function normalizeLegacyOpenAICodexModelsAddMetadata(

View file

@ -5,7 +5,7 @@ const CRESTODIAN_CLAUDE_CLI_MODEL = "claude-opus-4-7";
const CRESTODIAN_CODEX_MODEL = "gpt-5.5";
type CrestodianLocalPlannerBackend = {
kind: "claude-cli" | "codex-app-server" | "codex-cli";
kind: "claude-cli" | "codex-app-server";
label: string;
runner: "cli" | "embedded";
provider: string;
@ -32,16 +32,6 @@ const CODEX_APP_SERVER_BACKEND: CrestodianLocalPlannerBackend = {
buildConfig: buildCodexAppServerPlannerConfig,
};
const CODEX_CLI_BACKEND: CrestodianLocalPlannerBackend = {
kind: "codex-cli",
label: `codex-cli/${CRESTODIAN_CODEX_MODEL}`,
runner: "cli",
provider: "codex-cli",
model: CRESTODIAN_CODEX_MODEL,
buildConfig: (workspaceDir) =>
buildCliPlannerConfig(workspaceDir, `codex-cli/${CRESTODIAN_CODEX_MODEL}`),
};
export function selectCrestodianLocalPlannerBackends(
overview: CrestodianOverview,
): CrestodianLocalPlannerBackend[] {
@ -50,7 +40,7 @@ export function selectCrestodianLocalPlannerBackends(
backends.push(CLAUDE_CLI_BACKEND);
}
if (overview.tools.codex.found) {
backends.push(CODEX_APP_SERVER_BACKEND, CODEX_CLI_BACKEND);
backends.push(CODEX_APP_SERVER_BACKEND);
}
return backends;
}

View file

@ -67,7 +67,7 @@ export function buildCrestodianAssistantUserPrompt(params: {
`Default model: ${params.overview.defaultModel ?? "not configured"}`,
`Config valid: ${params.overview.config.valid}`,
`Gateway reachable: ${params.overview.gateway.reachable}`,
`Codex CLI: ${params.overview.tools.codex.found ? "found" : "not found"}`,
`Codex binary: ${params.overview.tools.codex.found ? "found" : "not found"}`,
`Claude Code CLI: ${params.overview.tools.claude.found ? "found" : "not found"}`,
`OpenAI API key: ${params.overview.tools.apiKeys.openai ? "found" : "not found"}`,
`Anthropic API key: ${params.overview.tools.apiKeys.anthropic ? "found" : "not found"}`,

View file

@ -165,9 +165,9 @@ describe("Crestodian assistant", () => {
codex: { command: "codex", found: true },
}),
).map((backend) => backend.kind),
).toEqual(["claude-cli", "codex-app-server", "codex-cli"]);
).toEqual(["claude-cli", "codex-app-server"]);
const [codexAppServer, codexCli] = selectCrestodianLocalPlannerBackends(
const [codexAppServer] = selectCrestodianLocalPlannerBackends(
overview({
codex: { command: "codex", found: true },
}),
@ -182,13 +182,6 @@ describe("Crestodian assistant", () => {
expect(codexAppServerDefaults.workspace).toBe("/tmp/workspace");
expect(codexAppServerModel.primary).toBe("openai/gpt-5.5");
expect(codexAppServerCodexEntry.enabled).toBe(true);
const codexCliConfig = requireRecord(codexCli?.buildConfig("/tmp/workspace"));
const codexCliAgents = requireRecord(codexCliConfig.agents);
const codexCliDefaults = requireRecord(codexCliAgents.defaults);
const codexCliModel = requireRecord(codexCliDefaults.model);
expect(codexCliDefaults.workspace).toBe("/tmp/workspace");
expect(codexCliModel.primary).toBe("codex-cli/gpt-5.5");
});
it("falls back to Codex app-server when Claude CLI planning fails", async () => {
@ -242,14 +235,8 @@ describe("Crestodian assistant", () => {
expect(embeddedCodexEntry.enabled).toBe(true);
});
it("uses Codex CLI if the app-server planner is not usable", async () => {
const runCliAgent = vi.fn(async (params: RunCliAgentParams): Promise<EmbeddedPiRunResult> => {
if (params.provider === "codex-cli") {
return {
payloads: [{ text: '{"reply":"CLI fallback.","command":"models"}' }],
meta: { durationMs: 0 },
};
}
it("does not fall back to Codex CLI if the app-server planner is not usable", async () => {
const runCliAgent = vi.fn(async (): Promise<EmbeddedPiRunResult> => {
throw new Error("unexpected cli provider");
});
const runEmbeddedPiAgent = vi.fn(async () => {
@ -268,18 +255,9 @@ describe("Crestodian assistant", () => {
removeTempDir: async () => {},
},
});
if (result === null) {
throw new Error("Expected planner result");
}
expect(result.command).toBe("models");
expect(result.reply).toBe("CLI fallback.");
expect(result.modelLabel).toBe("codex-cli/gpt-5.5");
expect(result).toBeNull();
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(runCliAgent).toHaveBeenCalledTimes(1);
const firstCliCall = firstMockArg(runCliAgent);
expect(firstCliCall.provider).toBe("codex-cli");
expect(firstCliCall.model).toBe("gpt-5.5");
expect(firstCliCall.cleanupCliLiveSessionOnRunEnd).toBe(true);
expect(runCliAgent).not.toHaveBeenCalled();
});
});

View file

@ -112,7 +112,7 @@ const PLUGIN_UNINSTALL_RE =
const OPENAI_API_DEFAULT_MODEL_REF = `${DEFAULT_PROVIDER}/${DEFAULT_MODEL}`;
const ANTHROPIC_API_DEFAULT_MODEL_REF = "anthropic/claude-opus-4-7";
const CLAUDE_CLI_DEFAULT_MODEL_REF = "claude-cli/claude-opus-4-7";
const CODEX_CLI_DEFAULT_MODEL_REF = "codex-cli/gpt-5.5";
const CODEX_APP_SERVER_DEFAULT_MODEL_REF = "openai/gpt-5.5";
export function parseCrestodianOperation(input: string): CrestodianOperation {
const trimmed = input.trim();
@ -385,7 +385,7 @@ function chooseSetupModel(
return { model: CLAUDE_CLI_DEFAULT_MODEL_REF, source: "Claude Code CLI" };
}
if (overview.tools.codex.found) {
return { model: CODEX_CLI_DEFAULT_MODEL_REF, source: "Codex CLI" };
return { model: CODEX_APP_SERVER_DEFAULT_MODEL_REF, source: "Codex app-server" };
}
return { source: "none" };
}

View file

@ -114,22 +114,21 @@ describe("gateway cli backend live helpers", () => {
expect(shouldRunCliModelSwitchProbe("codex-cli", "codex-cli/gpt-5.5")).toBe(false);
});
it("configures legacy CLI model refs as canonical provider models plus CLI runtime", async () => {
it("rejects removed Codex CLI refs for live CLI backend selection", async () => {
const { resolveCliBackendLiveModelSelection } =
await import("./gateway-cli-backend.live-helpers.js");
expect(
expect(() =>
resolveCliBackendLiveModelSelection({
rawModel: "codex-cli/gpt-5.4",
defaultProvider: "claude-cli",
}),
).toEqual({
providerId: "codex-cli",
cliModelKey: "codex-cli/gpt-5.4",
configModelKey: "openai/gpt-5.4",
configModelSwitchTarget: undefined,
agentRuntime: { id: "codex-cli" },
});
).toThrow(/codex-cli\/\.\.\. is no longer supported/u);
});
it("configures legacy CLI model refs as canonical provider models plus CLI runtime", async () => {
const { resolveCliBackendLiveModelSelection } =
await import("./gateway-cli-backend.live-helpers.js");
expect(
resolveCliBackendLiveModelSelection({

View file

@ -73,6 +73,11 @@ export function resolveCliBackendLiveModelSelection(params: {
}
const migrated = migrateLegacyRuntimeModelRef(params.rawModel);
if (migrated?.legacyProvider === "codex-cli") {
throw new Error(
"OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/... is no longer supported. Use a supported CLI backend such as claude-cli or google-gemini-cli.",
);
}
if (migrated?.cli) {
return {
providerId: migrated.runtime,

View file

@ -164,7 +164,7 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
origin: "bundled",
enabledByDefault: true,
providers: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
cliBackends: [],
contracts: {
imageGenerationProviders: ["openai"],
videoGenerationProviders: ["openai"],

View file

@ -196,7 +196,7 @@ describe("loadPluginLookUpTable", () => {
},
},
},
cliBackends: ["codex-cli"],
cliBackends: [],
setup: {
providers: [{ id: "openai" }],
},
@ -248,7 +248,7 @@ describe("loadPluginLookUpTable", () => {
expect(table.owners.providers.get("openai")).toEqual(["openai"]);
expect(table.owners.modelCatalogProviders.get("openai")).toEqual(["openai"]);
expect(table.owners.modelCatalogProviders.get("azure-openai-responses")).toEqual(["openai"]);
expect(table.owners.cliBackends.get("codex-cli")).toEqual(["openai"]);
expect(table.owners.cliBackends.get("codex-cli")).toBeUndefined();
expect(table.owners.setupProviders.get("openai")).toEqual(["openai"]);
expect(table.owners.commandAliases.get("telegram-send")).toEqual(["telegram"]);
expect(table.owners.contracts.get("tools")).toEqual(["telegram"]);

View file

@ -86,7 +86,6 @@ function setOwningProviderManifestPlugins() {
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
modelSupport: {
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
},
@ -111,7 +110,6 @@ function setOwningProviderManifestPluginsWithWorkspace() {
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
modelSupport: {
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
},
@ -516,7 +514,7 @@ describe("resolvePluginProviders", () => {
setOwningProviderManifestPlugins();
expectOwningPluginIds("claude-cli", ["anthropic"]);
expectOwningPluginIds("codex-cli", ["openai"]);
expectOwningPluginIds("codex-cli");
});
it("reflects provider ownership manifest changes on the next lookup", () => {

View file

@ -94,8 +94,8 @@ describe("docker build helper", () => {
expect(liveCliBackend).toContain(
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"',
);
expect(liveCliBackend).toContain("direct Codex CLI probe failed before OpenClaw gateway smoke");
expect(liveCliBackend).toContain("==> Direct Codex CLI probe ok");
expect(liveCliBackend).toContain("codex-cli is no longer a bundled CLI backend");
expect(liveCliBackend).not.toContain("==> Direct Codex CLI probe ok");
expect(liveCliBackend).not.toContain(
'echo "==> Reuse live-test image: $LIVE_IMAGE_NAME (OPENCLAW_SKIP_DOCKER_BUILD=1)"',
);

View file

@ -115,7 +115,9 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("update-channel-switch skill-install update-corrupt-plugin");
expect(workflow).toContain("update-corrupt-plugin upgrade-survivor");
expect(workflow).toContain("published-upgrade-survivor");
expect(workflow).toContain("published-upgrade-survivor root-managed-vps-upgrade update-restart-auth");
expect(workflow).toContain(
"published-upgrade-survivor root-managed-vps-upgrade update-restart-auth",
);
expect(workflow).toContain("plugins-offline plugin-update");
expect(workflow).toContain("include_release_path_suites=true");
expect(workflow).not.toContain("telegram_mode requires source=npm");
@ -382,10 +384,12 @@ describe("package artifact reuse", () => {
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter");
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai");
expect(workflow).toContain("inputs.live_suite_filter == 'live-gateway-advisory-docker'");
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4");
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6");
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key");
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1");
expect((workflow.match(/service_tier=\\"fast\\"/g) ?? []).length).toBeGreaterThanOrEqual(2);
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1");
expect(workflow).not.toContain('service_tier=\\"fast\\"');
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_ARGS=");
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=");
expect(workflow).not.toContain(
'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","--skip-git-repo-check"]',
);