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
+5 -4
View File
@@ -97,10 +97,11 @@ router.get("/config", (req, res) => {
// GET /api/codex-config/file?path=/abs/path — return body of a single file.
// Path must resolve under CODEX_HOME, the project .codex dir, or (only for a
// file literally named AGENTS.md) the project root. codexDiscovery.readFileSafe
// re-checks containment with the same leaf/ancestor symlink-escape guard the
// write side uses (codex-mutate.js's assertNotSymlinkEscape) — a plain
// string-only path check would let a symlink dropped under the root read
// arbitrary files outside it.
// checks that containment lexically — a symlink whose LOCATION is inside a
// root is readable even when it resolves outside, matching the other four
// discovery readers. The write side (codex-mutate.js) keeps the stricter
// realpath-based assertNotSymlinkEscape — reads and writes intentionally
// disagree on symlinks; see readFileSafe's doc comment for why.
router.get("/file", (req, res) => {
const p = req.query.path;
if (typeof p !== "string" || !p) {