From 3f5c4f8612a42e2fd199ac124348f481c509cab7 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Sun, 20 Sep 2026 15:01:46 +0900 Subject: [PATCH 1/2] fix(management): validate modelSupportsVerbosity record shape The provider management validator shape-checked sibling capability records but not modelSupportsVerbosity, so an invalid value (array, non-boolean values, empty keys) could persist and reach consumers unchecked. --- src/server/auth-cors.ts | 2 ++ .../management-provider-validation.test.ts | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index c8f53c861ae..49d6f9e65cd 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -801,6 +801,8 @@ export function providerManagementConfigError( if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`; const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax"); if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`; + const verbositySupportError = booleanRecordConfigError(raw.modelSupportsVerbosity, "modelSupportsVerbosity"); + if (verbositySupportError) return `provider ${name} ${verbositySupportError}`; const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError( raw.modelReasoningSummaryDelivery, raw.modelSupportsReasoningSummaries, diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index a663550a511..1be5229a64d 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -2232,6 +2232,36 @@ describe("provider management validation", () => { } expect(loadConfig().providers["custom-summary-capability"].modelSupportsReasoningSummaries).toEqual({ strict: false }); + const acceptedVerbosityCapability = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: { terse: false }, + }, + }), + }); + expect(acceptedVerbosityCapability.status).toBe(200); + for (const invalid of [[], { terse: "false" }, { "": false }]) { + const rejected = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: invalid, + }, + }), + }); + expect(rejected.status).toBe(400); + } + expect(loadConfig().providers["custom-verbosity-capability"].modelSupportsVerbosity).toEqual({ terse: false }); + const acceptedSummaryDelivery = await fetch(new URL("/api/providers", server.url), { method: "POST", headers: { "content-type": "application/json" }, From 0e97a45452a9fa13df252f0ee9b5802f65d44081 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:39:04 +0900 Subject: [PATCH 2/2] test(management): isolate verbosity validation regression --- scripts/test-layout/layout.json | 1 + tests/fixtures/test-layout-expected.json | 1 + .../management-provider-validation.test.ts | 30 -------- .../management-provider-verbosity.test.ts | 71 +++++++++++++++++++ 4 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 tests/server/management-provider-verbosity.test.ts diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ef1c68922bb..405bf09de26 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -939,6 +939,7 @@ "management-provider-proto-override.test.ts": "server", "management-provider-synthetic-max.test.ts": "server", "management-provider-validation.test.ts": "server", + "management-provider-verbosity.test.ts": "server", "management-route-registry.test.ts": "server", "management-workflow-budget-routes.test.ts": "server", "memory-watchdog.test.ts": "server", diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index d8a39f76b8b..da054536441 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -765,6 +765,7 @@ "management-provider-proto-override.test.ts": "server", "management-provider-synthetic-max.test.ts": "server", "management-provider-validation.test.ts": "server", + "management-provider-verbosity.test.ts": "server", "management-route-registry.test.ts": "server", "management-workflow-budget-routes.test.ts": "server", "memory-watchdog.test.ts": "server", diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index 1be5229a64d..a663550a511 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -2232,36 +2232,6 @@ describe("provider management validation", () => { } expect(loadConfig().providers["custom-summary-capability"].modelSupportsReasoningSummaries).toEqual({ strict: false }); - const acceptedVerbosityCapability = await fetch(new URL("/api/providers", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - name: "custom-verbosity-capability", - provider: { - adapter: "openai-responses", - baseUrl: "https://api.example.test/v1", - modelSupportsVerbosity: { terse: false }, - }, - }), - }); - expect(acceptedVerbosityCapability.status).toBe(200); - for (const invalid of [[], { terse: "false" }, { "": false }]) { - const rejected = await fetch(new URL("/api/providers", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - name: "custom-verbosity-capability", - provider: { - adapter: "openai-responses", - baseUrl: "https://api.example.test/v1", - modelSupportsVerbosity: invalid, - }, - }), - }); - expect(rejected.status).toBe(400); - } - expect(loadConfig().providers["custom-verbosity-capability"].modelSupportsVerbosity).toEqual({ terse: false }); - const acceptedSummaryDelivery = await fetch(new URL("/api/providers", server.url), { method: "POST", headers: { "content-type": "application/json" }, diff --git a/tests/server/management-provider-verbosity.test.ts b/tests/server/management-provider-verbosity.test.ts new file mode 100644 index 00000000000..75f8e65deea --- /dev/null +++ b/tests/server/management-provider-verbosity.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadConfig, saveConfig } from "../../src/config"; +import { startServer } from "../../src/server"; +import { installIsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { managementFetch as fetch } from "../helpers/management-auth"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +test("provider management validates verbosity records without replacing valid config", async () => { + const testDir = mkdtempSync(join(tmpdir(), "ocx-provider-verbosity-")); + const previousHome = process.env.OPENCODEX_HOME; + const previousToken = process.env.OPENCODEX_API_AUTH_TOKEN; + const isolatedCodexHome = installIsolatedCodexHome("ocx-provider-verbosity-codex-"); + process.env.OPENCODEX_HOME = testDir; + let server: ReturnType | undefined; + try { + saveConfig({ + port: 0, + defaultProvider: "openai", + openaiProviderTierVersion: 2, + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "direct", + }, + }, + }); + server = startServer(0); + const acceptedVerbosityCapability = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: { terse: false }, + }, + }), + }); + expect(acceptedVerbosityCapability.status).toBe(200); + for (const invalid of [[], { terse: "false" }, { "": false }]) { + const rejected = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: invalid, + }, + }), + }); + expect(rejected.status).toBe(400); + } + expect(loadConfig().providers["custom-verbosity-capability"].modelSupportsVerbosity).toEqual({ terse: false }); + } finally { + await server?.stop(true); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + if (previousToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; + else process.env.OPENCODEX_API_AUTH_TOKEN = previousToken; + isolatedCodexHome.restore(); + removeTreeWithRetry(testDir); + } +}, 60_000);