fix(codex-config): close whole-branch review defects in the Codex panel

Plugins/marketplaces GET routes were silently empty because codex-cli.js
never unwrapped the CLI's {installed,available}/{marketplaces} objects
into arrays; backup writes bypassed the symlink-escape guard entirely;
readFileSafe was stricter than every other Codex reader, showing a
symlinked skill/rule in the list and then refusing to open it;
marketplaceAdd only accepted http(s) URLs, rejecting owner/repo sources
the CLI itself takes. Also: docs drift, a dead i18n key, and a scope-cast
hazard in the shared Editor/ConfirmDelete modals.

Fixes, one per finding: unwrap installed/marketplaces in codex-cli.js so
every list route keeps returning a bare list; guard the backup path in
codex-mutate.js with the same assertNotSymlinkEscape the write path
already used (Claude's cc-mutate.js has no such guard on its primary
write path either, so left untouched there); relax readFileSafe's
containment to lexical-only, matching the other four Codex readers,
while leaving the write side's realpath check untouched; reuse
cc-plugin-cli's assertSource for marketplaceAdd instead of a third
validator (ssh remains out of scope, no credential story for it yet).

Server: 1557/1557 (was 1548). Client: tsc -b clean, 469/469 tests.
This commit is contained in:
2026-09-09 01:17:06 +07:00
parent fef3e640d0
commit acff059e92
16 changed files with 359 additions and 108 deletions
+10 -1
View File
@@ -1646,7 +1646,7 @@ POST /api/codex-config/marketplaces/action Body: { name, action: remove }
`type` is one of `skills`, `agents`, `rules`, `memory`, `config`. `name` is required for `skills`/`agents`/`rules` and forbidden for `memory`/`config` (each is a single fixed file per scope — `AGENTS.md` and `config.toml` respectively). `PUT`/`DELETE` always back the previous content up first (`server/lib/codex-mutate.js`) and always resolve `project` scope against the **dashboard server's own cwd** — unlike the read routes, there is no `?cwd=` override on a write, for the same reason `cc-config.js`'s `PATCH /settings` refuses one. Mutation errors: `EBADTYPE`/`EBADSCOPE`/`EBADNAME`/`EBADCONTENT`/`EOUTOFROOT` (400), `ETOOLARGE` (413), `ENOTFOUND` (404). Every successful write/delete broadcasts `codex_config_changed`.
`GET /mcp`, `/plugins`, `/marketplaces` proxy `codex mcp list --json` / `codex plugin list --json` / `codex marketplace list --json`; a fresh install with none of a kind returns `{ mcp: [] }` etc. rather than a parse error, and the client further coerces a non-array response to `[]` defensively. The add/remove routes shell out to `codex …` with an explicit argv array (never a shell string); a non-zero CLI exit is reported as **422 `ECLIEXIT`** with `stdout`/`stderr` preserved, never a 500. `POST /mcp/add` takes either `{ name, url, bearerTokenEnvVar? }` (HTTP transport) or `{ name, command, args?, env? }` (stdio transport) — never both, mirroring `codex mcp add`'s own mutually-exclusive flags. Other CLI errors: `EBADARG` (400), `ECLINOTFOUND` (503 — the `codex` binary isn't on `PATH`), `ETIMEOUT` (504).
`GET /mcp`, `/plugins`, `/marketplaces` proxy `codex mcp list --json` / `codex plugin list --json` / `codex plugin marketplace list --json`; a fresh install with none of a kind returns `{ mcp: [] }` etc. rather than a parse error. Unlike `mcp list --json` (a bare array), `plugin list --json` and `plugin marketplace list --json` return `{installed, available}` / `{marketplaces}` objects — `server/lib/codex-cli.js` unwraps those down to a plain array before the route ever sees them, so every list route's contract stays "the named field is a list"; the client's `Array.isArray` coercion is only a defensive fallback for CLI output that fails to parse at all. The add/remove routes shell out to `codex …` with an explicit argv array (never a shell string); a non-zero CLI exit is reported as **422 `ECLIEXIT`** with `stdout`/`stderr` preserved, never a 500. `POST /mcp/add` takes either `{ name, url, bearerTokenEnvVar? }` (HTTP transport) or `{ name, command, args?, env? }` (stdio transport) — never both, mirroring `codex mcp add`'s own mutually-exclusive flags. `POST /marketplaces/add`'s `url` field is misnamed for history but accepts the same source shapes as `codex plugin marketplace add`: `owner/repo`, an http(s) git URL, or an absolute local path (ssh remotes are out of scope). Other CLI errors: `EBADARG` (400), `ECLINOTFOUND` (503 — the `codex` binary isn't on `PATH`), `ETIMEOUT` (504).
Because `CodexMcpServer`/`CodexPlugin`/`CodexMarketplace` are opaque JSON blobs from the CLI (`[key: string]: unknown`) rather than a shape the dashboard controls, the Codex tab renders them as generic key/value cards instead of the richly-typed plugin/MCP cards the Claude Code tab shows.
@@ -1868,6 +1868,15 @@ Broadcast whenever Claude Code configuration changes — either by dashboard mut
{ "type": "cc_config_changed", "data": { "source": "fs", "paths": ["/Users/foo/.claude/settings.json"] } }
```
#### codex_config_changed
The Codex counterpart of `cc_config_changed` — broadcast by `routes/codex-config.js` after every successful mutation: `PUT`/`DELETE /api/codex-config/file`, and the CLI-backed `mcp/add`, `mcp/remove`, `plugins/action`, `marketplaces/add`, `marketplaces/action` routes. Always `source: "dashboard"` — there is no filesystem watcher on `$CODEX_HOME` (unlike the Claude side's `cc-watcher.js`). The Codex Config page subscribes and refetches automatically.
```json
{ "type": "codex_config_changed", "data": { "source": "dashboard", "action": "write", "scope": "user", "type": "rules", "name": "default" } }
{ "type": "codex_config_changed", "data": { "source": "dashboard", "action": "add", "type": "mcp", "name": "docs" } }
```
#### remote_data.updated
Broadcast once per successful remote sync (background poller, manual **Sync now**, or immediate pull after add/re-enable). Clients use this — and the per-session `session_created` / `session_updated` frames emitted in the same pass — to refetch sessions, costs, and analytics without polling.