From e00f86019827d2a8b41a4e137ceb1faab94d3563 Mon Sep 17 00:00:00 2001 From: Vitor Cepeda Lopes Date: Wed, 19 Aug 2026 22:42:33 +0100 Subject: [PATCH] fix(codex): Computer Use fails when Desktop bundles a newer app-server (#125883) * fix(codex): allow newer app-server runtimes * fix(codex): bound external app-server compatibility --------- Co-authored-by: TheAngryPit <16145902+TheAngryPit@users.noreply.github.com> Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com> --- docs/plugins/codex-harness-reference.md | 11 ++- docs/plugins/codex-harness.md | 22 +++-- docs/plugins/codex-native-plugins.md | 7 +- docs/plugins/sdk-agent-harness.md | 5 +- extensions/codex/package.json | 1 + .../codex/src/app-server/client.test.ts | 98 ++++++++++++++----- extensions/codex/src/app-server/client.ts | 28 +++++- .../src/app-server/shared-client.test.ts | 37 ++++++- extensions/codex/src/app-server/version.ts | 8 +- extensions/codex/src/manifest.test.ts | 1 - pnpm-lock.yaml | 3 + 11 files changed, 169 insertions(+), 52 deletions(-) diff --git a/docs/plugins/codex-harness-reference.md b/docs/plugins/codex-harness-reference.md index ac81b69182c7..7ef9e18afe2e 100644 --- a/docs/plugins/codex-harness-reference.md +++ b/docs/plugins/codex-harness-reference.md @@ -317,10 +317,11 @@ If the normal app-server runtime would be `danger-full-access`, enabling permission profile instead. Codex-managed network enforcement is sandboxed networking, so a full-access profile would not protect outbound traffic. -The plugin accepts exactly stable Codex app-server `0.147.0`. Older or newer -versions, prereleases, build-suffixed versions, and unversioned app-server -handshakes are rejected. The same exact-version requirement applies to explicit -custom executables, remote app-servers, and macOS desktop binaries. +The plugin ships Codex app-server `0.147.0` and accepts external versions through +`0.148.0-alpha.15`. Versions outside that tested range and malformed or +unversioned handshakes are rejected. Build metadata does not affect SemVer +precedence. The same range applies to explicit custom executables, remote +app-servers, and macOS desktop binaries; admission is not readiness proof. OpenClaw treats non-loopback WebSocket app-server URLs as remote and requires identity-bearing WebSocket auth through `appServer.authToken` or an @@ -369,7 +370,7 @@ configured plugin's details to reserve the denied app IDs. It does not scan unrelated marketplaces or install, enable, or authenticate the disabled plugin; missing ownership fails closed. -Only connect OpenClaw to a `0.147.0` remote app-server trusted to accept +Only connect OpenClaw to a remote app-server in the supported range trusted to accept configured marketplace plugin installs and inventory refreshes. Missing modern inventory methods and server, authentication, or transport failures fail closed. diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index e5266019781b..1fabca16165a 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -89,10 +89,12 @@ channel is the communication surface. - The official `@openclaw/codex` plugin installed. Include `codex` in `plugins.allow` if your config uses an allowlist. -- Codex app-server `0.147.0`. The plugin ships and manages `@openai/codex` - `0.147.0` by default, so a `codex` command on `PATH` does not affect normal - startup. Explicit custom, remote, and macOS desktop-owned app-servers must - report the same exact stable `0.147.0` version. +- Codex app-server `0.147.0` through `0.148.0-alpha.15`. The plugin still ships and manages the + exact `@openai/codex` `0.147.0` artifact, so a `codex` command on `PATH` does + not affect normal startup. Explicit custom, remote, and macOS desktop-owned + app-servers must report valid SemVer inside that tested external range. + Versions above the managed artifact initialize with a warning; acceptance + permits an attempt and is not readiness or capability proof. - Node.js on the remote Codex app-server host when `remoteWorkspaceRoot` is set and cross-machine workspace attachments must be transferred. - Codex auth through `openclaw models auth login --provider openai`, an @@ -1353,11 +1355,13 @@ instead of a plain OpenAI API-key failure. Doctor rewrites legacy model refs to `openai/*`, removes stale session and whole-agent runtime pins, and preserves existing auth-profile overrides. -**The app-server is rejected:** use exactly stable Codex `0.147.0`. Older or -newer versions, prereleases, build-suffixed versions, and unversioned servers -are rejected because OpenClaw validates generated schemas and runtime contracts -against the Codex version it ships. Update or remove custom, remote, or desktop -binary overrides that select another version. +**The app-server is rejected:** use Codex `0.147.0` through +`0.148.0-alpha.15`. OpenClaw rejects versions outside that tested range, plus +malformed and unversioned servers. Same-version prereleases such as +`0.147.0-alpha.2` remain below the stable minimum; build metadata such as +`0.147.0+desktop` does not affect precedence. An accepted external version is +permitted to initialize rather than treated as proof of compatibility, so +startup and capability operations can still fail with their normal diagnostics. **`/codex status` cannot connect:** check that the `codex` plugin is enabled, that `plugins.allow` includes it when an allowlist is diff --git a/docs/plugins/codex-native-plugins.md b/docs/plugins/codex-native-plugins.md index cf3d87a2286d..41c859ad11fa 100644 --- a/docs/plugins/codex-native-plugins.md +++ b/docs/plugins/codex-native-plugins.md @@ -22,9 +22,10 @@ working. - The agent runtime must be the native Codex harness. - `plugins.entries.codex.enabled` is `true`. - `plugins.entries.codex.config.codexPlugins.enabled` is `true`. -- Codex app-server reports exactly stable `0.147.0`. The official plugin ships - `@openai/codex` `0.147.0`; custom, remote, and macOS desktop-owned binaries - must use the same exact version. +- Codex app-server reports a version from `0.147.0` through + `0.148.0-alpha.15`. The official plugin still ships `@openai/codex` + `0.147.0`; accepted external versions remain subject to normal startup and + capability validation. - The target Codex app-server can see the expected marketplace, plugin, and app inventory. - Migration supports only `openai-curated` plugins that it observed as diff --git a/docs/plugins/sdk-agent-harness.md b/docs/plugins/sdk-agent-harness.md index 7ebc5a55c5b7..02e32e359251 100644 --- a/docs/plugins/sdk-agent-harness.md +++ b/docs/plugins/sdk-agent-harness.md @@ -319,8 +319,9 @@ For operator setup, model prefix examples, and Codex-only configs, see The Codex plugin enforces the minimum app-server version documented in [Codex Harness](/plugins/codex-harness). It checks the initialize handshake and -blocks older or unversioned servers, so OpenClaw only runs against the protocol -surface it has tested. +blocks versions outside the tested external range plus malformed or unversioned +servers. Admission permits startup to continue; it does not prove later runtime +or capability operations will succeed. ### Tool-result middleware diff --git a/extensions/codex/package.json b/extensions/codex/package.json index a75186c46783..d681f50bc278 100644 --- a/extensions/codex/package.json +++ b/extensions/codex/package.json @@ -9,6 +9,7 @@ "type": "module", "dependencies": { "@openai/codex": "0.147.0", + "semver": "7.8.5", "smol-toml": "1.7.1", "typebox": "1.3.6", "ws": "8.21.1", diff --git a/extensions/codex/src/app-server/client.test.ts b/extensions/codex/src/app-server/client.test.ts index dd0a066e2482..bdfb607ea186 100644 --- a/extensions/codex/src/app-server/client.test.ts +++ b/extensions/codex/src/app-server/client.test.ts @@ -8,7 +8,11 @@ import { } from "./client.js"; import { resetSharedCodexAppServerClientForTests } from "./shared-client.js"; import { createClientHarness } from "./test-support.js"; -import { CODEX_APP_SERVER_VERSION } from "./version.js"; +import { + CODEX_APP_SERVER_VERSION, + MAX_SUPPORTED_CODEX_APP_SERVER_VERSION, + MIN_SUPPORTED_CODEX_APP_SERVER_VERSION, +} from "./version.js"; const CODEX_DYNAMIC_TOOL_SERVER_REQUEST_TIMEOUT_MS = 660_000; @@ -357,11 +361,11 @@ describe("CodexAppServerClient", () => { const { harness, initializing, outbound } = startInitialize(); harness.send({ id: outbound.id, - result: { userAgent: "openclaw/0.124.9 (macOS; test)" }, + result: { userAgent: "openclaw/0.146.9 (macOS; test)" }, }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required, but detected 0.124.9`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required, but detected 0.146.9`, ); expect(harness.writes).toHaveLength(1); }); @@ -374,7 +378,7 @@ describe("CodexAppServerClient", () => { }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required, but detected 0.146.0`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required, but detected 0.146.0`, ); expect(harness.writes).toHaveLength(1); }); @@ -387,22 +391,23 @@ describe("CodexAppServerClient", () => { }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required, but detected 0.147.0-alpha.2`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required, but detected 0.147.0-alpha.2`, ); expect(harness.writes).toHaveLength(1); }); - it("blocks Codex app-server build metadata on the exact supported version", async () => { + it("accepts compatible build metadata on the minimum supported version", async () => { + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); const { harness, initializing, outbound } = startInitialize(); harness.send({ id: outbound.id, - result: { userAgent: "openclaw/0.147.0+alpha.2 (macOS; test)" }, + result: { userAgent: "openclaw/0.147.0+desktop (macOS; test)" }, }); - await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required, but detected 0.147.0+alpha.2`, - ); - expect(harness.writes).toHaveLength(1); + await expect(initializing).resolves.toBeUndefined(); + expect(harness.client.getServerVersion()).toBe("0.147.0+desktop"); + expect(JSON.parse(harness.writes[1] ?? "{}")).toEqual({ method: "initialized" }); + expect(warn).not.toHaveBeenCalled(); }); it("blocks Codex app-server prereleases outside generated stable schemas", async () => { @@ -413,7 +418,7 @@ describe("CodexAppServerClient", () => { }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, ); expect(harness.writes).toHaveLength(1); }); @@ -426,31 +431,72 @@ describe("CodexAppServerClient", () => { }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, ); expect(harness.writes).toHaveLength(1); }); - it("blocks stable Codex app-server versions newer than generated schemas", async () => { - const newerVersion = "0.146.2"; - const { harness, initializing, outbound } = startInitialize(); - harness.send({ - id: outbound.id, - result: { userAgent: `openclaw/${newerVersion} (macOS; test)` }, - }); + it.each(["0.148.0-alpha.9", MAX_SUPPORTED_CODEX_APP_SERVER_VERSION])( + "accepts tested Codex Desktop prerelease %s for normal startup validation", + async (newerVersion) => { + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + const { harness, initializing, outbound } = startInitialize(); + harness.send({ + id: outbound.id, + result: { userAgent: `openclaw/${newerVersion} (macOS; test)` }, + }); - await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required`, - ); - expect(harness.writes).toHaveLength(1); - }); + await expect(initializing).resolves.toBeUndefined(); + expect(harness.client.getServerVersion()).toBe(newerVersion); + expect(JSON.parse(harness.writes[1] ?? "{}")).toEqual({ method: "initialized" }); + expect(warn).toHaveBeenCalledWith( + "codex app-server is newer than OpenClaw's managed runtime; continuing with normal startup validation", + { + detectedVersion: newerVersion, + validatedVersion: CODEX_APP_SERVER_VERSION, + }, + ); + }, + ); + + it.each(["0.148.0-alpha.16", "0.148.0", "1.0.0"])( + "blocks unverified future app-server version %s during initialize", + async (version) => { + const { harness, initializing, outbound } = startInitialize(); + harness.send({ + id: outbound.id, + result: { userAgent: `openclaw/${version} (macOS; test)` }, + }); + + await expect(initializing).rejects.toThrow( + `through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, + ); + expect(harness.writes).toHaveLength(1); + }, + ); + + it.each(["0.147.00", "0.148.0-alpha..9", "0.148.0-alpha.09"])( + "blocks malformed app-server version %s during initialize", + async (version) => { + const { harness, initializing, outbound } = startInitialize(); + harness.send({ + id: outbound.id, + result: { userAgent: `openclaw/${version} (macOS; test)` }, + }); + + await expect(initializing).rejects.toThrow( + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, + ); + expect(harness.writes).toHaveLength(1); + }, + ); it("blocks app-server initialize responses without a version", async () => { const { harness, initializing, outbound } = startInitialize(); harness.send({ id: outbound.id, result: {} }); await expect(initializing).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, ); expect(harness.writes).toHaveLength(1); }); diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index 5cb184d97c78..49f10451d7d8 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -8,6 +8,7 @@ import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-ha import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; +import { parse as parseSemver } from "semver"; import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js"; import { type CodexAppServerRequestMethod, @@ -30,7 +31,11 @@ import { closeCodexAppServerTransportAndWait, type CodexAppServerTransport, } from "./transport.js"; -import { CODEX_APP_SERVER_VERSION } from "./version.js"; +import { + CODEX_APP_SERVER_VERSION, + MAX_SUPPORTED_CODEX_APP_SERVER_VERSION, + MIN_SUPPORTED_CODEX_APP_SERVER_VERSION, +} from "./version.js"; const CODEX_APP_SERVER_PARSE_LOG_MAX = 500; const CODEX_APP_SERVER_PARSE_BUFFER_MAX = 8 * 1024 * 1024; @@ -999,7 +1004,7 @@ class CodexAppServerVersionError extends Error { ? `detected ${detectedVersion}` : "OpenClaw could not determine the running Codex version"; super( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required, but ${detected}. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required, but ${detected}. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`, ); this.name = "CodexAppServerVersionError"; this.detectedVersion = detectedVersion; @@ -1008,9 +1013,26 @@ class CodexAppServerVersionError extends Error { function assertSupportedCodexAppServerVersion(response: CodexInitializeResponse): string { const detectedVersion = readCodexVersionFromUserAgent(response.userAgent); - if (detectedVersion !== CODEX_APP_SERVER_VERSION) { + if (!detectedVersion) { throw new CodexAppServerVersionError(detectedVersion); } + const detected = parseSemver(detectedVersion); + if ( + !detected || + detected.compare(MIN_SUPPORTED_CODEX_APP_SERVER_VERSION) < 0 || + detected.compare(MAX_SUPPORTED_CODEX_APP_SERVER_VERSION) > 0 + ) { + throw new CodexAppServerVersionError(detectedVersion); + } + if (detected.compare(CODEX_APP_SERVER_VERSION) > 0) { + embeddedAgentLog.warn( + "codex app-server is newer than OpenClaw's managed runtime; continuing with normal startup validation", + { + detectedVersion, + validatedVersion: CODEX_APP_SERVER_VERSION, + }, + ); + } return detectedVersion; } diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index abfd0b447328..8c35d8ae7590 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -9,7 +9,11 @@ import type { CodexAppServerStartOptions } from "./config.js"; import { acquireCodexNativeConfigFence } from "./native-config-fence.js"; import { codexNativeSubagentMonitorRuntime } from "./native-subagent-monitor.js"; import { createClientHarness } from "./test-support.js"; -import { CODEX_APP_SERVER_VERSION } from "./version.js"; +import { + CODEX_APP_SERVER_VERSION, + MAX_SUPPORTED_CODEX_APP_SERVER_VERSION, + MIN_SUPPORTED_CODEX_APP_SERVER_VERSION, +} from "./version.js"; const mocks = vi.hoisted(() => ({ bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions), @@ -279,7 +283,7 @@ describe("shared Codex app-server client", () => { await sendInitializeResult(harness, "openclaw/0.117.9 (macOS; test)"); await expect(listPromise).rejects.toThrow( - `Codex app-server ${CODEX_APP_SERVER_VERSION} is required`, + `A Codex app-server from ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} through ${MAX_SUPPORTED_CODEX_APP_SERVER_VERSION} is required`, ); expect(harness.process.stdin.destroyed).toBe(true); startSpy.mockRestore(); @@ -508,6 +512,35 @@ describe("shared Codex app-server client", () => { expect(pluginLocal.process.stdin.destroyed).toBe(true); }); + it("keeps a newer desktop app-server instead of falling back by version", async () => { + const desktop = createClientHarness(); + const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValueOnce(desktop.client); + const startOptions = configureManagedDesktopFallback(); + + const acquire = getSharedCodexAppServerClient({ startOptions, timeoutMs: 1_000 }); + await sendInitializeResult(desktop, "openclaw/0.148.0-alpha.9 (macOS; test)"); + const client = await acquire; + + expect(client).toBe(desktop.client); + expect(startSpy).toHaveBeenCalledTimes(1); + expect(startSpy.mock.calls[0]?.[0]).toMatchObject({ + command: "/Applications/Codex.app/Contents/Resources/codex", + commandSource: "resolved-managed", + managedFallbackCommandPaths: ["/cache/openclaw/codex"], + }); + expect(desktop.process.stdin.destroyed).toBe(false); + expect(mocks.embeddedAgentLog.warn).toHaveBeenCalledWith( + "codex app-server is newer than OpenClaw's managed runtime; continuing with normal startup validation", + { + detectedVersion: "0.148.0-alpha.9", + validatedVersion: CODEX_APP_SERVER_VERSION, + }, + ); + + await clearSharedCodexAppServerClientAndWait({ exitTimeoutMs: 25, forceKillDelayMs: 5 }); + expect(desktop.process.stdin.destroyed).toBe(true); + }); + it("shares a managed fallback with a waiter that arrives during fallback initialize", async () => { const desktop = createClientHarness(); const fallback = createClientHarness(); diff --git a/extensions/codex/src/app-server/version.ts b/extensions/codex/src/app-server/version.ts index deec3bd2cb70..fc21c6c7a6f3 100644 --- a/extensions/codex/src/app-server/version.ts +++ b/extensions/codex/src/app-server/version.ts @@ -1,7 +1,13 @@ /** * Version and package pins for the managed Codex app-server runtime. */ -/** Exact Codex app-server version shipped and supported by the OpenClaw Codex bridge. */ +/** Exact Codex app-server version shipped by the OpenClaw Codex bridge. */ export const CODEX_APP_SERVER_VERSION = "0.147.0"; +/** Inclusive runtime compatibility floor for external app-server binaries. */ +export const MIN_SUPPORTED_CODEX_APP_SERVER_VERSION = "0.147.0"; +/** Inclusive runtime compatibility ceiling for external app-server binaries. */ +// The ceiling is the newest Desktop build covered by upstream source inspection and +// a live Computer Use flow. Raising it requires equivalent protocol and live proof. +export const MAX_SUPPORTED_CODEX_APP_SERVER_VERSION = "0.148.0-alpha.15"; /** npm package name for the managed Codex app-server binary. */ export const MANAGED_CODEX_APP_SERVER_PACKAGE = "@openai/codex"; diff --git a/extensions/codex/src/manifest.test.ts b/extensions/codex/src/manifest.test.ts index 3c325804929e..8cdc6b586c5a 100644 --- a/extensions/codex/src/manifest.test.ts +++ b/extensions/codex/src/manifest.test.ts @@ -24,7 +24,6 @@ describe("codex package manifest", () => { expect(packageJson.devDependencies).toHaveProperty("@openclaw/plugin-sdk"); expect(packageJson.dependencies?.["@openai/codex"]).toBe(CODEX_APP_SERVER_VERSION); - expect(packageJson.dependencies).not.toHaveProperty("semver"); expect(packageJson.openclaw?.release?.requireLatestDependencies).toEqual(["@openai/codex"]); expect(packageJson.openclaw?.install?.requiredPlatformPackages).toEqual([ "@openai/codex-linux-x64", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ad8173af92d..2765de3bfde9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -641,6 +641,9 @@ importers: '@openai/codex': specifier: 0.147.0 version: 0.147.0 + semver: + specifier: 7.8.5 + version: 7.8.5 smol-toml: specifier: 1.7.1 version: 1.7.1