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
@@ -11,25 +11,33 @@ import { useTranslation } from "react-i18next";
import { ShieldAlert, Trash2 } from "lucide-react";
import type { CcArtifactType } from "../../lib/api";
export type ConfirmDeleteState<TType extends string = CcArtifactType> = {
/** See EditorModal.tsx's `DefaultScope` — same reasoning: Codex instantiates
* `TScope` as `"user" | "project"` instead of casting into Claude's wider
* default. */
export type ConfirmDeleteState<
TType extends string = CcArtifactType,
TScope extends string = "user" | "project" | "auto-memory",
> = {
type: TType;
scope: "user" | "project" | "auto-memory";
scope: TScope;
name?: string;
path: string;
project?: string; // set for type === "auto-memory"
} | null;
interface ConfirmDeleteModalProps<TType extends string = CcArtifactType> {
state: NonNullable<ConfirmDeleteState<TType>>;
interface ConfirmDeleteModalProps<
TType extends string = CcArtifactType,
TScope extends string = "user" | "project" | "auto-memory",
> {
state: NonNullable<ConfirmDeleteState<TType, TScope>>;
onCancel: () => void;
onConfirm: () => Promise<void>;
}
export function ConfirmDeleteModal<TType extends string = CcArtifactType>({
state,
onCancel,
onConfirm,
}: ConfirmDeleteModalProps<TType>) {
export function ConfirmDeleteModal<
TType extends string = CcArtifactType,
TScope extends string = "user" | "project" | "auto-memory",
>({ state, onCancel, onConfirm }: ConfirmDeleteModalProps<TType, TScope>) {
const { t } = useTranslation("ccConfig");
const [busy, setBusy] = useState(false);