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
+25 -8
View File
@@ -527,13 +527,17 @@ const paths = {
get: {
tags: ["CodexConfig"],
summary: "List installed plugins",
description: "Reads live from `codex plugin list --json`.",
description:
"Reads live from `codex plugin list --json` and unwraps its `{installed, available}` " +
"object down to the `installed` array — the route's `plugins` field is always a list.",
operationId: "codexConfigGetPlugins",
responses: {
200: {
description: "Plugins as reported by the CLI.",
description: "Installed plugins as reported by the CLI.",
content: {
"application/json": { schema: { type: "object", properties: { plugins: {} } } },
"application/json": {
schema: { type: "object", properties: { plugins: { type: "array", items: {} } } },
},
},
},
422: cliExitResponse,
@@ -547,13 +551,20 @@ const paths = {
get: {
tags: ["CodexConfig"],
summary: "List registered marketplaces",
description: "Reads live from `codex plugin marketplace list --json`.",
description:
"Reads live from `codex plugin marketplace list --json` and unwraps its " +
"`{marketplaces}` object down to a bare array.",
operationId: "codexConfigGetMarketplaces",
responses: {
200: {
description: "Marketplaces as reported by the CLI.",
content: {
"application/json": { schema: { type: "object", properties: { marketplaces: {} } } },
"application/json": {
schema: {
type: "object",
properties: { marketplaces: { type: "array", items: {} } },
},
},
},
},
422: cliExitResponse,
@@ -678,14 +689,17 @@ const paths = {
post: {
tags: ["CodexConfig"],
summary: "Register a marketplace",
description: "Runs `codex plugin marketplace add <url>`.",
description:
"Runs `codex plugin marketplace add <source>`. `source` is an owner/repo shorthand, " +
"an http(s) git URL, or an absolute local path — the same set `claude plugin " +
"marketplace add` accepts; ssh remotes are not.",
operationId: "codexConfigPostMarketplacesAdd",
requestBody: {
required: true,
content: {
"application/json": {
schema: { type: "object", required: ["url"], properties: { url: { type: "string" } } },
example: { url: "https://example.com/marketplace.json" },
example: { url: "openai/codex" },
},
},
},
@@ -698,7 +712,10 @@ const paths = {
},
},
},
400: errorResponse("EBADARG", "marketplace url must be a valid http(s) URL"),
400: errorResponse(
"EBADARG",
"source must be owner/repo, an http(s) URL, or an absolute path"
),
422: cliExitResponse,
},
},