mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 02:15:23 +00:00
fix(codex): harden native runtime detection
This commit is contained in:
1 parent
b6717c5529
commit
ca54bad2aa
9 files changed
+256
-68
No files matched your search
@@ -2,6 +2,7 @@ import { ChildProcess, spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import type { ProfileType } from '../types/profile';
|
||||
import { runCleanup } from '../errors';
|
||||
import { expandPath } from '../utils/helpers';
|
||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||
import { escapeShellArg, stripAnthropicEnv, stripCodexSessionEnv } from '../utils/shell-executor';
|
||||
import type {
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
|
||||
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
|
||||
const CODEX_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
||||
const CODEX_INFO_FLAGS = new Set(['--help', '-h', '--version', '-v']);
|
||||
|
||||
function formatTomlString(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
@@ -83,10 +85,54 @@ function normalizeCodexReasoningOverride(value: string | number | undefined): st
|
||||
);
|
||||
}
|
||||
|
||||
function ensureExplicitCodexHomeDir(env: NodeJS.ProcessEnv): string | undefined {
|
||||
const codexHome = env.CODEX_HOME?.trim();
|
||||
function isInformationalCodexInvocation(args: string[]): boolean {
|
||||
if (args.length === 1) {
|
||||
return CODEX_INFO_FLAGS.has(args[0] || '');
|
||||
}
|
||||
|
||||
if (args.length === 2) {
|
||||
return CODEX_INFO_FLAGS.has(args[1] || '');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeExplicitCodexHomeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const rawCodexHome = env.CODEX_HOME;
|
||||
if (rawCodexHome === undefined) {
|
||||
return env;
|
||||
}
|
||||
|
||||
const trimmedCodexHome = rawCodexHome.trim();
|
||||
if (!trimmedCodexHome) {
|
||||
const nextEnv = { ...env };
|
||||
delete nextEnv.CODEX_HOME;
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
const normalizedCodexHome = expandPath(trimmedCodexHome);
|
||||
if (normalizedCodexHome === rawCodexHome) {
|
||||
return env;
|
||||
}
|
||||
|
||||
return {
|
||||
...env,
|
||||
CODEX_HOME: normalizedCodexHome,
|
||||
};
|
||||
}
|
||||
|
||||
function prepareExplicitCodexHome(
|
||||
env: NodeJS.ProcessEnv,
|
||||
args: string[]
|
||||
): { env: NodeJS.ProcessEnv; error?: string } {
|
||||
const normalizedEnv = normalizeExplicitCodexHomeEnv(env);
|
||||
const codexHome = normalizedEnv.CODEX_HOME;
|
||||
if (!codexHome) {
|
||||
return undefined;
|
||||
return { env: normalizedEnv };
|
||||
}
|
||||
|
||||
if (isInformationalCodexInvocation(args)) {
|
||||
return { env: normalizedEnv };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -94,18 +140,27 @@ function ensureExplicitCodexHomeDir(env: NodeJS.ProcessEnv): string | undefined
|
||||
} catch (err) {
|
||||
const error = err as NodeJS.ErrnoException;
|
||||
if (error.code !== 'EEXIST') {
|
||||
return `[X] Unable to initialize CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`;
|
||||
return {
|
||||
env: normalizedEnv,
|
||||
error: `[X] Unable to initialize CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.statSync(codexHome).isDirectory()) {
|
||||
return `[X] CODEX_HOME path is not a directory: ${codexHome}`;
|
||||
return {
|
||||
env: normalizedEnv,
|
||||
error: `[X] CODEX_HOME path is not a directory: ${codexHome}`,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
return { env: normalizedEnv };
|
||||
} catch (err) {
|
||||
const error = err as NodeJS.ErrnoException;
|
||||
return `[X] Unable to access CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`;
|
||||
return {
|
||||
env: normalizedEnv,
|
||||
error: `[X] Unable to access CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +288,12 @@ export class CodexAdapter implements TargetAdapter {
|
||||
return exitWithCleanup(1);
|
||||
}
|
||||
|
||||
const codexHomeInitError = ensureExplicitCodexHomeDir(env);
|
||||
if (codexHomeInitError) {
|
||||
console.error(codexHomeInitError);
|
||||
const codexHomePreparation = prepareExplicitCodexHome(env, args);
|
||||
if (codexHomePreparation.error) {
|
||||
console.error(codexHomePreparation.error);
|
||||
return exitWithCleanup(1);
|
||||
}
|
||||
const launchEnv = codexHomePreparation.env;
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||
@@ -248,7 +304,7 @@ export class CodexAdapter implements TargetAdapter {
|
||||
child = spawn(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
|
||||
{ stdio: 'inherit', windowsHide: true, env }
|
||||
{ stdio: 'inherit', windowsHide: true, env: launchEnv }
|
||||
);
|
||||
} else if (needsShell) {
|
||||
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
|
||||
@@ -256,10 +312,10 @@ export class CodexAdapter implements TargetAdapter {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
shell: true,
|
||||
env,
|
||||
env: launchEnv,
|
||||
});
|
||||
} else {
|
||||
child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env });
|
||||
child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env: launchEnv });
|
||||
}
|
||||
|
||||
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
||||
|
||||
@@ -7,6 +7,22 @@ import type { TargetBinaryInfo } from './target-adapter';
|
||||
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
|
||||
const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version'];
|
||||
|
||||
function buildWindowsCodexCandidates(matches: string[]): string[] {
|
||||
const shellCandidates = matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry));
|
||||
const bareCandidates = matches.filter((entry) => !/\.(exe|cmd|bat|ps1)$/i.test(entry));
|
||||
const prioritized: string[] = [];
|
||||
|
||||
for (const entry of shellCandidates) {
|
||||
if (/\.(cmd|bat)$/i.test(entry)) {
|
||||
prioritized.push(entry.replace(/\.(cmd|bat)$/i, '.ps1'));
|
||||
}
|
||||
prioritized.push(entry);
|
||||
}
|
||||
|
||||
prioritized.push(...bareCandidates);
|
||||
return [...new Set(prioritized)];
|
||||
}
|
||||
|
||||
function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||
@@ -112,12 +128,7 @@ export function detectCodexCli(): string | null {
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const candidates = isWindows
|
||||
? [
|
||||
...matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry)),
|
||||
...matches.filter((entry) => !/\.(exe|cmd|bat|ps1)$/i.test(entry)),
|
||||
]
|
||||
: matches;
|
||||
const candidates = isWindows ? buildWindowsCodexCandidates(matches) : matches;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
|
||||
@@ -646,7 +646,7 @@ export function summarizeCodexMcpServers(value: unknown): CodexMcpServerDiagnost
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
||||
function getCodexSupportMatrix(supportsManagedRouting: boolean): CodexSupportMatrixEntry[] {
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
@@ -657,14 +657,18 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
||||
{
|
||||
id: 'cliproxy-provider-codex',
|
||||
label: 'cliproxy provider=codex',
|
||||
supported: true,
|
||||
notes: 'Routed through the CLIProxy Codex Responses bridge.',
|
||||
supported: supportsManagedRouting,
|
||||
notes: supportsManagedRouting
|
||||
? 'Routed through the CLIProxy Codex Responses bridge.'
|
||||
: 'Requires a Codex build that exposes --config overrides.',
|
||||
},
|
||||
{
|
||||
id: 'settings-with-bridge',
|
||||
label: 'settings with bridge metadata',
|
||||
supported: true,
|
||||
notes: 'Supported when the resolved API profile points at a Codex CLIProxy bridge.',
|
||||
supported: supportsManagedRouting,
|
||||
notes: supportsManagedRouting
|
||||
? 'Supported when the resolved API profile points at a Codex CLIProxy bridge.'
|
||||
: 'Requires a Codex build that exposes --config overrides.',
|
||||
},
|
||||
{
|
||||
id: 'cliproxy-composite',
|
||||
@@ -696,6 +700,7 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
||||
export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiagnostics> {
|
||||
const paths = resolveCodexConfigPaths();
|
||||
const binaryInfo = getCodexBinaryInfo();
|
||||
const supportsConfigOverrides = !!binaryInfo && codexBinarySupportsConfigOverrides(binaryInfo);
|
||||
const docsReference = getCompatibleCliDocsReference('codex');
|
||||
const fileProbe = await probeTomlObjectFile(
|
||||
paths.configPath,
|
||||
@@ -715,12 +720,12 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
||||
const features = summarizeCodexFeatureFlags(config?.features);
|
||||
const projectTrust = summarizeCodexProjectTrust(config?.projects);
|
||||
const mcpServers = summarizeCodexMcpServers(config?.mcp_servers);
|
||||
const supportMatrix = getCodexSupportMatrix();
|
||||
const supportMatrix = getCodexSupportMatrix(supportsConfigOverrides);
|
||||
|
||||
const warnings: string[] = [];
|
||||
if (!binaryInfo) {
|
||||
warnings.push('Codex binary is not detected in PATH or CCS_CODEX_PATH.');
|
||||
} else if (!codexBinarySupportsConfigOverrides(binaryInfo)) {
|
||||
} else if (!supportsConfigOverrides) {
|
||||
warnings.push(
|
||||
'This Codex build does not expose --config overrides required for CCS-backed Codex routing.'
|
||||
);
|
||||
@@ -766,7 +771,7 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
||||
source: process.env.CCS_CODEX_PATH ? 'CCS_CODEX_PATH' : binaryInfo ? 'PATH' : 'missing',
|
||||
version: binaryInfo?.version ?? null,
|
||||
overridePath: process.env.CCS_CODEX_PATH || null,
|
||||
supportsConfigOverrides: codexBinarySupportsConfigOverrides(binaryInfo),
|
||||
supportsConfigOverrides,
|
||||
},
|
||||
file: fileProbe.diagnostics,
|
||||
workspacePath: process.cwd(),
|
||||
|
||||
Reference in new issue
Block a user