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>
This commit is contained in:
Vitor Cepeda Lopes
2026-08-19 22:42:33 +01:00
committed by GitHub
parent fa407417f0
commit e00f860198
11 changed files with 169 additions and 52 deletions
+6 -5
View File
@@ -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.
+13 -9
View File
@@ -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
+4 -3
View File
@@ -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
+3 -2
View File
@@ -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
+1
View File
@@ -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",
+72 -26
View File
@@ -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);
});
+25 -3
View File
@@ -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;
}
@@ -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();
+7 -1
View File
@@ -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";
-1
View File
@@ -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",
+3
View File
@@ -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