From 7750f7ee6d3be9ffb523a3e2a2e3706cc25e8ff8 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:22:15 -0500 Subject: [PATCH] fix(codex): preserve desktop install and cache identity --- .../codex/src/app-server/attempt-startup.ts | 3 + .../codex/src/app-server/computer-use.test.ts | 85 ++++++++++--------- .../codex/src/app-server/computer-use.ts | 8 +- .../codex/src/app-server/managed-binary.ts | 2 +- .../app-server/plugin-app-cache-key.test.ts | 29 +++++++ .../src/app-server/plugin-app-cache-key.ts | 8 +- .../codex/src/app-server/shared-client.ts | 7 ++ 7 files changed, 98 insertions(+), 44 deletions(-) diff --git a/extensions/codex/src/app-server/attempt-startup.ts b/extensions/codex/src/app-server/attempt-startup.ts index f6b77b32c1c9..f9330050657b 100644 --- a/extensions/codex/src/app-server/attempt-startup.ts +++ b/extensions/codex/src/app-server/attempt-startup.ts @@ -76,6 +76,7 @@ import { clearSharedCodexAppServerClientIfCurrentAndUnclaimed, createIsolatedCodexAppServerClient, isCodexAppServerStartSelectionChangedError, + readCodexAppServerClientDesktopGenerationFingerprint, releaseLeasedSharedCodexAppServerClient, retireSharedCodexAppServerClientIfCurrent, type CodexAppServerClientOptions, @@ -334,6 +335,8 @@ export async function startCodexAttemptThread(params: { envApiKeyFingerprint: params.startupEnvApiKeyCacheKey, appServerVersion: activeStartupClient.getServerVersion(), runtimeIdentity: startupRuntimeIdentity, + desktopGenerationFingerprint: + readCodexAppServerClientDesktopGenerationFingerprint(activeStartupClient), }); const appServerRuntimeFingerprint = buildCodexAppServerRuntimeFingerprint({ appServer: params.appServer, diff --git a/extensions/codex/src/app-server/computer-use.test.ts b/extensions/codex/src/app-server/computer-use.test.ts index 1e67fd2c530f..845974574f24 100644 --- a/extensions/codex/src/app-server/computer-use.test.ts +++ b/extensions/codex/src/app-server/computer-use.test.ts @@ -992,49 +992,52 @@ describe("Codex Computer Use setup", () => { }); }); - it("provisions the managed wrapper and service for an explicit isolated-home install", async () => { - const root = tempDirs.make("openclaw-codex-explicit-install-"); - const agentDir = path.join(root, "agent"); - const codexHome = path.join(agentDir, "codex-home"); - const managedMarketplacePath = path.join( - codexHome, - ".tmp", - "bundled-marketplaces", - "openai-bundled", - ); - fs.mkdirSync(managedMarketplacePath, { recursive: true }); - const harness = createClientHarness(); - vi.spyOn(harness.client, "getRuntimeIdentity").mockReturnValue({ - serverVersion: "0.148.0", - codexHome, - }); - sharedClientMocks.readCodexAppServerClientProcessIdentity.mockReturnValue({ - clientId: "client-explicit-install", - command: "/Applications/ChatGPT.app/Contents/Resources/codex", - nativeCommand: "/Applications/ChatGPT.app/Contents/Resources/codex", - argsFingerprint: "args", - }); - const request = createBundledMarketplaceComputerUseRequest(managedMarketplacePath); + it.each(["config", "env"] as const)( + "provisions the managed wrapper and service for a %s-selected desktop install", + async (commandSource) => { + const root = tempDirs.make("openclaw-codex-explicit-install-"); + const agentDir = path.join(root, "agent"); + const codexHome = path.join(agentDir, "codex-home"); + const managedMarketplacePath = path.join( + codexHome, + ".tmp", + "bundled-marketplaces", + "openai-bundled", + ); + fs.mkdirSync(managedMarketplacePath, { recursive: true }); + const harness = createClientHarness(); + vi.spyOn(harness.client, "getRuntimeIdentity").mockReturnValue({ + serverVersion: "0.148.0", + codexHome, + }); + sharedClientMocks.readCodexAppServerClientProcessIdentity.mockReturnValue({ + clientId: "client-explicit-install", + command: "/Applications/ChatGPT.app/Contents/Resources/codex", + commandSource, + argsFingerprint: "args", + }); + const request = createBundledMarketplaceComputerUseRequest(managedMarketplacePath); - const status = await installCodexComputerUse({ - agentDir, - client: harness.client, - request, - pluginConfig: { computerUse: { enabled: true, autoInstall: false } }, - }); + const status = await installCodexComputerUse({ + agentDir, + client: harness.client, + request, + pluginConfig: { computerUse: { enabled: true, autoInstall: false } }, + }); - expect(status.ready).toBe(true); - expect(managedProvisioningMocks.ensureCodexManagedBundledMarketplace).toHaveBeenCalledWith({ - codexHome, - ownershipRoot: agentDir, - appServerCommand: "/Applications/ChatGPT.app/Contents/Resources/codex", - }); - expect(managedProvisioningMocks.ensureCodexComputerUseServiceApp).toHaveBeenCalledWith({ - codexHome, - ownershipRoot: agentDir, - appServerCommand: "/Applications/ChatGPT.app/Contents/Resources/codex", - }); - }); + expect(status.ready).toBe(true); + expect(managedProvisioningMocks.ensureCodexManagedBundledMarketplace).toHaveBeenCalledWith({ + codexHome, + ownershipRoot: agentDir, + appServerCommand: "/Applications/ChatGPT.app/Contents/Resources/codex", + }); + expect(managedProvisioningMocks.ensureCodexComputerUseServiceApp).toHaveBeenCalledWith({ + codexHome, + ownershipRoot: agentDir, + appServerCommand: "/Applications/ChatGPT.app/Contents/Resources/codex", + }); + }, + ); it("allows auto-install from a configured local marketplace path", async () => { const request = createComputerUseRequest({ installed: false }); diff --git a/extensions/codex/src/app-server/computer-use.ts b/extensions/codex/src/app-server/computer-use.ts index e5bd9326311f..ea46c96c470c 100644 --- a/extensions/codex/src/app-server/computer-use.ts +++ b/extensions/codex/src/app-server/computer-use.ts @@ -31,6 +31,7 @@ import { type ResolvedCodexComputerUseConfig, } from "./config.js"; import { resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath } from "./desktop-app-paths.js"; +import { isManagedCodexDesktopCommand } from "./managed-binary.js"; import { acquireCodexNativeConfigFence } from "./native-config-fence.js"; import type { CodexListMcpServerStatusResponse, @@ -453,7 +454,12 @@ async function prepareExplicitManagedComputerUseInstall( return; } const codexHome = params.client.getRuntimeIdentity()?.codexHome; - const command = readCodexAppServerClientProcessIdentity(params.client)?.nativeCommand; + const processIdentity = readCodexAppServerClientProcessIdentity(params.client); + const command = + processIdentity?.nativeCommand ?? + (processIdentity && isManagedCodexDesktopCommand(processIdentity.command, "darwin") + ? processIdentity.command + : undefined); if (!codexHome || !command) { return; } diff --git a/extensions/codex/src/app-server/managed-binary.ts b/extensions/codex/src/app-server/managed-binary.ts index 42b53a2ec1fa..4c69be55da53 100644 --- a/extensions/codex/src/app-server/managed-binary.ts +++ b/extensions/codex/src/app-server/managed-binary.ts @@ -104,7 +104,7 @@ export function resolveManagedCodexNativeCommand( return undefined; } -/** Returns whether a resolved managed command is owned by the macOS desktop app. */ +/** Returns whether a command is one of the standard macOS desktop app executables. */ export function isManagedCodexDesktopCommand( command: string, platform: NodeJS.Platform = process.platform, diff --git a/extensions/codex/src/app-server/plugin-app-cache-key.test.ts b/extensions/codex/src/app-server/plugin-app-cache-key.test.ts index c907b97b8922..12cdf0ecd570 100644 --- a/extensions/codex/src/app-server/plugin-app-cache-key.test.ts +++ b/extensions/codex/src/app-server/plugin-app-cache-key.test.ts @@ -50,6 +50,35 @@ describe("resolveCodexPluginAppCacheEndpoint", () => { expect(second).not.toContain("secret-token"); }); + it("separates plugin inventory across managed desktop generations", () => { + const base = { + appServer: { + start: { + transport: "stdio" as const, + command: "/Applications/ChatGPT.app/Contents/Resources/codex", + args: ["app-server"], + headers: {}, + }, + }, + agentDir: "/tmp/openclaw-agent", + runtimeIdentity: { + serverVersion: "0.20.0", + codexHome: "/tmp/openclaw-agent/codex-home", + }, + }; + + const generationX = buildCodexPluginAppCacheKey({ + ...base, + desktopGenerationFingerprint: "desktop-x", + }); + const generationY = buildCodexPluginAppCacheKey({ + ...base, + desktopGenerationFingerprint: "desktop-y", + }); + + expect(generationX).not.toEqual(generationY); + }); + it("fingerprints the remote app-server runtime used by thread bindings", () => { const first = buildCodexAppServerRuntimeFingerprint({ appServer: { diff --git a/extensions/codex/src/app-server/plugin-app-cache-key.ts b/extensions/codex/src/app-server/plugin-app-cache-key.ts index 6122d57e46d8..2ce4383bc52f 100644 --- a/extensions/codex/src/app-server/plugin-app-cache-key.ts +++ b/extensions/codex/src/app-server/plugin-app-cache-key.ts @@ -54,6 +54,7 @@ type CodexPluginAppCacheKeyParams = Omit< appServer: Pick; agentDir?: string; runtimeIdentity?: CodexAppServerRuntimeIdentity; + desktopGenerationFingerprint?: string; }; /** Builds the full app inventory cache key for Codex plugin/app discovery. */ @@ -68,7 +69,12 @@ export function buildCodexPluginAppCacheKey(params: CodexPluginAppCacheKeyParams accountId: params.accountId, envApiKeyFingerprint: params.envApiKeyFingerprint, appServerVersion: params.appServerVersion ?? params.runtimeIdentity?.serverVersion, - runtimeIdentity: params.runtimeIdentity, + runtimeIdentity: params.desktopGenerationFingerprint + ? { + ...params.runtimeIdentity, + desktopGeneration: params.desktopGenerationFingerprint, + } + : params.runtimeIdentity, }, OPENCLAW_VERSION, CODEX_PLUGIN_VERSION, diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index cd5b3db40652..090284f00394 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -165,6 +165,13 @@ export function readCodexAppServerClientProcessIdentity( }; } +/** Returns the lifecycle generation that owns a managed desktop client. */ +export function readCodexAppServerClientDesktopGenerationFingerprint( + client: CodexAppServerClient, +): string | undefined { + return getCodexAppServerClientStartMetadata().get(client)?.desktopGeneration?.fingerprint; +} + /** Resolves non-secret spawn identity before startup; argv is represented only by its hash. */ export function resolveCodexAppServerSpawnIdentity( startOptions: CodexAppServerStartOptions,