Fix Codex Microsoft Store binary detection on Windows (#1020)

Fixes #1013
This commit is contained in:
ezra
2026-05-19 05:12:55 +00:00
committed by GitHub
parent e38d0e0fa9
commit ce822f989f
2 changed files with 116 additions and 9 deletions
@@ -52,7 +52,7 @@ import {
resolveProviderCommandPrefix,
type ProviderRuntimeSettings,
} from "../provider-launch-config.js";
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
import { findExecutable, isCommandAvailable, probeExecutable } from "../../../utils/executable.js";
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
import { spawnProcess } from "../../../utils/spawn.js";
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
@@ -375,8 +375,61 @@ function mergeCodexConfiguredDefaults(
};
}
function codexMicrosoftStorePackageRoot(): string | null {
const localAppData = process.env.LOCALAPPDATA;
if (!localAppData) {
return null;
}
return path.join(localAppData, "Packages");
}
async function findCodexMicrosoftStoreBinary(): Promise<string | null> {
if (process.platform !== "win32") {
return null;
}
const packageRoot = codexMicrosoftStorePackageRoot();
if (!packageRoot) {
return null;
}
let entries: Dirent[];
try {
entries = await fs.readdir(packageRoot, { withFileTypes: true });
} catch {
return null;
}
const codexPackages = entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith("OpenAI.Codex_"))
.map((entry) => entry.name)
.sort();
for (const packageName of codexPackages) {
const candidate = path.join(
packageRoot,
packageName,
"LocalCache",
"Local",
"OpenAI",
"Codex",
"bin",
"codex.exe",
);
if (await probeExecutable(candidate)) {
return candidate;
}
}
return null;
}
async function findDefaultCodexBinary(): Promise<string | null> {
return (await findExecutable("codex")) ?? (await findCodexMicrosoftStoreBinary());
}
async function resolveCodexBinary(): Promise<string> {
const found = await findExecutable("codex");
const found = await findDefaultCodexBinary();
if (found) {
return found;
}
@@ -5299,13 +5352,13 @@ export class CodexAppServerAgentClient implements AgentClient {
if (command?.mode === "replace") {
return await isCommandAvailable(command.argv[0]);
}
return await isCommandAvailable("codex");
return (await findDefaultCodexBinary()) !== null;
}
async getDiagnostic(): Promise<{ diagnostic: string }> {
try {
const available = await this.isAvailable();
const resolvedBinary = await findExecutable("codex");
const resolvedBinary = await findDefaultCodexBinary();
const entries: Array<{ label: string; value: string }> = [
{
label: "Binary",
@@ -5448,6 +5501,8 @@ export const __codexAppServerInternals = {
CodexAppServerClient,
codexModelSupportsFastMode,
CodexAppServerAgentSession,
findCodexMicrosoftStoreBinary,
findDefaultCodexBinary,
formatCodexQuestionPrompts,
mapCodexQuestionRequestToToolCall,
mapCodexPatchNotificationToToolCall,
@@ -1,4 +1,4 @@
import { mkdtempSync, rmSync } from "node:fs";
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
@@ -9,10 +9,11 @@ import { AgentManager } from "../agent-manager.js";
import { AgentStorage } from "../agent-storage.js";
import { ClaudeAgentClient } from "./claude/agent.js";
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
import { __codexAppServerInternals, CodexAppServerAgentClient } from "./codex-app-server-agent.js";
import { OpenCodeAgentClient } from "./opencode-agent.js";
const originalEnv = {
LOCALAPPDATA: process.env.LOCALAPPDATA,
PATH: process.env.PATH,
PATHEXT: process.env.PATHEXT,
};
@@ -31,7 +32,19 @@ function isolatePathTo(dir: string): void {
}
}
function isolateCodexDefaultDiscoveryTo(dir: string): void {
isolatePathTo(dir);
if (process.platform === "win32") {
process.env.LOCALAPPDATA = dir;
}
}
afterEach(() => {
if (originalEnv.LOCALAPPDATA === undefined) {
delete process.env.LOCALAPPDATA;
} else {
process.env.LOCALAPPDATA = originalEnv.LOCALAPPDATA;
}
process.env.PATH = originalEnv.PATH;
process.env.PATHEXT = originalEnv.PATHEXT;
for (const dir of tempDirs.splice(0)) {
@@ -42,12 +55,51 @@ afterEach(() => {
describe("default provider availability", () => {
test("Codex reports unavailable when the default command cannot be resolved", async () => {
const binDir = makeTempDir("provider-availability-codex-");
isolatePathTo(binDir);
isolateCodexDefaultDiscoveryTo(binDir);
const client = new CodexAppServerAgentClient(createTestLogger());
await expect(client.isAvailable()).resolves.toBe(false);
});
test("Codex reports available from a Microsoft Store install path when PATH misses codex", async () => {
const originalPlatform = process.platform;
const originalLocalAppData = process.env.LOCALAPPDATA;
const root = makeTempDir("provider-availability-codex-store-");
const emptyPathDir = join(root, "empty-path");
const codexBinDir = join(
root,
"Packages",
"OpenAI.Codex_abc123",
"LocalCache",
"Local",
"OpenAI",
"Codex",
"bin",
);
const codexExe = join(codexBinDir, "codex.exe");
mkdirSync(emptyPathDir, { recursive: true });
mkdirSync(codexBinDir, { recursive: true });
copyFileSync(process.execPath, codexExe);
Object.defineProperty(process, "platform", { value: "win32", writable: true });
process.env.LOCALAPPDATA = root;
isolatePathTo(emptyPathDir);
process.env.PATHEXT = ".EXE";
try {
const client = new CodexAppServerAgentClient(createTestLogger());
await expect(__codexAppServerInternals.findDefaultCodexBinary()).resolves.toBe(codexExe);
await expect(client.isAvailable()).resolves.toBe(true);
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
if (originalLocalAppData === undefined) {
delete process.env.LOCALAPPDATA;
} else {
process.env.LOCALAPPDATA = originalLocalAppData;
}
}
});
test("Claude reports unavailable when the default command cannot be resolved", async () => {
const binDir = makeTempDir("provider-availability-claude-");
isolatePathTo(binDir);
@@ -66,7 +118,7 @@ describe("default provider availability", () => {
test("AgentManager reports Codex unavailable without throwing", async () => {
const binDir = makeTempDir("provider-availability-manager-bin-");
isolatePathTo(binDir);
isolateCodexDefaultDiscoveryTo(binDir);
const workdir = makeTempDir("provider-availability-manager-work-");
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
const manager = new AgentManager({
@@ -88,7 +140,7 @@ describe("default provider availability", () => {
test("resumeAgentFromPersistence stops before provider spawn when Codex is unavailable", async () => {
const binDir = makeTempDir("provider-availability-resume-bin-");
isolatePathTo(binDir);
isolateCodexDefaultDiscoveryTo(binDir);
const workdir = makeTempDir("provider-availability-resume-work-");
const storage = new AgentStorage(join(workdir, "agents"), createTestLogger());
const manager = new AgentManager({