fix(codex): preserve desktop install and cache identity

This commit is contained in:
joshavant
2026-08-22 00:22:15 -05:00
parent e1b16dffca
commit 7750f7ee6d
7 changed files with 98 additions and 44 deletions
@@ -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,
@@ -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 });
@@ -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;
}
@@ -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,
@@ -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: {
@@ -54,6 +54,7 @@ type CodexPluginAppCacheKeyParams = Omit<
appServer: Pick<CodexAppServerRuntimeOptions, "start">;
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,
@@ -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,