diff --git a/src/targets/codex-detector.ts b/src/targets/codex-detector.ts index 73cd79e4..89efbfa4 100644 --- a/src/targets/codex-detector.ts +++ b/src/targets/codex-detector.ts @@ -1,11 +1,19 @@ import * as fs from 'fs'; import * as childProcess from 'child_process'; import { expandPath } from '../utils/helpers'; -import { escapeShellArg, getWindowsEscapedCommandShell } from '../utils/shell-executor'; +import { + escapeShellArg, + getWindowsEscapedCommandShell, + stripAnthropicEnv, + stripBrowserEnv, + stripCodexSessionEnv, +} from '../utils/shell-executor'; import type { TargetBinaryInfo } from './target-adapter'; const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides'; const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version']; +const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT'; +const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY'; function buildWindowsCodexCandidates(matches: string[]): string[] { const shellCandidates = matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry)); @@ -32,11 +40,22 @@ function buildWindowsCodexCandidates(matches: string[]): string[] { return [...new Set([...prioritized, ...bareCandidates])]; } +function buildCodexProbeEnv(): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...stripBrowserEnv(stripCodexSessionEnv(stripAnthropicEnv(process.env))), + }; + delete env[CCSXP_CLIPROXY_SHORTCUT_ENV]; + delete env[CODEX_RUNTIME_ENV_KEY]; + return env; +} + function runCodexProbe(codexPath: string, args: string[]): string | undefined { const isWindows = process.platform === 'win32'; const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath); const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath); + const env = buildCodexProbeEnv(); + try { if (isPowerShellScript) { return childProcess.execFileSync( @@ -44,6 +63,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined { ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args], { encoding: 'utf8', + env, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, windowsHide: true, @@ -55,6 +75,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined { const cmdString = [codexPath, ...args].map(escapeShellArg).join(' '); const result = childProcess.spawnSync(cmdString, { encoding: 'utf8', + env, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, windowsHide: true, @@ -65,6 +86,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined { return childProcess.execFileSync(codexPath, args, { encoding: 'utf8', + env, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, }); diff --git a/tests/unit/targets/codex-detector.test.ts b/tests/unit/targets/codex-detector.test.ts index b361d8ac..620b52e0 100644 --- a/tests/unit/targets/codex-detector.test.ts +++ b/tests/unit/targets/codex-detector.test.ts @@ -6,16 +6,29 @@ import * as path from 'path'; import { detectCodexCli, getCodexBinaryInfo } from '../../../src/targets/codex-detector'; +const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT'; + describe('codex-detector', () => { let tmpDir: string; let originalPath: string | undefined; let originalCodexPath: string | undefined; + let originalProbeEnv: Record; + const probeEnvKeys = [ + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_API_KEY', + 'CCS_BROWSER_DEVTOOLS_URL', + 'CODEX_THREAD_ID', + 'CCS_CODEX_API_KEY', + CCSXP_CLIPROXY_SHORTCUT_ENV, + 'CCS_SAFE_VALUE', + ]; const originalPlatform = process.platform; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-detector-test-')); originalPath = process.env.PATH; originalCodexPath = process.env.CCS_CODEX_PATH; + originalProbeEnv = Object.fromEntries(probeEnvKeys.map((key) => [key, process.env[key]])); process.env.PATH = ''; }); @@ -28,6 +41,11 @@ describe('codex-detector', () => { if (originalCodexPath !== undefined) process.env.CCS_CODEX_PATH = originalCodexPath; else delete process.env.CCS_CODEX_PATH; + for (const key of probeEnvKeys) { + if (originalProbeEnv[key] !== undefined) process.env[key] = originalProbeEnv[key]; + else delete process.env[key]; + } + fs.rmSync(tmpDir, { recursive: true, force: true }); }); @@ -121,6 +139,39 @@ describe('codex-detector', () => { execSyncSpy.mockRestore(); }); + it('runs Codex feature probes with the same sensitive env stripping used for Codex launches', () => { + const fakeCodex = path.join(tmpDir, 'codex'); + fs.writeFileSync(fakeCodex, ''); + process.env.CCS_CODEX_PATH = fakeCodex; + process.env.ANTHROPIC_AUTH_TOKEN = 'secret-auth-token'; + process.env.ANTHROPIC_API_KEY = 'secret-api-key'; + process.env.CCS_BROWSER_DEVTOOLS_URL = 'ws://127.0.0.1:9222/devtools/browser/secret'; + process.env.CODEX_THREAD_ID = 'thread-secret'; + process.env.CCS_CODEX_API_KEY = 'runtime-secret'; + process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = '1'; + process.env.CCS_SAFE_VALUE = 'safe-value'; + + const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation(() => { + return 'codex-cli 0.119.0-alpha.1'; + }); + + const info = getCodexBinaryInfo(); + + expect(info?.features).toContain('config-overrides'); + const probeOptions = execFileSyncSpy.mock.calls + .map((call) => call[2] as { env?: NodeJS.ProcessEnv } | undefined) + .find((options) => options?.env); + expect(probeOptions?.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + expect(probeOptions?.env?.ANTHROPIC_API_KEY).toBeUndefined(); + expect(probeOptions?.env?.CCS_BROWSER_DEVTOOLS_URL).toBeUndefined(); + expect(probeOptions?.env?.CODEX_THREAD_ID).toBeUndefined(); + expect(probeOptions?.env?.CCS_CODEX_API_KEY).toBeUndefined(); + expect(probeOptions?.env?.[CCSXP_CLIPROXY_SHORTCUT_ENV]).toBeUndefined(); + expect(probeOptions?.env?.CCS_SAFE_VALUE).toBe('safe-value'); + + execFileSyncSpy.mockRestore(); + }); + it('falls back to a direct -c probe when help text omits the config flag', () => { const fakeCodex = path.join(tmpDir, 'codex'); fs.writeFileSync(fakeCodex, '');