From 5a0306ba74e0ce82be996935ef17b8ecc87725a5 Mon Sep 17 00:00:00 2001 From: chefadmin-netizen Date: Tue, 8 Sep 2026 02:01:52 +0200 Subject: [PATCH 1/4] docs(policy): execute verified fixes without external approval waits --- AGENTS.md | 19 ++++++---- MAINTAINERS.md | 52 +++++++++++++++++---------- tests/review-execution-policy.test.ts | 31 ++++++++++++++++ 3 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 tests/review-execution-policy.test.ts diff --git a/AGENTS.md b/AGENTS.md index 0b8cd3f3..82240089 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ fail a check: - `devlog/` stays listed in `.gitignore` for the working tree; the submodule gitlink is tracked, its contents are not. -Two rules keep it that way. Never commit anything under `devlog/` to *this* +Two rules keep it that way. Never commit anything under `devlog/` to _this_ repository — commit inside the submodule, then update the pointer here as a separate commit. And never nest a git repository inside the submodule: a `160000` gitlink in a tree that CI does not initialize breaks @@ -132,14 +132,19 @@ The **`enforce-target`** CI check accepts **`main`** as the only integration base. A same-repository maintainer promotion from **`dev`** onto **`main`** remains an explicit leftover exception. It rejects empty, thin, or malformed descriptions; authors with repository push permission skip the leftover -ancestry heuristic only. As with approval requirements in -[`MAINTAINERS.md`](./MAINTAINERS.md), this is enforced by convention until -branch protection is configured. +ancestry heuristic only. Required technical checks apply even when branch +protection is not configured. External approval is advisory, never a blocker. [`MAINTAINERS.md`](./MAINTAINERS.md) is authoritative for review and merge -policy (approvals, CI requirements, security review, promotion). This file +policy (review, CI requirements, security analysis, promotion). This file summarizes; it never overrides it. +During authorized implementation and PR/release completion, a valid in-scope +finding means repair plus verification, not a report-only stopping point. +Continue the inspect → fix → test → land loop while safe authorized work remains. +Explicit read-only requests remain read-only. Never fabricate independent review +or bypass actual repository permissions; do not wait for an external reviewer. + ## Review guidelines These rules apply to all code reviews on this repository, including automated @@ -155,13 +160,13 @@ reviewers (Codex, CodeRabbit). - **Security boundary (highest priority):** changes touching authentication, credential/token handling, OAuth flows, GitHub Actions workflows, release automation (`scripts/release.ts`, `.github/workflows/release.yml`), or - dependency installation require explicit security review per + dependency installation require explicit security analysis per `MAINTAINERS.md`. Treat token logging/serialization, secret exposure, workflow permission escalation, and mutable third-party action refs as release blockers. - **Runtime constraints:** the proxy is Bun-native. Flag Node-only APIs, assumptions about a compile step, or code paths that break `bun run - typecheck` / `bun run test`. +typecheck` / `bun run test`. - **Tests:** behavior changes in `src/` need a focused regression test near the existing tests for that subsystem. Shared routing, adapter, config, or server changes need the full suite green. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3e197f3c..e616589d 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -5,11 +5,11 @@ review and merge policy. ## Current maintainers -| GitHub account | Project role | Responsibilities | -| --- | --- | --- | -| [@GroepOnline](https://github.com/GroepOnline) | Project owner | Project direction, releases, repository administration, and final governance decisions | -| [@Ingwannu](https://github.com/Ingwannu) | Maintainer | Issue and pull-request triage, `main` integration, security review, and repository maintenance | -| [@Wibias](https://github.com/Wibias) | Maintainer | Issue and pull-request triage, `main` integration, and provider/CI maintenance | +| GitHub account | Project role | Responsibilities | +| ---------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------- | +| [@GroepOnline](https://github.com/GroepOnline) | Project owner | Project direction, releases, repository administration, and final governance decisions | +| [@Ingwannu](https://github.com/Ingwannu) | Maintainer | Issue and pull-request triage, `main` integration, security review, and repository maintenance | +| [@Wibias](https://github.com/Wibias) | Maintainer | Issue and pull-request triage, `main` integration, and provider/CI maintenance | The table describes project responsibilities. Actual repository permissions remain controlled through GitHub repository settings. @@ -25,14 +25,21 @@ see [The retired `dev2-go` line](#the-retired-dev2-go-line). exception; a feature or fork head targeting `dev` is `wrong_base`. - The **`enforce-target`** CI check rejects empty, thin, or malformed descriptions; authors with repository push permission skip the leftover - ancestry heuristic only. As with the approval requirement - above, this is enforced by convention until branch protection is configured - (see the note under the change log). -- A pull request requires approval from at least one maintainer and successful required CI checks - before merge. -- Authors do not approve their own pull requests. + ancestry heuristic only. Required technical checks must succeed on the exact + head being merged, whether enforced by branch protection or by the operator. +- External review and GitHub approval are advisory, never a prerequisite for + authorized execution, merge, release, or deployment. Missing, delayed, stale, + or unavailable external review must not stop the work. +- The executing maintainer or agent must inspect the exact diff, repair valid + in-scope findings, and verify the resulting head before merging. A review + status is not a substitute for evaluating its concrete findings. +- Do not manufacture approval, switch identities to self-approve, or describe + self-review as independent review. Record the verification actually performed. - Authentication, credential handling, GitHub Actions, release automation, dependency installation, - and other security-boundary changes require explicit security review. + and other security-boundary changes require explicit, recorded security analysis. + The executing agent may perform that analysis; an external reviewer is optional. + Unfixed security defects and failed or unknown required technical checks still + block release. Repository permissions and protected-branch rules are not bypassed. - A new or promoted provider preset is a credential-destination change. Before merge it needs the primary-source evidence listed under [Adding a provider to the catalog](https://opencodex.me/contributing/#evidence-required-for-a-canonical-preset): documented @@ -42,12 +49,20 @@ see [The retired `dev2-go` line](#the-retired-dev2-go-line). with the service is disclosed, not disqualifying, and it does not lower the evidence bar. When the evidence is incomplete, prefer an inert `src/providers/free-directory.ts` reference row over a canonical registry entry. -- Security-sensitive and release-related changes should be reviewed by both maintainers when - practical. +- Additional maintainer review is welcome when available, without waiting for it. - Direct pushes are reserved for maintainer-owned integration work, urgent repairs, or incident recovery. The same CI and documentation requirements still apply. - Promotion from `dev` to `main` and npm releases is maintainer-controlled. +### Review means repair within the authorized task + +During implementation, PR completion, release, or deployment work, noticing, +finding, or reviewing a valid in-scope defect means reproducing it, fixing it, +adding appropriate regression coverage, and continuing through verification. +A findings list or handoff does not finish executable work. Explicitly read-only +reviews, audits, and diagnosis requests remain read-only unless fixes are also +authorized; unrelated work and actions needing new authority stay out of scope. + ## The retired `dev2-go` line `dev2-go` was a parallel integration line that rebuilt the runtime as a Go @@ -101,11 +116,10 @@ Adding or removing a maintainer requires: and release automation keep the two owners already listed for those paths, so this addition does not widen the review surface for them. - CODEOWNERS requests reviews rather than enforcing them — no branch protection - rule is configured on this repository, so code-owner approval is a convention - here, not a gate. The same is true of the approval requirement in the review - and merge policy above. Widening the security boundary, or enforcing either - of these through branch protection, is a separate decision. + This entry describes the policy at that time. The current review policy above + makes external approval advisory while retaining technical and security checks. + CODEOWNERS routes review requests; it does not itself enforce branch protection. + Always inspect current repository settings rather than infer them from this log. ## Security reports diff --git a/tests/review-execution-policy.test.ts b/tests/review-execution-policy.test.ts new file mode 100644 index 00000000..bc794c15 --- /dev/null +++ b/tests/review-execution-policy.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +const policy = readFileSync( + new URL("../MAINTAINERS.md", import.meta.url), + "utf8", +); +const agents = readFileSync(new URL("../AGENTS.md", import.meta.url), "utf8"); + +describe("review execution policy", () => { + test("external approval is advisory without weakening technical verification", () => { + expect(policy).toContain( + "External review and GitHub approval are advisory", + ); + expect(policy).toContain("failed or unknown required technical checks"); + expect(policy).toContain("explicit, recorded security analysis"); + expect(policy).toContain("protected-branch rules are not bypassed"); + expect(policy).not.toContain( + "requires approval from at least one maintainer", + ); + expect(agents).toContain("External approval is advisory, never a blocker"); + }); + + test("findings become verified repairs within authority, not fabricated approval", () => { + expect(policy).toContain("fixing it,"); + expect(policy).toContain("regression coverage"); + expect(policy).toContain("unless fixes are also"); + expect(policy).toContain("Do not manufacture approval"); + expect(agents).toContain("Explicit read-only requests remain read-only"); + }); +}); From 447846e913f0c06d6028485741dc9802a3b86344 Mon Sep 17 00:00:00 2001 From: chefadmin-netizen Date: Tue, 8 Sep 2026 02:12:50 +0200 Subject: [PATCH 2/4] release: prepare v1.4.0 for client artifacts and workspace GUI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5afe04f2..3efc632b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@groeponline/opencodex", - "version": "1.3.2", + "version": "1.4.0", "description": "GroepOnline fork — Universal provider proxy for OpenAI Codex & Claude Code. Use any LLM with Codex CLI/App/SDK and Claude Code.", "type": "module", "main": "./bin/package-main.mjs", From 9e69321823796efc2073343a282089bfd6130027 Mon Sep 17 00:00:00 2001 From: chefadmin-netizen Date: Tue, 8 Sep 2026 02:29:19 +0200 Subject: [PATCH 3/4] test: isolate image relay fixtures from discovery probes --- tests/server-images.test.ts | 1441 ++++++++++++++++++++++++----------- 1 file changed, 1007 insertions(+), 434 deletions(-) diff --git a/tests/server-images.test.ts b/tests/server-images.test.ts index 90d4aea5..4819eab8 100644 --- a/tests/server-images.test.ts +++ b/tests/server-images.test.ts @@ -7,8 +7,14 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { existsSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { saveCodexAccountCredential } from "../src/codex/account-store"; -import { clearAccountNeedsReauth, clearAccountQuota } from "../src/codex/auth-api"; -import { clearCodexUpstreamHealth, clearThreadAccountMap } from "../src/codex/routing"; +import { + clearAccountNeedsReauth, + clearAccountQuota, +} from "../src/codex/auth-api"; +import { + clearCodexUpstreamHealth, + clearThreadAccountMap, +} from "../src/codex/routing"; import { saveConfig } from "../src/config"; import { selectImagesProvider } from "../src/providers/openai-sidecar"; import { startServer } from "../src/server"; @@ -16,7 +22,10 @@ import { saveCredential } from "../src/oauth/store"; import type { OcxConfig } from "../src/types"; import { ANTIGRAVITY_REQUEST_UA } from "../src/adapters/google-antigravity-wire"; import { fakeChatGptJwt } from "./helpers/fake-chatgpt-jwt"; -import { installIsolatedCodexHome, type IsolatedCodexHome } from "./helpers/isolated-codex-home"; +import { + installIsolatedCodexHome, + type IsolatedCodexHome, +} from "./helpers/isolated-codex-home"; const previousApiToken = process.env.OPENCODEX_API_AUTH_TOKEN; const previousOpencodexHome = process.env.OPENCODEX_HOME; @@ -42,11 +51,13 @@ beforeEach(() => { afterEach(() => { globalThis.fetch = originalFetch; - if (previousApiToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; + if (previousApiToken === undefined) + delete process.env.OPENCODEX_API_AUTH_TOKEN; else process.env.OPENCODEX_API_AUTH_TOKEN = previousApiToken; if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (previousImagesApiKey === undefined) delete process.env.OPENCODEX_TEST_IMAGES_API_KEY; + if (previousImagesApiKey === undefined) + delete process.env.OPENCODEX_TEST_IMAGES_API_KEY; else process.env.OPENCODEX_TEST_IMAGES_API_KEY = previousImagesApiKey; isolatedCodexHome?.restore(); isolatedCodexHome = null; @@ -63,10 +74,18 @@ interface CapturedRequest { body: unknown; } -function fakeImagesUpstream(captured: CapturedRequest[], status = 200, payload?: unknown) { +function fakeImagesUpstream( + captured: CapturedRequest[], + status = 200, + payload?: unknown, +) { const upstream = Bun.serve({ port: 0, async fetch(req) { + // Local port-discovery probes are not image relays and have no JSON body. + // Keep them out of the captured business-request count. + if (req.method !== "POST") + return new Response("Not found", { status: 404 }); captured.push({ path: new URL(req.url).pathname, headers: req.headers, @@ -79,15 +98,27 @@ function fakeImagesUpstream(captured: CapturedRequest[], status = 200, payload?: }, }); globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); let path: string | undefined; - if (url.hostname === "chatgpt.com" && url.pathname.startsWith("/backend-api/codex")) { + if ( + url.hostname === "chatgpt.com" && + url.pathname.startsWith("/backend-api/codex") + ) { path = url.pathname.slice("/backend-api/codex".length); - } else if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { + } else if ( + url.hostname === "api.openai.com" && + url.pathname.startsWith("/v1") + ) { path = url.pathname; } - if (path) return originalFetch(new URL(`${path}${url.search}`, upstream.url), init); + if (path) + return originalFetch(new URL(`${path}${url.search}`, upstream.url), init); return originalFetch(input, init); }) as typeof fetch; return upstream; @@ -109,6 +140,21 @@ function forwardConfig(_baseUrl = ""): OcxConfig { } as OcxConfig; } +test("image upstream fixture rejects discovery probes without capturing a relay", async () => { + const captured: CapturedRequest[] = []; + const upstream = fakeImagesUpstream(captured); + try { + for (const method of ["GET", "HEAD"]) { + const response = await originalFetch(upstream.url, { method }); + expect(response.status).toBe(404); + await response.text(); + } + expect(captured).toHaveLength(0); + } finally { + await upstream.stop(true); + } +}); + const disabledOpenAiProvider = { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", @@ -124,7 +170,11 @@ const canonicalOpenAiProvider = { } as const; function keyedProvider(_baseUrl = "") { - return { adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", apiKey: "sk-platform-key" }; + return { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + apiKey: "sk-platform-key", + }; } test("POST /v1/images/generations relays to the ChatGPT forward provider with forwarded auth", async () => { @@ -134,23 +184,38 @@ test("POST /v1/images/generations relays to the ChatGPT forward provider with fo const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, - "chatgpt-account-id": "acct-123", + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + "chatgpt-account-id": "acct-123", + }, + body: JSON.stringify({ + prompt: "a halftone gothic hero", + model: "gpt-image-2", + size: "auto", + }), }, - body: JSON.stringify({ prompt: "a halftone gothic hero", model: "gpt-image-2", size: "auto" }), - }); + ); expect(response.status).toBe(200); - expect(await response.json()).toEqual({ created: 1_767_000_000, data: [{ b64_json: "aGVsbG8=" }] }); + expect(await response.json()).toEqual({ + created: 1_767_000_000, + data: [{ b64_json: "aGVsbG8=" }], + }); expect(captured).toHaveLength(1); expect(captured[0].path).toBe("/images/generations"); - expect(captured[0].headers.get("authorization")).toBe(`Bearer ${DIRECT_CHATGPT_TOKEN}`); + expect(captured[0].headers.get("authorization")).toBe( + `Bearer ${DIRECT_CHATGPT_TOKEN}`, + ); expect(captured[0].headers.get("chatgpt-account-id")).toBe("acct-123"); - expect(captured[0].body).toMatchObject({ prompt: "a halftone gothic hero", model: "gpt-image-2" }); + expect(captured[0].body).toMatchObject({ + prompt: "a halftone gothic hero", + model: "gpt-image-2", + }); } finally { await server.stop(true); await upstream.stop(true); @@ -198,7 +263,12 @@ test("a routed pool account's token overrides the caller bearer on the forward r }, codexAccounts: [ { id: "main", email: "main@example.test", isMain: true }, - { id: "pool-a", email: "pool@example.test", isMain: false, chatgptAccountId: "acct-pool-a" }, + { + id: "pool-a", + email: "pool@example.test", + isMain: false, + chatgptAccountId: "acct-pool-a", + }, ], activeCodexAccountId: "pool-a", } as OcxConfig); @@ -211,15 +281,23 @@ test("a routed pool account's token overrides the caller bearer on the forward r const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json", authorization: "Bearer caller-token" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer caller-token", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); // Pool routing selected pool-a; the caller token must NOT reach upstream. - expect(captured[0].headers.get("authorization")).toBe("Bearer pool-access-token"); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer pool-access-token", + ); expect(captured[0].headers.get("chatgpt-account-id")).toBe("acct-pool-a"); } finally { await server.stop(true); @@ -234,17 +312,23 @@ test("zstd-compressed request bodies are decoded before the relay", async () => const server = startServer(0); try { - const raw = JSON.stringify({ prompt: "compressed prompt", model: "gpt-image-2" }); - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - "content-encoding": "zstd", - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, - "chatgpt-account-id": "acct-123", - }, - body: Bun.zstdCompressSync(Buffer.from(raw)), + const raw = JSON.stringify({ + prompt: "compressed prompt", + model: "gpt-image-2", }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + "content-encoding": "zstd", + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + "chatgpt-account-id": "acct-123", + }, + body: Bun.zstdCompressSync(Buffer.from(raw)), + }, + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); expect(captured[0].headers.get("content-encoding")).toBeNull(); @@ -264,24 +348,31 @@ test("falls back to a keyed openai-responses provider when no forward provider e openaiProviderTierVersion: 2, providers: { openai: disabledOpenAiProvider, - "openai-apikey": keyedProvider(upstream.url.toString().replace(/\/$/, "")), + "openai-apikey": keyedProvider( + upstream.url.toString().replace(/\/$/, ""), + ), }, } as OcxConfig); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - // The caller's ChatGPT OAuth token must NOT reach a platform API-key upstream. - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + // The caller's ChatGPT OAuth token must NOT reach a platform API-key upstream. + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); - expect(captured[0].headers.get("authorization")).toBe("Bearer sk-platform-key"); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer sk-platform-key", + ); // Keyed baseUrl had no /v1 suffix — the relay normalizes to the platform path. expect(captured[0].path).toBe("/v1/images/generations"); } finally { @@ -300,7 +391,10 @@ test("an explicit custom Images provider uses its configured endpoint, key, and headers: req.headers, body: await req.json(), }); - return Response.json({ created: 1_767_000_000, data: [{ b64_json: "aGVsbG8=" }] }); + return Response.json({ + created: 1_767_000_000, + data: [{ b64_json: "aGVsbG8=" }], + }); }, }); saveConfig({ @@ -322,20 +416,28 @@ test("an explicit custom Images provider uses its configured endpoint, key, and const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); expect(captured[0].path).toBe("/v1/images/generations"); - expect(captured[0].headers.get("authorization")).toBe("Bearer custom-images-key"); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer custom-images-key", + ); expect(captured[0].headers.get("x-provider-route")).toBe("images"); - expect(captured[0].body).toMatchObject({ prompt: "a cat", model: "gpt-image-2" }); + expect(captured[0].body).toMatchObject({ + prompt: "a cat", + model: "gpt-image-2", + }); } finally { await server.stop(true); await upstream.stop(true); @@ -353,7 +455,10 @@ test("an explicit Images provider accepts bearer admission without leaking the p headers: req.headers, body: await req.json(), }); - return Response.json({ created: 1_767_000_000, data: [{ b64_json: "aGVsbG8=" }] }); + return Response.json({ + created: 1_767_000_000, + data: [{ b64_json: "aGVsbG8=" }], + }); }, }); saveConfig({ @@ -375,18 +480,27 @@ test("an explicit Images provider accepts bearer admission without leaking the p const server = startServer(0); try { - const response = await fetch(`http://127.0.0.1:${server.port}/v1/images/generations`, { - method: "POST", - headers: { - "content-type": "application/json", - authorization: "Bearer proxy-admission-secret", + const response = await fetch( + `http://127.0.0.1:${server.port}/v1/images/generations`, + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer proxy-admission-secret", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); - expect(captured[0].headers.get("authorization")).toBe("Bearer custom-images-key"); - expect([...captured[0].headers.values()].some(value => value.includes("proxy-admission-secret"))).toBe(false); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer custom-images-key", + ); + expect( + [...captured[0].headers.values()].some((value) => + value.includes("proxy-admission-secret"), + ), + ).toBe(false); } finally { await server.stop(true); await upstream.stop(true); @@ -412,18 +526,25 @@ test("an invalid explicit Images provider returns 400 after bearer admission", a const server = startServer(0); try { - const response = await fetch(`http://127.0.0.1:${server.port}/v1/images/generations`, { - method: "POST", - headers: { - "content-type": "application/json", - authorization: "Bearer proxy-admission-secret", + const response = await fetch( + `http://127.0.0.1:${server.port}/v1/images/generations`, + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer proxy-admission-secret", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.type).toBe("invalid_request_error"); - expect(json.error.message).toContain("must be an API-key openai-responses provider"); + expect(json.error.message).toContain( + "must be an API-key openai-responses provider", + ); } finally { await server.stop(true); } @@ -438,7 +559,9 @@ test("an invalid explicit Images provider fails closed instead of using another openaiProviderTierVersion: 2, providers: { openai: disabledOpenAiProvider, - "openai-apikey": keyedProvider(upstream.url.toString().replace(/\/$/, "")), + "openai-apikey": keyedProvider( + upstream.url.toString().replace(/\/$/, ""), + ), "custom-images": { adapter: "openai-chat", baseUrl: "https://images.example.test/v1", @@ -450,15 +573,20 @@ test("an invalid explicit Images provider fails closed instead of using another const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(400); expect(captured).toHaveLength(0); - const json = await response.json() as { error: { message: string } }; - expect(json.error.message).toContain("must be an API-key openai-responses provider"); + const json = (await response.json()) as { error: { message: string } }; + expect(json.error.message).toContain( + "must be an API-key openai-responses provider", + ); } finally { await server.stop(true); await upstream.stop(true); @@ -479,13 +607,16 @@ test("an explicit Images provider cannot reuse a registry-managed provider id", const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("must name a custom provider"); } finally { await server.stop(true); @@ -494,40 +625,118 @@ test("an explicit Images provider cannot reuse a registry-managed provider id", test.each([ ["missing", undefined, "is not configured"], - ["disabled", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", disabled: true }, "is disabled"], - ["wrong adapter", { adapter: "openai-chat", baseUrl: "https://images.example.test/v1", apiKey: "key" }, "must be an API-key openai-responses provider"], - ["forward auth", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", authMode: "forward" }, "must be an API-key openai-responses provider"], - ["oauth auth", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", authMode: "oauth" }, "must be an API-key openai-responses provider"], - ["local auth", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", apiKey: "key", authMode: "local" }, "must be an API-key openai-responses provider"], - ["missing key", { adapter: "openai-responses", baseUrl: "https://images.example.test/v1", authMode: "key" }, "has no usable API key"], -] as const)("explicit Images provider rejects %s configuration", async (_case, provider, expectedError) => { - const selection = await selectImagesProvider({ - port: 0, - defaultProvider: "custom-images", - providers: provider ? { "custom-images": provider } : {}, - images: { provider: "custom-images" }, - } as OcxConfig); - - expect(selection.keyed).toBeUndefined(); - expect(selection.forwardCandidates).toHaveLength(0); - expect(selection.error).toContain(expectedError); -}); + [ + "disabled", + { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + apiKey: "key", + disabled: true, + }, + "is disabled", + ], + [ + "wrong adapter", + { + adapter: "openai-chat", + baseUrl: "https://images.example.test/v1", + apiKey: "key", + }, + "must be an API-key openai-responses provider", + ], + [ + "forward auth", + { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + apiKey: "key", + authMode: "forward", + }, + "must be an API-key openai-responses provider", + ], + [ + "oauth auth", + { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + apiKey: "key", + authMode: "oauth", + }, + "must be an API-key openai-responses provider", + ], + [ + "local auth", + { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + apiKey: "key", + authMode: "local", + }, + "must be an API-key openai-responses provider", + ], + [ + "missing key", + { + adapter: "openai-responses", + baseUrl: "https://images.example.test/v1", + authMode: "key", + }, + "has no usable API key", + ], +] as const)( + "explicit Images provider rejects %s configuration", + async (_case, provider, expectedError) => { + const selection = await selectImagesProvider({ + port: 0, + defaultProvider: "custom-images", + providers: provider ? { "custom-images": provider } : {}, + images: { provider: "custom-images" }, + } as OcxConfig); + + expect(selection.keyed).toBeUndefined(); + expect(selection.forwardCandidates).toHaveLength(0); + expect(selection.error).toContain(expectedError); + }, +); // A failed ChefVault lease on an explicit Images provider must keep its credential/authority // split: a revoked lease is a non-retryable 401 authentication_error, an unreachable authority // is a retryable 503 api_error. Codex uses the type to decide whether to retry. test.each([ - ["revoked", "chefvault://providers/images/revoked", 410, { code: "revoked", message: "lease revoked" }, 401, "authentication_error"], - ["authority_unavailable", "chefvault://providers/images/outage", 503, { message: "authority down" }, 503, "api_error"], + [ + "revoked", + "chefvault://providers/images/revoked", + 410, + { code: "revoked", message: "lease revoked" }, + 401, + "authentication_error", + ], + [ + "authority_unavailable", + "chefvault://providers/images/outage", + 503, + { message: "authority down" }, + 503, + "api_error", + ], ] as const)( "a %s ChefVault lease failure maps the Images provider error to %d/%s", async (_case, ref, vaultStatus, vaultBody, expectedStatus, expectedType) => { const previousVaultUrl = process.env.CHEF_PROVIDER_SECURITY_URL; const previousVaultToken = process.env.CHEF_PROVIDER_SECURITY_TOKEN; process.env.CHEF_PROVIDER_SECURITY_URL = "http://vault.test"; - process.env.CHEF_PROVIDER_SECURITY_TOKEN = "access-token-images-vault-32chars"; - globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + process.env.CHEF_PROVIDER_SECURITY_TOKEN = + "access-token-images-vault-32chars"; + globalThis.fetch = (async ( + input: RequestInfo | URL, + init?: RequestInit, + ) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; if (url === "http://vault.test/v1/credentials/resolve") { return Response.json(vaultBody, { status: vaultStatus }); } @@ -556,9 +765,11 @@ test.each([ expect(selection.errorType).toBe(expectedType); } finally { globalThis.fetch = originalFetch; - if (previousVaultUrl === undefined) delete process.env.CHEF_PROVIDER_SECURITY_URL; + if (previousVaultUrl === undefined) + delete process.env.CHEF_PROVIDER_SECURITY_URL; else process.env.CHEF_PROVIDER_SECURITY_URL = previousVaultUrl; - if (previousVaultToken === undefined) delete process.env.CHEF_PROVIDER_SECURITY_TOKEN; + if (previousVaultToken === undefined) + delete process.env.CHEF_PROVIDER_SECURITY_TOKEN; else process.env.CHEF_PROVIDER_SECURITY_TOKEN = previousVaultToken; } }, @@ -573,17 +784,22 @@ test("keyed baseUrl with a /v1 suffix is normalized (no double /v1)", async () = openaiProviderTierVersion: 2, providers: { openai: disabledOpenAiProvider, - "openai-apikey": keyedProvider(`${upstream.url.toString().replace(/\/$/, "")}/v1`), + "openai-apikey": keyedProvider( + `${upstream.url.toString().replace(/\/$/, "")}/v1`, + ), }, } as OcxConfig); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); expect(captured[0].path).toBe("/v1/images/generations"); @@ -603,20 +819,27 @@ test("an unauthenticated request skips the forward provider when a keyed provide providers: { // ENABLED forward provider: an accidental forward relay would fail loudly (port 1). openai: canonicalOpenAiProvider, - "openai-apikey": keyedProvider(upstream.url.toString().replace(/\/$/, "")), + "openai-apikey": keyedProvider( + upstream.url.toString().replace(/\/$/, ""), + ), }, } as OcxConfig); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); - expect(captured[0].headers.get("authorization")).toBe("Bearer sk-platform-key"); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer sk-platform-key", + ); } finally { await server.stop(true); await upstream.stop(true); @@ -633,13 +856,16 @@ test("an unauthenticated request gets 401 when only the forward provider exists" const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(401); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("ChatGPT auth"); } finally { await server.stop(true); @@ -655,11 +881,18 @@ test("pool auth failure is not hidden by the keyed API provider", async () => { openaiProviderTierVersion: 2, providers: { openai: { ...canonicalOpenAiProvider, codexAccountMode: "pool" }, - "openai-apikey": keyedProvider(upstream.url.toString().replace(/\/$/, "")), + "openai-apikey": keyedProvider( + upstream.url.toString().replace(/\/$/, ""), + ), }, codexAccounts: [ { id: "main", email: "main@example.test", isMain: true }, - { id: "pool-a", email: "pool@example.test", isMain: false, chatgptAccountId: "acct-pool-a" }, + { + id: "pool-a", + email: "pool@example.test", + isMain: false, + chatgptAccountId: "acct-pool-a", + }, ], // pool-a has NO stored credential, so forward-auth resolution throws CodexAuthContextError. activeCodexAccountId: "pool-a", @@ -667,14 +900,20 @@ test("pool auth failure is not hidden by the keyed API provider", async () => { const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json", authorization: "Bearer caller-token" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer caller-token", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(401); expect(captured).toHaveLength(0); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("reauthentication"); } finally { await server.stop(true); @@ -692,20 +931,31 @@ test("forward-auth failure surfaces its own error when no keyed provider exists" }, codexAccounts: [ { id: "main", email: "main@example.test", isMain: true }, - { id: "pool-a", email: "pool@example.test", isMain: false, chatgptAccountId: "acct-pool-a" }, + { + id: "pool-a", + email: "pool@example.test", + isMain: false, + chatgptAccountId: "acct-pool-a", + }, ], activeCodexAccountId: "pool-a", } as OcxConfig); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json", authorization: "Bearer caller-token" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer caller-token", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(401); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("reauthentication"); } finally { await server.stop(true); @@ -719,22 +969,31 @@ test("returns an honest 400 when no OpenAI-family upstream is configured", async openaiProviderTierVersion: 2, providers: { openai: disabledOpenAiProvider, - groq: { adapter: "openai-chat", baseUrl: "https://api.groq.example/v1", apiKey: "gsk-x" }, + groq: { + adapter: "openai-chat", + baseUrl: "https://api.groq.example/v1", + apiKey: "gsk-x", + }, }, } as OcxConfig); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); // 4xx (not 5xx): codex retries every 5xx up to 5 total attempts, and this is a permanent // configuration state. The actionable part is the message, which codex Debug-prints into // the model-visible tool failure. expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.message).toContain("image generation"); expect(json.error.message).toContain("disable image_generation"); } finally { @@ -745,24 +1004,32 @@ test("returns an honest 400 when no OpenAI-family upstream is configured", async test("relays upstream error status and body verbatim", async () => { const captured: CapturedRequest[] = []; const upstream = fakeImagesUpstream(captured, 403, { - error: { message: "Your plan does not allow image generation.", type: "forbidden" }, + error: { + message: "Your plan does not allow image generation.", + type: "forbidden", + }, }); saveConfig(forwardConfig(upstream.url.toString().replace(/\/$/, ""))); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, - "chatgpt-account-id": "acct-123", + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + "chatgpt-account-id": "acct-123", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(403); - const json = await response.json() as { error: { message: string } }; - expect(json.error.message).toBe("Your plan does not allow image generation."); + const json = (await response.json()) as { error: { message: string } }; + expect(json.error.message).toBe( + "Your plan does not allow image generation.", + ); } finally { await server.stop(true); await upstream.stop(true); @@ -774,15 +1041,30 @@ test("a hung upstream times out with 504 after config.images.timeoutMs", async ( port: 0, fetch(req) { return new Promise((_, reject) => { - req.signal.addEventListener("abort", () => reject(new Error("client aborted")), { once: true }); + req.signal.addEventListener( + "abort", + () => reject(new Error("client aborted")), + { once: true }, + ); }); }, }); globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); - if (url.hostname === "chatgpt.com" && url.pathname.startsWith("/backend-api/codex")) { - return originalFetch(new URL(url.pathname.slice("/backend-api/codex".length), upstream.url), init); + if ( + url.hostname === "chatgpt.com" && + url.pathname.startsWith("/backend-api/codex") + ) { + return originalFetch( + new URL(url.pathname.slice("/backend-api/codex".length), upstream.url), + init, + ); } return originalFetch(input, init); }) as typeof fetch; @@ -793,17 +1075,20 @@ test("a hung upstream times out with 504 after config.images.timeoutMs", async ( const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, - "chatgpt-account-id": "acct-123", + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${DIRECT_CHATGPT_TOKEN}`, + "chatgpt-account-id": "acct-123", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + ); expect(response.status).toBe(504); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("timed out"); } finally { await server.stop(true); @@ -867,7 +1152,9 @@ test("the proxy admission secret is never relayed to the forward upstream", asyn openaiProviderTierVersion: 2, providers: { openai: canonicalOpenAiProvider, - "openai-apikey": keyedProvider(upstream.url.toString().replace(/\/$/, "")), + "openai-apikey": keyedProvider( + upstream.url.toString().replace(/\/$/, ""), + ), }, } as OcxConfig); @@ -876,15 +1163,25 @@ test("the proxy admission secret is never relayed to the forward upstream", asyn // Authorization carries the proxy's OWN admission token — it authenticates the caller to the // proxy, but must never be forwarded as ChatGPT credentials. OpenAI forward is skipped; a // configured keyed provider may still serve the request with its own apiKey. - const response = await fetch(`http://127.0.0.1:${server.port}/v1/images/generations`, { - method: "POST", - headers: { "content-type": "application/json", authorization: "Bearer local-secret" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + `http://127.0.0.1:${server.port}/v1/images/generations`, + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer local-secret", + }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); expect(captured).toHaveLength(1); - expect(captured[0].headers.get("authorization")).toBe("Bearer sk-platform-key"); - expect([...captured[0].headers.values()].some(v => v.includes("local-secret"))).toBe(false); + expect(captured[0].headers.get("authorization")).toBe( + "Bearer sk-platform-key", + ); + expect( + [...captured[0].headers.values()].some((v) => v.includes("local-secret")), + ).toBe(false); } finally { await server.stop(true); await upstream.stop(true); @@ -921,7 +1218,8 @@ interface CcaFetchRequest { body: unknown; } -const CCA_TINY_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="; +const CCA_TINY_PNG = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="; /** * Stub globalThis.fetch for CCA image tests: requests to the registry host @@ -938,18 +1236,33 @@ function ccaFetchMock( const status = response?.status ?? 200; const payload = response?.payload ?? { response: { - candidates: [{ - content: { parts: [{ inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }] }, - }], + candidates: [ + { + content: { + parts: [ + { inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }, + ], + }, + }, + ], }, }; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); const headers = new Headers(init?.headers); let parsedBody: unknown; if (init?.body && typeof init.body === "string") { - try { parsedBody = JSON.parse(init.body); } catch { /* non-JSON body */ } + try { + parsedBody = JSON.parse(init.body); + } catch { + /* non-JSON body */ + } } if (url.hostname === "daily-cloudcode-pa.googleapis.com") { registryHits.push({ url: requestUrl, headers, body: parsedBody }); @@ -979,13 +1292,16 @@ test("CCA image fallback generates images via Google Antigravity when no OpenAI const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a neon cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a neon cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); - const json = await response.json() as { data: { b64_json: string }[] }; + const json = (await response.json()) as { data: { b64_json: string }[] }; expect(json.data).toHaveLength(1); expect(json.data[0].b64_json).toBe(CCA_TINY_PNG); @@ -993,14 +1309,24 @@ test("CCA image fallback generates images via Google Antigravity when no OpenAI expect(registryHits).toHaveLength(1); expect(registryHits[0].url).toContain("daily-cloudcode-pa.googleapis.com"); expect(registryHits[0].url).toContain("generateContent"); - const body = registryHits[0].body as { model?: string; request?: { generationConfig?: { responseModalities?: string[] } } }; + const body = registryHits[0].body as { + model?: string; + request?: { generationConfig?: { responseModalities?: string[] } }; + }; expect(body.model).toBe("gemini-3.1-flash-image"); - expect(body.request?.generationConfig?.responseModalities).toEqual(["TEXT", "IMAGE"]); - expect(registryHits[0].headers.get("authorization")).toBe("Bearer cca-access-token"); + expect(body.request?.generationConfig?.responseModalities).toEqual([ + "TEXT", + "IMAGE", + ]); + expect(registryHits[0].headers.get("authorization")).toBe( + "Bearer cca-access-token", + ); // The CCA image request must use the shared Antigravity User-Agent (not a // bespoke "opencodex-images/1.0"), so the request fingerprint matches the // OAuth credential. - expect(registryHits[0].headers.get("user-agent")).toBe(ANTIGRAVITY_REQUEST_UA); + expect(registryHits[0].headers.get("user-agent")).toBe( + ANTIGRAVITY_REQUEST_UA, + ); // The attacker host (config baseUrl) must NOT receive any request. expect(otherHits).toHaveLength(0); @@ -1012,18 +1338,24 @@ test("CCA image fallback generates images via Google Antigravity when no OpenAI test("CCA image fallback preserves upstream 429 status", async () => { const registryHits: CcaFetchRequest[] = []; const otherHits: CcaFetchRequest[] = []; - ccaFetchMock(registryHits, otherHits, { status: 429, payload: { error: { message: "Rate limited" } } }); + ccaFetchMock(registryHits, otherHits, { + status: 429, + payload: { error: { message: "Rate limited" } }, + }); saveConfig(ccaConfig()); await saveCredential("google-antigravity", { ...CCA_CREDENTIAL }); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(429); // The registry host was hit, not the attacker host. expect(registryHits).toHaveLength(1); @@ -1046,7 +1378,7 @@ test("CCA fallback does not serve image edits", async () => { }); // Edits should NOT hit the CCA fallback — it's text-to-image only. expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("image generation"); } finally { await server.stop(true); @@ -1065,20 +1397,25 @@ test("CCA image fallback never sends Authorization to a tampered config baseUrl const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(200); // The registry host received the request with the OAuth bearer token. expect(registryHits).toHaveLength(1); expect(registryHits[0].url).toContain("daily-cloudcode-pa.googleapis.com"); - expect(registryHits[0].headers.get("authorization")).toBe("Bearer cca-access-token"); + expect(registryHits[0].headers.get("authorization")).toBe( + "Bearer cca-access-token", + ); // The attacker host received ZERO requests — no Authorization header leak. - const authLeak = attackerHits.filter(r => r.headers.get("authorization")); + const authLeak = attackerHits.filter((r) => r.headers.get("authorization")); expect(attackerHits).toHaveLength(0); expect(authLeak).toHaveLength(0); } finally { @@ -1095,13 +1432,16 @@ test("CCA image fallback rejects an empty prompt with 400 before any OAuth work" const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("prompt is required"); } finally { await server.stop(true); @@ -1113,13 +1453,16 @@ test("CCA image fallback rejects a whitespace-only prompt with 400", async () => const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: " ", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: " ", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("prompt is required"); } finally { await server.stop(true); @@ -1148,7 +1491,12 @@ test("CCA fallback serves images when OpenAI forward auth fails but Google Antig }, codexAccounts: [ { id: "main", email: "main@example.test", isMain: true }, - { id: "pool-a", email: "pool@example.test", isMain: false, chatgptAccountId: "acct-pool-a" }, + { + id: "pool-a", + email: "pool@example.test", + isMain: false, + chatgptAccountId: "acct-pool-a", + }, ], activeCodexAccountId: "pool-a", } as OcxConfig); @@ -1156,14 +1504,20 @@ test("CCA fallback serves images when OpenAI forward auth fails but Google Antig const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json", authorization: "Bearer caller-token" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer caller-token", + }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); // OpenAI forward auth failed, but CCA picked up the slack. expect(response.status).toBe(200); - const json = await response.json() as { data: { b64_json: string }[] }; + const json = (await response.json()) as { data: { b64_json: string }[] }; expect(json.data).toHaveLength(1); expect(json.data[0].b64_json).toBe(CCA_TINY_PNG); @@ -1185,13 +1539,16 @@ test("CCA OAuth no credential saved returns 401 (login required), not a misleadi const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(401); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("login required"); } finally { await server.stop(true); @@ -1204,7 +1561,12 @@ test("CCA fetch network failure returns 502 without leaking the timeout timer", // the timeout timer alive. With a short timeout this would keep the process // alive. The fix wraps everything in try/finally so cleanup always runs. globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "daily-cloudcode-pa.googleapis.com") { throw new TypeError("fetch failed: connection refused"); @@ -1217,13 +1579,16 @@ test("CCA fetch network failure returns 502 without leaking the timeout timer", const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("CCA image generation failed"); } finally { await server.stop(true); @@ -1234,9 +1599,17 @@ test("keyed/forward relay streams the upstream body and passes payload + status // Regression for the arrayBuffer() -> streaming bounded reader change on the // keyed/forward relay path: a normal-sized streamed body must still be relayed // verbatim with its status and content-type. - const payload = JSON.stringify({ created: 1_767_000_000, data: [{ b64_json: "aGVsbG8=" }] }); + const payload = JSON.stringify({ + created: 1_767_000_000, + data: [{ b64_json: "aGVsbG8=" }], + }); globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { const encoded = new TextEncoder().encode(payload); @@ -1249,7 +1622,10 @@ test("keyed/forward relay streams the upstream body and passes payload + status controller.close(); }, }); - return new Response(body, { status: 200, headers: { "content-type": "application/json" } }); + return new Response(body, { + status: 200, + headers: { "content-type": "application/json" }, + }); } return originalFetch(input, init); }) as typeof fetch; @@ -1266,13 +1642,16 @@ test("keyed/forward relay streams the upstream body and passes payload + status const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(200); - const json = await response.json() as { data: { b64_json: string }[] }; + const json = (await response.json()) as { data: { b64_json: string }[] }; expect(json.data[0].b64_json).toBe("aGVsbG8="); } finally { await server.stop(true); @@ -1290,7 +1669,12 @@ test("keyed/forward relay rejects an oversized streamed body mid-stream with 502 const CHUNK = new Uint8Array(1024); // 1 KiB, reused for every enqueue const MAX_BYTES = 4 * 1024; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { let sent = 0; @@ -1302,7 +1686,10 @@ test("keyed/forward relay rejects an oversized streamed body mid-stream with 502 if (sent > MAX_BYTES + CHUNK.byteLength) controller.close(); }, }); - return new Response(body, { status: 200, headers: { "content-type": "application/json" } }); + return new Response(body, { + status: 200, + headers: { "content-type": "application/json" }, + }); } return originalFetch(input, init); }) as typeof fetch; @@ -1319,17 +1706,21 @@ test("keyed/forward relay rejects an oversized streamed body mid-stream with 502 const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/too large/i); } finally { await server.stop(true); - if (previousMax === undefined) delete process.env.OPENCODEX_IMAGES_RESPONSE_MAX_BYTES; + if (previousMax === undefined) + delete process.env.OPENCODEX_IMAGES_RESPONSE_MAX_BYTES; else process.env.OPENCODEX_IMAGES_RESPONSE_MAX_BYTES = previousMax; } }, 5_000); @@ -1338,7 +1729,12 @@ test("keyed/forward relay returns 502 when the upstream response has no body", a // Regression for the streaming bounded reader: a 204/no-body upstream response // has a null body, which the relay must map to a 502 rather than crashing. globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { return new Response(null, { status: 204 }); @@ -1358,13 +1754,16 @@ test("keyed/forward relay returns 502 when the upstream response has no body", a const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("no body"); } finally { await server.stop(true); @@ -1378,20 +1777,36 @@ test("keyed/forward relay body-read timeout returns 504 when upstream stalls aft // body-read catch must map to 504 (not 502) via the linkedSignal.signal.aborted // check — the abort surfaces as a generic AbortError, not TimeoutError. globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { const fetchSignal = init?.signal; const stalledBody = new ReadableStream({ start(controller) { - const abortError = new DOMException("The operation was aborted.", "AbortError"); + const abortError = new DOMException( + "The operation was aborted.", + "AbortError", + ); if (fetchSignal) { if (fetchSignal.aborted) controller.error(abortError); - else fetchSignal.addEventListener("abort", () => controller.error(abortError), { once: true }); + else + fetchSignal.addEventListener( + "abort", + () => controller.error(abortError), + { once: true }, + ); } }, }); - return new Response(stalledBody, { status: 200, headers: { "content-type": "application/json" } }); + return new Response(stalledBody, { + status: 200, + headers: { "content-type": "application/json" }, + }); } return originalFetch(input, init); }) as typeof fetch; @@ -1409,13 +1824,16 @@ test("keyed/forward relay body-read timeout returns 504 when upstream stalls aft const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", model: "gpt-image-2" }), + }, + ); expect(response.status).toBe(504); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/body read|timed out/i); } finally { await server.stop(true); @@ -1433,20 +1851,36 @@ test("keyed/forward relay body-read client cancellation returns 499, not 504", a // fetch abort tears down the connection before the server response can be read. const { handleImages } = await import("../src/server/images"); globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "api.openai.com" && url.pathname.startsWith("/v1")) { const fetchSignal = init?.signal; const stalledBody = new ReadableStream({ start(controller) { - const abortError = new DOMException("The operation was aborted.", "AbortError"); + const abortError = new DOMException( + "The operation was aborted.", + "AbortError", + ); if (fetchSignal) { if (fetchSignal.aborted) controller.error(abortError); - else fetchSignal.addEventListener("abort", () => controller.error(abortError), { once: true }); + else + fetchSignal.addEventListener( + "abort", + () => controller.error(abortError), + { once: true }, + ); } }, }); - return new Response(stalledBody, { status: 200, headers: { "content-type": "application/json" } }); + return new Response(stalledBody, { + status: 200, + headers: { "content-type": "application/json" }, + }); } return originalFetch(input, init); }) as typeof fetch; @@ -1479,7 +1913,7 @@ test("keyed/forward relay body-read client cancellation returns 499, not 504", a setTimeout(() => ctrl.abort(), 100); const response = await responsePromise; expect(response.status).toBe(499); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("canceled"); }, 5_000); @@ -1491,7 +1925,12 @@ test("CCA body-read timeout returns 504 when upstream stalls after sending heade // signal-state check (linkedSignal.signal.aborted) is what maps it, not // err.name matching. globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "daily-cloudcode-pa.googleapis.com") { const fetchSignal = init?.signal; @@ -1500,12 +1939,19 @@ test("CCA body-read timeout returns 504 when upstream stalls after sending heade // Never produce data — stall until the fetch signal aborts, then // error the stream as AbortError (the typical rejection Bun's // stream layer produces on linked-signal abort, NOT TimeoutError). - const abortError = new DOMException("The operation was aborted.", "AbortError"); + const abortError = new DOMException( + "The operation was aborted.", + "AbortError", + ); if (fetchSignal) { if (fetchSignal.aborted) { controller.error(abortError); } else { - fetchSignal.addEventListener("abort", () => controller.error(abortError), { once: true }); + fetchSignal.addEventListener( + "abort", + () => controller.error(abortError), + { once: true }, + ); } } }, @@ -1523,13 +1969,16 @@ test("CCA body-read timeout returns 504 when upstream stalls after sending heade const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(504); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; // Either the body-read timeout message or the general timeout message. expect(json.error.message).toMatch(/body read|timed out/i); } finally { @@ -1548,20 +1997,36 @@ test("CCA body-read client cancellation returns 499, not 504", async () => { // fetch abort tears down the connection before the server response can be read. const { handleImages } = await import("../src/server/images"); globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "daily-cloudcode-pa.googleapis.com") { const fetchSignal = init?.signal; const stalledBody = new ReadableStream({ start(controller) { - const abortError = new DOMException("The operation was aborted.", "AbortError"); + const abortError = new DOMException( + "The operation was aborted.", + "AbortError", + ); if (fetchSignal) { if (fetchSignal.aborted) controller.error(abortError); - else fetchSignal.addEventListener("abort", () => controller.error(abortError), { once: true }); + else + fetchSignal.addEventListener( + "abort", + () => controller.error(abortError), + { once: true }, + ); } }, }); - return new Response(stalledBody, { status: 200, headers: { "content-type": "application/json" } }); + return new Response(stalledBody, { + status: 200, + headers: { "content-type": "application/json" }, + }); } return originalFetch(input, init); }) as typeof fetch; @@ -1586,25 +2051,31 @@ test("CCA body-read client cancellation returns 499, not 504", async () => { setTimeout(() => ctrl.abort(), 100); const response = await responsePromise; expect(response.status).toBe(499); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("canceled"); }, 5_000); test("CCA image fallback preserves upstream 400 (not collapsed to 502)", async () => { const registryHits: CcaFetchRequest[] = []; const otherHits: CcaFetchRequest[] = []; - ccaFetchMock(registryHits, otherHits, { status: 400, payload: { error: { message: "Invalid prompt content" } } }); + ccaFetchMock(registryHits, otherHits, { + status: 400, + payload: { error: { message: "Invalid prompt content" } }, + }); saveConfig(ccaConfig()); await saveCredential("google-antigravity", { ...CCA_CREDENTIAL }); const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); // 400 must be forwarded, not collapsed to 502. expect(response.status).toBe(400); expect(registryHits).toHaveLength(1); @@ -1624,14 +2095,18 @@ test("CCA image response with malformed inlineData.data (non-string) returns 502 ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - content: { parts: [ - { inlineData: { mimeType: "image/png", data: 12345 } }, // number - { inlineData: { mimeType: "image/png", data: { foo: "bar" } } }, // object - { inlineData: { mimeType: "image/png", data: "" } }, // empty string - { inlineData: { mimeType: "image/png", data: null } }, // null - ] }, - }], + candidates: [ + { + content: { + parts: [ + { inlineData: { mimeType: "image/png", data: 12345 } }, // number + { inlineData: { mimeType: "image/png", data: { foo: "bar" } } }, // object + { inlineData: { mimeType: "image/png", data: "" } }, // empty string + { inlineData: { mimeType: "image/png", data: null } }, // null + ], + }, + }, + ], }, }, }); @@ -1641,13 +2116,16 @@ test("CCA image response with malformed inlineData.data (non-string) returns 502 const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("no image data"); expect(registryHits).toHaveLength(1); expect(otherHits).toHaveLength(0); @@ -1664,13 +2142,17 @@ test("CCA image response skips malformed inlineData.data but keeps valid string ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - content: { parts: [ - { inlineData: { mimeType: "image/png", data: 42 } }, - { inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }, - { inlineData: { mimeType: "image/png", data: "" } }, - ] }, - }], + candidates: [ + { + content: { + parts: [ + { inlineData: { mimeType: "image/png", data: 42 } }, + { inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }, + { inlineData: { mimeType: "image/png", data: "" } }, + ], + }, + }, + ], }, }, }); @@ -1680,13 +2162,16 @@ test("CCA image response skips malformed inlineData.data but keeps valid string const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(200); - const json = await response.json() as { data: { b64_json: string }[] }; + const json = (await response.json()) as { data: { b64_json: string }[] }; expect(json.data).toHaveLength(1); expect(json.data[0].b64_json).toBe(CCA_TINY_PNG); expect(registryHits).toHaveLength(1); @@ -1702,7 +2187,9 @@ test("CCA image response with non-array parts returns 502 (envelope validation)" // A truthy but non-array parts object would throw inside for...of before the // Array.isArray guard was added. It must be caught and surfaced as 502. ccaFetchMock(registryHits, otherHits, { - payload: { response: { candidates: [{ content: { parts: "not-an-array" } }] } }, + payload: { + response: { candidates: [{ content: { parts: "not-an-array" } }] }, + }, }); saveConfig(ccaConfig()); @@ -1710,13 +2197,16 @@ test("CCA image response with non-array parts returns 502 (envelope validation)" const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("no valid parts array"); expect(registryHits).toHaveLength(1); expect(otherHits).toHaveLength(0); @@ -1746,15 +2236,31 @@ const CCA_CREDENTIAL_EXPIRED = { */ function hungOauthFetchMock() { globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const requestUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; const url = new URL(requestUrl); if (url.hostname === "oauth2.googleapis.com") { // Never resolve until the fetch signal aborts (just like production). return new Promise((_resolve, reject) => { const sig = init?.signal; if (sig) { - if (sig.aborted) reject(new DOMException("The operation was aborted.", "AbortError")); - else sig.addEventListener("abort", () => reject(new DOMException("The operation was aborted.", "AbortError")), { once: true }); + if (sig.aborted) + reject( + new DOMException("The operation was aborted.", "AbortError"), + ); + else + sig.addEventListener( + "abort", + () => + reject( + new DOMException("The operation was aborted.", "AbortError"), + ), + { once: true }, + ); } }); } @@ -1789,7 +2295,7 @@ test("CCA client abort during OAuth preflight returns 499, not a hung response", setTimeout(() => ctrl.abort(), 100); const response = await responsePromise; expect(response.status).toBe(499); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("canceled"); }, 5_000); @@ -1814,7 +2320,7 @@ test("CCA deadline expiry during OAuth preflight returns 504, not a hung respons const response = await handleImages(req, cfg, "generations", logCtx); expect(response.status).toBe(504); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toContain("timed out"); }, 5_000); @@ -1829,10 +2335,12 @@ test("CCA finishReason SAFETY returns 400 (non-retryable), not 502", async () => ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - content: { parts: [] }, - finishReason: "SAFETY", - }], + candidates: [ + { + content: { parts: [] }, + finishReason: "SAFETY", + }, + ], }, }, }); @@ -1842,13 +2350,18 @@ test("CCA finishReason SAFETY returns 400 (non-retryable), not 502", async () => const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.type).toBe("invalid_request_error"); expect(json.error.message).toContain("safety filter"); expect(json.error.message).toContain("SAFETY"); @@ -1875,13 +2388,18 @@ test("CCA promptFeedback.blockReason returns 400 (non-retryable)", async () => { const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.type).toBe("invalid_request_error"); expect(json.error.message).toContain("safety filter"); expect(json.error.message).toContain("promptFeedback"); @@ -1908,13 +2426,18 @@ test("CCA finishReason BLOCKLIST returns 400 (non-retryable)", async () => { const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.type).toBe("invalid_request_error"); expect(json.error.message).toContain("BLOCKLIST"); expect(registryHits).toHaveLength(1); @@ -1932,10 +2455,12 @@ test("CCA blocked candidate with empty content.parts returns 400, not 502", asyn ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - finishReason: "PROHIBITED_CONTENT", - // content is entirely absent — common when generation is blocked - }], + candidates: [ + { + finishReason: "PROHIBITED_CONTENT", + // content is entirely absent — common when generation is blocked + }, + ], }, }, }); @@ -1945,13 +2470,18 @@ test("CCA blocked candidate with empty content.parts returns 400, not 502", asyn const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { type: string; message: string } }; + const json = (await response.json()) as { + error: { type: string; message: string }; + }; expect(json.error.type).toBe("invalid_request_error"); expect(json.error.message).toContain("PROHIBITED_CONTENT"); expect(registryHits).toHaveLength(1); @@ -1969,10 +2499,16 @@ test("CCA finishReason STOP with valid image is not affected by safety block log ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - content: { parts: [{ inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }] }, - finishReason: "STOP", - }], + candidates: [ + { + content: { + parts: [ + { inlineData: { mimeType: "image/png", data: CCA_TINY_PNG } }, + ], + }, + finishReason: "STOP", + }, + ], }, }, }); @@ -1982,13 +2518,16 @@ test("CCA finishReason STOP with valid image is not affected by safety block log const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(200); - const json = await response.json() as { data: { b64_json: string }[] }; + const json = (await response.json()) as { data: { b64_json: string }[] }; expect(json.data).toHaveLength(1); expect(json.data[0].b64_json).toBe(CCA_TINY_PNG); expect(registryHits).toHaveLength(1); @@ -2009,18 +2548,27 @@ test("CCA-only request with proxy admission bearer succeeds and never sends it u const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { - "content-type": "application/json", - authorization: "Bearer proxy-admission-secret", + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer proxy-admission-secret", + }, + body: JSON.stringify({ prompt: "a cat" }), }, - body: JSON.stringify({ prompt: "a cat" }), - }); + ); expect(response.status).toBe(200); expect(registryHits).toHaveLength(1); - expect([...registryHits[0].headers.values()].some(v => v.includes("proxy-admission-secret"))).toBe(false); - expect(registryHits[0].headers.get("authorization")).toBe("Bearer cca-access-token"); + expect( + [...registryHits[0].headers.values()].some((v) => + v.includes("proxy-admission-secret"), + ), + ).toBe(false); + expect(registryHits[0].headers.get("authorization")).toBe( + "Bearer cca-access-token", + ); } finally { await server.stop(true); delete process.env.OPENCODEX_API_AUTH_TOKEN; @@ -2038,13 +2586,16 @@ test("CCA logged-in without projectId returns project-discovery error, not provi const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/Cloud Code Assist project/i); expect(json.error.message).not.toMatch(/none is configured/i); expect(registryHits).toHaveLength(0); @@ -2063,13 +2614,16 @@ test("CCA rejects n>1 before contacting Google", async () => { const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat", n: 2 }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat", n: 2 }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/n=1/i); expect(registryHits).toHaveLength(0); } finally { @@ -2093,13 +2647,16 @@ test("CCA RECITATION finishReason returns non-retryable 400", async () => { const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "copyrighted stuff" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "copyrighted stuff" }), + }, + ); expect(response.status).toBe(400); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/RECITATION|safety/i); } finally { await server.stop(true); @@ -2112,9 +2669,15 @@ test("CCA rejects invalid base64 and non-image bytes instead of returning b64_js ccaFetchMock(registryHits, otherHits, { payload: { response: { - candidates: [{ - content: { parts: [{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } }] }, - }], + candidates: [ + { + content: { + parts: [ + { inlineData: { mimeType: "image/png", data: "aGVsbG8=" } }, + ], + }, + }, + ], }, }, }); @@ -2124,13 +2687,16 @@ test("CCA rejects invalid base64 and non-image bytes instead of returning b64_js const server = startServer(0); try { - const response = await fetch(new URL("/v1/images/generations", server.url), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ prompt: "a cat" }), - }); + const response = await fetch( + new URL("/v1/images/generations", server.url), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "a cat" }), + }, + ); expect(response.status).toBe(502); - const json = await response.json() as { error: { message: string } }; + const json = (await response.json()) as { error: { message: string } }; expect(json.error.message).toMatch(/base64|magic|validation/i); } finally { await server.stop(true); @@ -2139,8 +2705,12 @@ test("CCA rejects invalid base64 and non-image bytes instead of returning b64_js test("GET /v1/opencodex/artifacts/:id serves opaque artifacts with API auth", async () => { process.env.OPENCODEX_API_AUTH_TOKEN = "proxy-admission-secret"; - const { materializeInlineImage, createImageBudget, artifactHttpUrl } = await import("../src/images/artifacts"); - const filePath = await materializeInlineImage(CCA_TINY_PNG, createImageBudget()); + const { materializeInlineImage, createImageBudget, artifactHttpUrl } = + await import("../src/images/artifacts"); + const filePath = await materializeInlineImage( + CCA_TINY_PNG, + createImageBudget(), + ); const urlPath = artifactHttpUrl(filePath); // Non-loopback bind makes data-plane auth mandatory (same as production remote binds). @@ -2164,9 +2734,12 @@ test("GET /v1/opencodex/artifacts/:id serves opaque artifacts with API auth", as expect(bytes[0]).toBe(0x89); expect(bytes[1]).toBe(0x50); - const traversal = await fetch(`http://127.0.0.1:${server.port}/v1/opencodex/artifacts/../package.json`, { - headers: { authorization: "Bearer proxy-admission-secret" }, - }); + const traversal = await fetch( + `http://127.0.0.1:${server.port}/v1/opencodex/artifacts/../package.json`, + { + headers: { authorization: "Bearer proxy-admission-secret" }, + }, + ); expect(traversal.status).toBe(404); } finally { await server.stop(true); From cc5f8a2eba906dd5074c7fb49b48786dfec241a4 Mon Sep 17 00:00:00 2001 From: chefadmin-netizen Date: Tue, 8 Sep 2026 02:38:28 +0200 Subject: [PATCH 4/4] fix: close accepted sockets during startup port probes --- docs-site/src/content/docs/reference/cli.md | 101 +++++++------- src/server/ports.ts | 36 +++-- tests/ports.test.ts | 109 ++++++++++++++-- tests/shutdown-launcher.test.ts | 138 ++++++++++++-------- 4 files changed, 263 insertions(+), 121 deletions(-) diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index b703e735..074e3b12 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -18,8 +18,11 @@ autostart shim. ### `ocx start [--port ]` -Start the proxy server (preferred port `10100`). If that port is occupied, opencodex selects and -records another available port. It writes PID/runtime-port state and refuses to start a second live +Start the proxy server (preferred port `10100`). Without `--port`, if that port is occupied, +opencodex selects and records another available port. An explicit `--port` stays pinned and +fails if it remains busy; it never silently switches ports. Temporary availability probes close +incoming discovery connections immediately so a scanner cannot keep startup waiting. +It writes PID/runtime-port state and refuses to start a second live instance. On start it syncs each provider's models into Codex's catalog. On shutdown it restores native Codex — unless it was launched as a managed service (`OCX_SERVICE=1`). @@ -162,15 +165,15 @@ same stale-`app-server` warning and optional `--restart-codex` behavior as `ocx Manage the Codex `multi_agent_v2` feature flag and the 3-state multi-agent surface mode. -| Subcommand | Action | -| --- | --- | -| `status` (default) | Report the current v2 flag, multi-agent mode, and thread concurrency. | -| `on` | Enable the `multi_agent_v2` feature in `$CODEX_HOME/config.toml` and resync the catalog. | -| `off` | Disable the `multi_agent_v2` feature and resync. | -| `mode v1` | Force ALL models to v1, disable native v2, and preserve the thread limit under `[agents] max_threads`. | -| `mode default` | Respect upstream model pins (sol/terra=v2, luna=v1, rest=codex flag). Install default. | -| `mode v2` | Force ALL models to v2, enable native v2, and migrate the same thread limit to the v2 key. | -| `threads ` | Set the active v1/v2 thread limit (integer >= 1). | +| Subcommand | Action | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| `status` (default) | Report the current v2 flag, multi-agent mode, and thread concurrency. | +| `on` | Enable the `multi_agent_v2` feature in `$CODEX_HOME/config.toml` and resync the catalog. | +| `off` | Disable the `multi_agent_v2` feature and resync. | +| `mode v1` | Force ALL models to v1, disable native v2, and preserve the thread limit under `[agents] max_threads`. | +| `mode default` | Respect upstream model pins (sol/terra=v2, luna=v1, rest=codex flag). Install default. | +| `mode v2` | Force ALL models to v2, enable native v2, and migrate the same thread limit to the v2 key. | +| `threads ` | Set the active v1/v2 thread limit (integer >= 1). | ```bash ocx v2 status @@ -192,16 +195,16 @@ Operational dashboard features are also available without a browser. These comma identity-checked running proxy (including a fallback runtime port) and reuse the same management routes, validation, live configuration, and catalog refresh side effects as the GUI. -| Resource | Commands | -| --- | --- | -| Routing | `ocx combo ...` or `ocx route combo ...` | -| Agent policy | `ocx agent injection|effort|subagents|fallback|sidecar ...` | -| Observability | `ocx observe logs|usage|storage|memory|cache|debug ...` | -| API admission | `ocx access key|endpoints|models|test ...` | -| Claude Code | `ocx claude config status|set ...` | -| Grok Build | `ocx grok status|exclude|include|set|clear|apply ...` | -| Runtime control | `ocx system status|settings|startup|diagnostics|sync|update ...` | -| Offline config | `ocx config show|get|set|unset|validate|export|import ...` | +| Resource | Commands | +| --------------- | ---------------------------------------- | +| Routing | `ocx combo ...` or `ocx route combo ...` | +| Agent policy | `ocx agent injection | effort | subagents | fallback | sidecar ...` | +| Observability | `ocx observe logs | usage | storage | memory | cache | debug ...` | +| API admission | `ocx access key | endpoints | models | test ...` | +| Claude Code | `ocx claude config status | set ...` | +| Grok Build | `ocx grok status | exclude | include | set | clear | apply ...` | +| Runtime control | `ocx system status | settings | startup | diagnostics | sync | update ...` | +| Offline config | `ocx config show | get | set | unset | validate | export | import ...` | List/status is the default where unambiguous. Use `--json` for structured snapshots and `ocx observe logs --follow --jsonl` for a streaming request-log feed. Destructive removal/import, @@ -234,19 +237,19 @@ caps; and `shadow` manages background shadow-call interception. Non-interactive provider management. Registry entries are seeded by name; a custom name requires both `--adapter` and `--base-url`. -| Subcommand | Supported flags | Action | -| --- | --- | --- | -| `list` | `--json` | List configured providers and the remaining registry entries. | -| `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Add a registry/custom provider. `--force` overwrites; `--sync` refreshes a running proxy in human-output mode. | -| `edit ` | provider field flags, `--json` | Edit validated live provider fields without replacing key pools. | -| `test ` | `--json` | Probe the real upstream model endpoint. | -| `show ` | `--json` | Show config with API keys masked. | -| `remove ` | `--json` | Remove a non-default provider; the last provider cannot be removed. | -| `set-default ` | `--json` | Select an existing provider as the default. | -| `selected ` | `--set `, `--clear`, `--json` | Read or update the provider model allowlist. | -| `quota` | `--refresh`, `--json` | Read provider quota reports. | -| `presets` | `--json` | List dashboard provider presets. | -| `account-mode` | `pool`, `direct`, `--json` | Select pooled or direct Codex account routing. | +| Subcommand | Supported flags | Action | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `list` | `--json` | List configured providers and the remaining registry entries. | +| `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Add a registry/custom provider. `--force` overwrites; `--sync` refreshes a running proxy in human-output mode. | +| `edit ` | provider field flags, `--json` | Edit validated live provider fields without replacing key pools. | +| `test ` | `--json` | Probe the real upstream model endpoint. | +| `show ` | `--json` | Show config with API keys masked. | +| `remove ` | `--json` | Remove a non-default provider; the last provider cannot be removed. | +| `set-default ` | `--json` | Select an existing provider as the default. | +| `selected ` | `--set `, `--clear`, `--json` | Read or update the provider model allowlist. | +| `quota` | `--refresh`, `--json` | Read provider quota reports. | +| `presets` | `--json` | List dashboard provider presets. | +| `account-mode` | `pool`, `direct`, `--json` | Select pooled or direct Codex account routing. | ```bash ocx provider list --json @@ -422,16 +425,16 @@ Run opencodex as a login-managed background service (macOS **launchd**, Linux ** Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash. Service runs set `OCX_SERVICE=1` so a restart doesn't churn the Codex config. -| Subcommand | Action | -| --- | --- | -| none | Create/update and start the service. | -| `install` | Create and start the service. | -| `start` | Start an installed service. | -| `stop` | Stop the service and restore native Codex. | -| `status` | Report whether the service is running. | -| `repair` | Refresh installed service assets without re-registering (no Task Scheduler UAC). | -| `uninstall` | Remove the service and restore native Codex. | -| `remove` | Alias of `uninstall`. | +| Subcommand | Action | +| ----------- | -------------------------------------------------------------------------------- | +| none | Create/update and start the service. | +| `install` | Create and start the service. | +| `start` | Start an installed service. | +| `stop` | Stop the service and restore native Codex. | +| `status` | Report whether the service is running. | +| `repair` | Refresh installed service assets without re-registering (no Task Scheduler UAC). | +| `uninstall` | Remove the service and restore native Codex. | +| `remove` | Alias of `uninstall`. | ```bash ocx service @@ -460,12 +463,12 @@ changing is left untouched and retried later. Repair failures warn without faili command; manual fallback: `ocx codex-shim install`. Set `codexShimAutoRestore` to `false`, or set `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0` for a process-level opt-out. -| Subcommand | Action | -| --- | --- | -| `install` | Install the shim (or repair if stale). | +| Subcommand | Action | +| ----------- | ------------------------------------------------------ | +| `install` | Install the shim (or repair if stale). | | `uninstall` | Remove the shim and restore the original Codex binary. | -| `remove` | Alias of `uninstall`. | -| `status` | Report shim state (installed / stale / missing). | +| `remove` | Alias of `uninstall`. | +| `status` | Report shim state (installed / stale / missing). | ```bash ocx codex-shim install diff --git a/src/server/ports.ts b/src/server/ports.ts index bd96590b..04289584 100644 --- a/src/server/ports.ts +++ b/src/server/ports.ts @@ -13,9 +13,17 @@ export function isAddrInUse(err: unknown): boolean { return text.includes("eaddrinuse") || text.includes("in use"); } -export async function isPortAvailable(port: number, hostname = "127.0.0.1"): Promise { - return await new Promise(resolve => { +export async function isPortAvailable( + port: number, + hostname = "127.0.0.1", +): Promise { + return await new Promise((resolve) => { const server = createServer(); + // A local readiness or desktop probe can connect in the small interval after bind + // succeeds but before close runs. `server.close()` waits for that accepted socket, + // so discard all probe traffic rather than letting port selection hang on a client + // that never sends or closes. + server.on("connection", (socket) => socket.destroy()); server.once("error", () => resolve(false)); server.once("listening", () => { server.close(() => resolve(true)); @@ -41,7 +49,7 @@ export async function waitForPortAvailable( for (;;) { if (await isPortAvailable(port, hostname)) return true; if (Date.now() >= deadline) return false; - await new Promise(resolve => setTimeout(resolve, intervalMs)); + await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } @@ -60,7 +68,9 @@ export type FindAvailablePortOptions = { export class PortUnavailableError extends Error { readonly port: number; constructor(port: number, hostname: string) { - super(`Port ${port} on ${hostname} is still busy after prefer-retry; refusing ephemeral fallback.`); + super( + `Port ${port} on ${hostname} is still busy after prefer-retry; refusing ephemeral fallback.`, + ); this.name = "PortUnavailableError"; this.port = port; } @@ -76,13 +86,18 @@ export async function findAvailablePort( // Port 0 asks the OS to select an ephemeral port. Resolve it to that concrete // port here so callers never persist or advertise an unusable `:0` endpoint. if (preferredPort > 0 && preferRetryMs > 0) { - if (await waitForPortAvailable(preferredPort, hostname, { - timeoutMs: preferRetryMs, - intervalMs: opts.preferRetryIntervalMs ?? 50, - })) { + if ( + await waitForPortAvailable(preferredPort, hostname, { + timeoutMs: preferRetryMs, + intervalMs: opts.preferRetryIntervalMs ?? 50, + }) + ) { return preferredPort; } - } else if (preferredPort > 0 && (await isPortAvailable(preferredPort, hostname))) { + } else if ( + preferredPort > 0 && + (await isPortAvailable(preferredPort, hostname)) + ) { return preferredPort; } @@ -92,6 +107,9 @@ export async function findAvailablePort( return await new Promise((resolve, reject) => { const server = createServer(); + // See isPortAvailable: this is also a short-lived probe listener, not a server + // that should retain readiness-scanner connections while closing. + server.on("connection", (socket) => socket.destroy()); server.once("error", reject); server.once("listening", () => { const address = server.address(); diff --git a/tests/ports.test.ts b/tests/ports.test.ts index f95090bd..47a6dbb4 100644 --- a/tests/ports.test.ts +++ b/tests/ports.test.ts @@ -1,11 +1,24 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { createServer, type Server } from "node:net"; -import { findAvailablePort, isAddrInUse, isPortAvailable, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../src/server/ports"; +import { + createConnection, + createServer, + Server as NetServer, + type Server, + type Socket, +} from "node:net"; +import { + findAvailablePort, + isAddrInUse, + isPortAvailable, + PortUnavailableError, + shouldPersistSelectedPort, + waitForPortAvailable, +} from "../src/server/ports"; const servers: Server[] = []; function close(server: Server): Promise { - return new Promise(resolve => server.close(() => resolve())); + return new Promise((resolve) => server.close(() => resolve())); } function listen(port = 0): Promise<{ server: Server; port: number }> { @@ -25,11 +38,62 @@ function listen(port = 0): Promise<{ server: Server; port: number }> { }); } +/** + * Make the probe listener accept a real socket immediately before its first close. + * Without the probe connection handler in ports.ts, the original close callback waits + * for this client forever; this keeps the timing regression deterministic. + */ +async function expectProbeToClosePastAcceptedSocket( + runProbe: () => Promise, +): Promise { + const originalClose = NetServer.prototype.close as unknown as ( + this: Server, + callback?: () => void, + ) => Server; + let injected = false; + let client: Socket | undefined; + let timer: ReturnType | undefined; + NetServer.prototype.close = function ( + this: Server, + callback?: () => void, + ): Server { + if (injected) return originalClose.call(this, callback); + injected = true; + const address = this.address(); + if (!address || typeof address === "string") + throw new Error("probe server was not listening"); + client = createConnection(address.port, "127.0.0.1"); + client.on("error", () => {}); + client.once("connect", () => originalClose.call(this, callback)); + return this; + } as unknown as typeof NetServer.prototype.close; + try { + const completed = await Promise.race([ + runProbe().then(() => true), + new Promise((resolve) => { + timer = setTimeout(() => resolve(false), 1_000); + }), + ]); + expect(injected).toBe(true); + expect(completed).toBe(true); + } finally { + clearTimeout(timer); + NetServer.prototype.close = + originalClose as unknown as typeof NetServer.prototype.close; + client?.destroy(); + } +} + afterEach(async () => { await Promise.all(servers.splice(0).map(close)); }); describe("port selection", () => { + test("probe listeners close despite a connection accepted immediately before close", async () => { + await expectProbeToClosePastAcceptedSocket(() => isPortAvailable(0)); + await expectProbeToClosePastAcceptedSocket(() => findAvailablePort(0)); + }); + test("resolves port 0 to a concrete ephemeral port", async () => { const selected = await findAvailablePort(0); @@ -66,7 +130,10 @@ describe("port selection", () => { const { server, port } = await listen(); expect(await isPortAvailable(port)).toBe(false); - const waiting = waitForPortAvailable(port, "127.0.0.1", { timeoutMs: 2000, intervalMs: 25 }); + const waiting = waitForPortAvailable(port, "127.0.0.1", { + timeoutMs: 2000, + intervalMs: 25, + }); await close(server); const idx = servers.indexOf(server); if (idx >= 0) servers.splice(idx, 1); @@ -77,7 +144,12 @@ describe("port selection", () => { test("waitForPortAvailable returns false when the port stays busy past the timeout", async () => { const { port } = await listen(); - await expect(waitForPortAvailable(port, "127.0.0.1", { timeoutMs: 80, intervalMs: 20 })).resolves.toBe(false); + await expect( + waitForPortAvailable(port, "127.0.0.1", { + timeoutMs: 80, + intervalMs: 20, + }), + ).resolves.toBe(false); expect(await isPortAvailable(port)).toBe(false); }); @@ -85,7 +157,10 @@ describe("port selection", () => { const { server, port } = await listen(); expect(await isPortAvailable(port)).toBe(false); - const pending = findAvailablePort(port, "127.0.0.1", { preferRetryMs: 500, preferRetryIntervalMs: 25 }); + const pending = findAvailablePort(port, "127.0.0.1", { + preferRetryMs: 500, + preferRetryIntervalMs: 25, + }); // Free the preferred port during the retry window. setTimeout(() => { void close(server).then(() => { @@ -110,10 +185,24 @@ describe("port selection", () => { }); test("isAddrInUse recognizes bind conflicts by code or message and rejects everything else", () => { - expect(isAddrInUse(Object.assign(new Error("listen failed"), { code: "EADDRINUSE" }))).toBe(true); - expect(isAddrInUse(new Error("listen EADDRINUSE: address already in use ::1:8123"))).toBe(true); - expect(isAddrInUse(new Error("Failed to start server. Is port 8123 in use?"))).toBe(true); - expect(isAddrInUse(Object.assign(new Error("no ipv6"), { code: "EAFNOSUPPORT" }))).toBe(false); + expect( + isAddrInUse( + Object.assign(new Error("listen failed"), { code: "EADDRINUSE" }), + ), + ).toBe(true); + expect( + isAddrInUse( + new Error("listen EADDRINUSE: address already in use ::1:8123"), + ), + ).toBe(true); + expect( + isAddrInUse(new Error("Failed to start server. Is port 8123 in use?")), + ).toBe(true); + expect( + isAddrInUse( + Object.assign(new Error("no ipv6"), { code: "EAFNOSUPPORT" }), + ), + ).toBe(false); expect(isAddrInUse(new Error("permission denied"))).toBe(false); expect(isAddrInUse(null)).toBe(false); expect(isAddrInUse("EADDRINUSE")).toBe(false); diff --git a/tests/shutdown-launcher.test.ts b/tests/shutdown-launcher.test.ts index 2ba68a2d..fad4ec72 100644 --- a/tests/shutdown-launcher.test.ts +++ b/tests/shutdown-launcher.test.ts @@ -1,6 +1,12 @@ import { afterAll, describe, expect, test } from "bun:test"; import { spawn, spawnSync, type ChildProcess } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -19,7 +25,8 @@ import { join } from "node:path"; */ const BIN_OCX = join(import.meta.dir, "..", "bin", "ocx.mjs"); -const nodeAvailable = !spawnSync("node", ["--version"], { stdio: "ignore" }).error; +const nodeAvailable = !spawnSync("node", ["--version"], { stdio: "ignore" }) + .error; const runnable = process.platform !== "win32" && nodeAvailable; const spawned: ChildProcess[] = []; @@ -27,10 +34,20 @@ const tmpHomes: string[] = []; afterAll(() => { for (const c of spawned) { - try { c.kill("SIGKILL"); } catch { /* already gone */ } + // The test signals only the launcher below. Failure cleanup must also reap a + // Bun child that never reached readiness, rather than orphaning it. + try { + if (c.pid) process.kill(-c.pid, "SIGKILL"); + } catch { + /* already gone */ + } } for (const dir of tmpHomes) { - try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ } + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + /* best-effort */ + } } }); @@ -57,7 +74,10 @@ async function healthy(port: number): Promise { } } -async function waitUntil(fn: () => Promise, deadlineMs: number): Promise { +async function waitUntil( + fn: () => Promise, + deadlineMs: number, +): Promise { const end = Date.now() + deadlineMs; while (Date.now() < end) { if (await fn()) return true; @@ -68,53 +88,65 @@ async function waitUntil(fn: () => Promise, deadlineMs: number): Promis describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) { - test( - `${signal} to the launcher tears down the Bun proxy and restores Codex config (no orphan)`, - async () => { - const home = mkdtempSync(join(tmpdir(), "ocx-shutdown-")); - tmpHomes.push(home); - const port = await freePort(); - - // Seed a native Codex config so the proxy actually injects on start (injectCodexConfig - // no-ops when no config.toml exists) — this lets us prove the config is RESTORED. - const codexConfig = join(home, "config.toml"); - writeFileSync(codexConfig, 'model = "gpt-5.1"\n'); - - const child = spawn("node", [BIN_OCX, "start", "--port", String(port)], { - stdio: "ignore", - env: { ...process.env, OPENCODEX_HOME: home, CODEX_HOME: home }, - }); - spawned.push(child); - - let exited = false; - child.on("exit", () => { exited = true; }); - - // 1. Proxy comes up + injected the Codex config (Design B root override on loopback). - const up = await waitUntil(() => healthy(port), 20_000); - expect(up).toBe(true); - expect(existsSync(join(home, "ocx.pid"))).toBe(true); - const injected = readFileSync(codexConfig, "utf8"); - expect(injected).toContain("# Auto-injected by opencodex"); - expect(injected).toContain(`openai_base_url = "http://127.0.0.1:${port}/v1"`); - expect(injected).not.toContain("model_providers.opencodex"); - - // 2. Signal ONLY the launcher PID (the exact orphan trigger). - child.kill(signal); - - // 3. Launcher exits... - const launcherGone = await waitUntil(async () => exited, 15_000); - expect(launcherGone).toBe(true); - - // 4. ...and the Bun proxy is gone (port freed) — the regression guard. - const portFreed = await waitUntil(async () => !(await healthy(port)), 10_000); - expect(portFreed).toBe(true); - - // 5. Graceful cleanup ran: pid + runtime-port removed, Codex config restored. - expect(existsSync(join(home, "ocx.pid"))).toBe(false); - expect(existsSync(join(home, "runtime-port.json"))).toBe(false); - expect(readFileSync(codexConfig, "utf8")).not.toContain("opencodex"); - }, - 45_000, - ); + test(`${signal} to the launcher tears down the Bun proxy and restores Codex config (no orphan)`, async () => { + const home = mkdtempSync(join(tmpdir(), "ocx-shutdown-")); + tmpHomes.push(home); + const port = await freePort(); + + // Seed a native Codex config so the proxy actually injects on start (injectCodexConfig + // no-ops when no config.toml exists) — this lets us prove the config is RESTORED. + const codexConfig = join(home, "config.toml"); + writeFileSync(codexConfig, 'model = "gpt-5.1"\n'); + + const child = spawn("node", [BIN_OCX, "start", "--port", String(port)], { + detached: true, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, OPENCODEX_HOME: home, CODEX_HOME: home }, + }); + spawned.push(child); + let startupOutput = ""; + const capture = (chunk: Buffer) => { + startupOutput = (startupOutput + chunk.toString()).slice(-8_192); + }; + child.stdout?.on("data", capture); + child.stderr?.on("data", capture); + + let exited = false; + child.on("exit", () => { + exited = true; + }); + + // 1. Proxy comes up + injected the Codex config (Design B root override on loopback). + const up = await waitUntil(() => healthy(port), 20_000); + expect(up, `launcher did not become healthy: ${startupOutput}`).toBe( + true, + ); + expect(existsSync(join(home, "ocx.pid"))).toBe(true); + const injected = readFileSync(codexConfig, "utf8"); + expect(injected).toContain("# Auto-injected by opencodex"); + expect(injected).toContain( + `openai_base_url = "http://127.0.0.1:${port}/v1"`, + ); + expect(injected).not.toContain("model_providers.opencodex"); + + // 2. Signal ONLY the launcher PID (the exact orphan trigger). + child.kill(signal); + + // 3. Launcher exits... + const launcherGone = await waitUntil(async () => exited, 15_000); + expect(launcherGone).toBe(true); + + // 4. ...and the Bun proxy is gone (port freed) — the regression guard. + const portFreed = await waitUntil( + async () => !(await healthy(port)), + 10_000, + ); + expect(portFreed).toBe(true); + + // 5. Graceful cleanup ran: pid + runtime-port removed, Codex config restored. + expect(existsSync(join(home, "ocx.pid"))).toBe(false); + expect(existsSync(join(home, "runtime-port.json"))).toBe(false); + expect(readFileSync(codexConfig, "utf8")).not.toContain("opencodex"); + }, 45_000); } });