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
+8 -7
View File
@@ -159,8 +159,9 @@ export function CodexConfig() {
data: CodexFileResponse | null;
error: string | null;
} | null>(null);
const [editor, setEditor] = useState<EditorState<CodexArtifactType>>(null);
const [confirmDelete, setConfirmDelete] = useState<ConfirmDeleteState<CodexArtifactType>>(null);
const [editor, setEditor] = useState<EditorState<CodexArtifactType, "user" | "project">>(null);
const [confirmDelete, setConfirmDelete] =
useState<ConfirmDeleteState<CodexArtifactType, "user" | "project">>(null);
const [toast, setToast] = useState<Toast>(null);
useEffect(() => {
@@ -299,12 +300,12 @@ export function CodexConfig() {
const handleSave = useCallback(
async (args: {
type: CodexArtifactType;
targetScope: "user" | "project" | "auto-memory";
targetScope: "user" | "project";
name: string | undefined;
content: string;
}) => {
const result: CodexMutationResult = await api.codexConfig.write({
scope: args.targetScope as "user" | "project",
scope: args.targetScope,
type: args.type,
name: args.name,
content: args.content,
@@ -325,7 +326,7 @@ export function CodexConfig() {
if (!confirmDelete) return;
try {
const result = await api.codexConfig.remove({
scope: confirmDelete.scope as "user" | "project",
scope: confirmDelete.scope,
type: confirmDelete.type,
name: confirmDelete.name,
});
@@ -413,7 +414,7 @@ export function CodexConfig() {
)}
{viewer && <FileViewer state={viewer} onClose={() => setViewer(null)} />}
{editor && (
<EditorModal<CodexArtifactType>
<EditorModal<CodexArtifactType, "user" | "project">
state={editor}
fetchContent={(p) => api.codexConfig.file(p).then((r) => r.text)}
onClose={() => setEditor(null)}
@@ -422,7 +423,7 @@ export function CodexConfig() {
/>
)}
{confirmDelete && (
<ConfirmDeleteModal<CodexArtifactType>
<ConfirmDeleteModal<CodexArtifactType, "user" | "project">
state={confirmDelete}
onCancel={() => setConfirmDelete(null)}
onConfirm={handleDelete}