From 668d05f9736d08dfb6fdc443f74d694c3380827d Mon Sep 17 00:00:00 2001 From: BinBandit Date: Mon, 9 Mar 2026 14:44:45 +1100 Subject: [PATCH] fix(server): soften Codex CLI version enforcement T3 Code 0.0.5 regressed enterprise and internal Codex builds by hard blocking session startup whenever codex-cli 0.111.0 reported a parsable version below the open-source minimum, even if the CLI still worked. Remove the synchronous startup gate and keep the minimum-version check as a provider-health warning instead, so users still see compatibility feedback without losing access to working internal Codex forks. Closes #604 --- apps/server/src/codexAppServerManager.test.ts | 52 --------------- apps/server/src/codexAppServerManager.ts | 66 ------------------- .../provider/Layers/ProviderHealth.test.ts | 9 +-- .../src/provider/Layers/ProviderHealth.ts | 19 ++---- 4 files changed, 12 insertions(+), 134 deletions(-) diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 4e0073d75ed..39d1b010bd0 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -317,58 +317,6 @@ describe("startSession", () => { } }); - it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { - const manager = new CodexAppServerManager(); - const events: Array<{ method: string; kind: string; message?: string }> = []; - manager.on("event", (event) => { - events.push({ - method: event.method, - kind: event.kind, - ...(event.message ? { message: event.message } : {}), - }); - }); - - const versionCheck = vi - .spyOn( - manager as unknown as { - assertSupportedCodexCliVersion: (input: { - binaryPath: string; - cwd: string; - homePath?: string; - }) => void; - }, - "assertSupportedCodexCliVersion", - ) - .mockImplementation(() => { - throw new Error( - "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", - ); - }); - - try { - await expect( - manager.startSession({ - threadId: asThreadId("thread-1"), - provider: "codex", - runtimeMode: "full-access", - }), - ).rejects.toThrow( - "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", - ); - expect(versionCheck).toHaveBeenCalledTimes(1); - expect(events).toEqual([ - { - method: "session/startFailed", - kind: "error", - message: - "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", - }, - ]); - } finally { - versionCheck.mockRestore(); - manager.stopAll(); - } - }); }); describe("sendTurn", () => { diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index a8a8ce4607b..c80b9f75f82 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -22,12 +22,6 @@ import { import { normalizeModelSlug } from "@t3tools/shared/model"; import { Effect, ServiceMap } from "effect"; -import { - formatCodexCliUpgradeMessage, - isCodexCliVersionSupported, - parseCodexCliVersion, -} from "./provider/codexCliVersion"; - type PendingRequestKey = string; interface PendingRequest { @@ -144,8 +138,6 @@ export interface CodexThreadSnapshot { turns: CodexThreadTurnSnapshot[]; } -const CODEX_VERSION_CHECK_TIMEOUT_MS = 4_000; - const ANSI_ESCAPE_CHAR = String.fromCharCode(27); const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); const CODEX_STDERR_LOG_REGEX = @@ -543,11 +535,6 @@ export class CodexAppServerManager extends EventEmitter): void { context.session = { ...context.session, @@ -1521,51 +1500,6 @@ function readCodexProviderOptions(input: CodexAppServerStartSessionInput): { }; } -function assertSupportedCodexCliVersion(input: { - readonly binaryPath: string; - readonly cwd: string; - readonly homePath?: string; -}): void { - const result = spawnSync(input.binaryPath, ["--version"], { - cwd: input.cwd, - env: { - ...process.env, - ...(input.homePath ? { CODEX_HOME: input.homePath } : {}), - }, - encoding: "utf8", - shell: process.platform === "win32", - stdio: ["ignore", "pipe", "pipe"], - timeout: CODEX_VERSION_CHECK_TIMEOUT_MS, - maxBuffer: 1024 * 1024, - }); - - if (result.error) { - const lower = result.error.message.toLowerCase(); - if ( - lower.includes("enoent") || - lower.includes("command not found") || - lower.includes("not found") - ) { - throw new Error(`Codex CLI (${input.binaryPath}) is not installed or not executable.`); - } - throw new Error( - `Failed to execute Codex CLI version check: ${result.error.message || String(result.error)}`, - ); - } - - const stdout = result.stdout ?? ""; - const stderr = result.stderr ?? ""; - if (result.status !== 0) { - const detail = stderr.trim() || stdout.trim() || `Command exited with code ${result.status}.`; - throw new Error(`Codex CLI version check failed. ${detail}`); - } - - const parsedVersion = parseCodexCliVersion(`${stdout}\n${stderr}`); - if (parsedVersion && !isCodexCliVersionSupported(parsedVersion)) { - throw new Error(formatCodexCliUpgradeMessage(parsedVersion)); - } -} - function readResumeCursorThreadId(resumeCursor: unknown): string | undefined { if (!resumeCursor || typeof resumeCursor !== "object" || Array.isArray(resumeCursor)) { return undefined; diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 90df9b691fe..cc74947e674 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -85,13 +85,13 @@ it.effect("returns unavailable when codex is missing", () => }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), ); -it.effect("returns unavailable when codex is below the minimum supported version", () => +it.effect("returns a warning when codex is below the minimum supported version", () => Effect.gen(function* () { const status = yield* checkCodexProviderStatus; assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - assert.strictEqual(status.authStatus, "unknown"); + assert.strictEqual(status.status, "warning"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "authenticated"); assert.strictEqual( status.message, "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", @@ -101,6 +101,7 @@ it.effect("returns unavailable when codex is below the minimum supported version mockSpawnerLayer((args) => { const joined = args.join(" "); if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 }; + if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 }; throw new Error(`Unexpected args: ${joined}`); }), ), diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index 59f41edf81e..7060096b761 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -253,16 +253,10 @@ export const checkCodexProviderStatus: Effect.Effect< } const parsedVersion = parseCodexCliVersion(`${version.stdout}\n${version.stderr}`); - if (parsedVersion && !isCodexCliVersionSupported(parsedVersion)) { - return { - provider: CODEX_PROVIDER, - status: "error" as const, - available: false, - authStatus: "unknown" as const, - checkedAt, - message: formatCodexCliUpgradeMessage(parsedVersion), - }; - } + const versionWarningMessage = + parsedVersion && !isCodexCliVersionSupported(parsedVersion) + ? formatCodexCliUpgradeMessage(parsedVersion) + : null; // Probe 2: `codex login status` — is the user authenticated? const authProbe = yield* runCodexCommand(["login", "status"]).pipe( @@ -297,13 +291,14 @@ export const checkCodexProviderStatus: Effect.Effect< } const parsed = parseAuthStatusFromOutput(authProbe.success.value); + const message = parsed.message ?? versionWarningMessage ?? undefined; return { provider: CODEX_PROVIDER, - status: parsed.status, + status: versionWarningMessage && parsed.status === "ready" ? "warning" : parsed.status, available: true, authStatus: parsed.authStatus, checkedAt, - ...(parsed.message ? { message: parsed.message } : {}), + ...(message ? { message } : {}), } satisfies ServerProviderStatus; });