diff --git a/src/codex/prompt-layers.ts b/src/codex/prompt-layers.ts index 1e00afaa89..57ec8bde76 100644 --- a/src/codex/prompt-layers.ts +++ b/src/codex/prompt-layers.ts @@ -1028,7 +1028,10 @@ export function writeBaseVariant( ? input.id : input.id ?? newBaseVariantId(existing); if (!BASE_VARIANT_ID.test(targetId)) return { ok: false, error: "unknown_layer", detail: targetId }; - if (deleting && !existing.some(v => v.id === targetId)) { + // A caller-supplied id is an edit (or delete), never an alternate create path. + // Requiring it to exist keeps the generated-id path as the sole place where a + // new variant can enter, and therefore makes the cap impossible to bypass. + if (input.id !== null && !existing.some(v => v.id === targetId)) { return { ok: false, error: "unknown_layer", detail: targetId }; } if (!deleting && input.id === null && existing.length >= MAX_BASE_VARIANTS) { diff --git a/src/server/management/codex-prompt-routes.ts b/src/server/management/codex-prompt-routes.ts index 82a4b42540..0280310b34 100644 --- a/src/server/management/codex-prompt-routes.ts +++ b/src/server/management/codex-prompt-routes.ts @@ -397,7 +397,11 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise MAX_BODY_BYTES) { + return fail(ctx, "body_too_large", 400, `base variant exceeds ${MAX_BODY_BYTES} bytes`); + } + return settle(ctx, writeBaseVariant({ id, title: body.title, body: normalized }, revision, paths(ctx))); } if (url.pathname === "/api/codex-prompt/adopt" && req.method === "POST") { diff --git a/tests/codex-prompt-base-variants.test.ts b/tests/codex-prompt-base-variants.test.ts index 4371675a70..653aad519e 100644 --- a/tests/codex-prompt-base-variants.test.ts +++ b/tests/codex-prompt-base-variants.test.ts @@ -177,6 +177,15 @@ describe("base variant selection", () => { expect(writeBaseVariant({ id, title: "edited", body: "b2" }, rev(paths), paths).ok).toBe(true); }); + test("a caller-supplied id cannot create a variant", () => { + const paths = fixture("model = \"x\"\n"); + expect(writeBaseVariant({ id: "aaaaaa", title: "Injected", body: "b" }, rev(paths), paths)).toMatchObject({ + ok: false, + error: "unknown_layer", + }); + expect(readBaseVariants(paths)).toEqual([]); + }); + test("a stale revision is refused", () => { const paths = fixture("model = \"x\"\n"); const stale = rev(paths); diff --git a/tests/codex-prompt-route.test.ts b/tests/codex-prompt-route.test.ts index 31f0dfd421..52d9f7c8c8 100644 --- a/tests/codex-prompt-route.test.ts +++ b/tests/codex-prompt-route.test.ts @@ -637,6 +637,20 @@ describe("020 coverage completions", () => { expect(editDefault.status).toBe(400); expect(editDefault.body.code).toBe("unknown_layer"); + // A string id is edit-only. It cannot bypass the creation cap by naming a + // syntactically valid file that the server did not generate. + const editMissing = await call("PUT", "/api/codex-prompt/base", fx, { + id: "aaaaaa", title: "Missing", body: "b", revision: rev0, + }); + expect(editMissing.status).toBe(400); + expect(editMissing.body.code).toBe("unknown_layer"); + + const oversized = await call("PUT", "/api/codex-prompt/base", fx, { + id: null, title: "Too large", body: "x".repeat(64 * 1024 + 1), revision: rev0, + }); + expect(oversized.status).toBe(400); + expect(oversized.body.code).toBe("body_too_large"); + // An unknown variant would leave the key naming a file Codex cannot read. const unknown = await call("PUT", "/api/codex-prompt/base/select", fx, { kind: "variant", id: "zzzzzz", revision: rev0,