Merge pull request #850 from kaitranntt/kai/feat/issue-773-codex-runtime-target

feat: add codex runtime target and dashboard parity
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-03-29 14:43:09 -04:00
commit 6fc9aacc06
85 files changed
+7867 -208

No files matched your search

@@ -8,6 +8,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { Config, Settings } from '../../types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { ensureProfileHooksOrThrow } from '../../utils/websearch/profile-hook-injector';
import { isSensitiveKey } from '../../utils/sensitive-keys';
@@ -29,7 +30,7 @@ const SETTINGS_FILE_SUFFIX = '.settings.json';
const REDACTED_TOKEN_SENTINEL = '__CCS_REDACTED__';
function parseTargetValue(value: unknown): TargetType | null {
if (value === 'claude' || value === 'droid') {
if (isPersistedTargetType(value)) {
return value;
}
return null;
@@ -359,7 +360,7 @@ export function importApiProfileBundle(
if (input.profile.target !== undefined && bundleTarget === null) {
return {
success: false,
error: 'Invalid bundle profile target. Expected: claude or droid.',
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}.`,
};
}
+2 -3
View File
@@ -10,14 +10,13 @@ import { loadConfigSafe } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { expandPath } from '../../utils/helpers';
import type { TargetType } from '../../targets/target-adapter';
import { isPersistedTargetType } from '../../targets/target-metadata';
import type { Settings } from '../../types/config';
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
import { resolveCliproxyBridgeMetadata } from './cliproxy-profile-bridge';
const VALID_TARGETS: ReadonlySet<TargetType> = new Set<TargetType>(['claude', 'droid']);
function sanitizeTarget(target: unknown): TargetType {
if (typeof target === 'string' && VALID_TARGETS.has(target as TargetType)) {
if (isPersistedTargetType(target)) {
return target as TargetType;
}
return 'claude';
+2
View File
@@ -0,0 +1,2 @@
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
require('../ccs');
+185 -42
View File
@@ -56,6 +56,8 @@ import {
getTarget,
ClaudeAdapter,
DroidAdapter,
CodexAdapter,
evaluateTargetRuntimeCompatibility,
pruneOrphanedModels,
resolveDroidProvider,
type TargetCredentials,
@@ -66,6 +68,7 @@ import {
resolveDroidReasoningRuntime,
} from './targets/droid-reasoning-runtime';
import { DroidCommandRouterError, routeDroidCommandArgs } from './targets/droid-command-router';
import { resolveCliproxyBridgeMetadata } from './api/services/cliproxy-profile-bridge';
// Version and Update check utilities
import { getVersion } from './utils/version';
@@ -84,6 +87,15 @@ interface DetectedProfile {
remainingArgs: string[];
}
interface RuntimeReasoningResolution {
argsWithoutReasoningFlags: string[];
reasoningOverride: string | number | undefined;
reasoningSource: 'flag' | 'env' | undefined;
sourceDisplay: string | undefined;
}
const CODEX_RUNTIME_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
/**
* Smart profile detection
*/
@@ -97,6 +109,54 @@ function detectProfile(args: string[]): DetectedProfile {
}
}
function resolveRuntimeReasoningFlags(
args: string[],
envThinkingValue: string | undefined
): RuntimeReasoningResolution {
const runtime = resolveDroidReasoningRuntime(args, envThinkingValue);
if (runtime.duplicateDisplays.length > 0) {
console.error(
warn(
`[!] Multiple reasoning flags detected. Using first occurrence: ${runtime.sourceDisplay || '<first-flag>'}`
)
);
}
return {
argsWithoutReasoningFlags: runtime.argsWithoutReasoningFlags,
reasoningOverride: runtime.reasoningOverride,
reasoningSource: runtime.sourceFlag
? 'flag'
: runtime.reasoningOverride !== undefined
? 'env'
: undefined,
sourceDisplay: runtime.sourceDisplay,
};
}
function normalizeCodexRuntimeReasoningOverride(
value: string | number | undefined
): string | undefined {
return typeof value === 'string' && CODEX_RUNTIME_REASONING_LEVELS.has(value) ? value : undefined;
}
function exitWithRuntimeReasoningFlagError(
message: string,
options: {
codexAliasLevels: string;
includeDroidExecExample?: boolean;
}
): never {
console.error(fail(message));
console.error(' Examples: --thinking low, --thinking 8192, --thinking off');
console.error(` Codex alias: --effort ${options.codexAliasLevels}`);
if (options.includeDroidExecExample) {
console.error(' Droid exec: --reasoning-effort high');
}
process.exit(1);
}
// ========== Main Execution ==========
interface ProfileError extends Error {
@@ -183,6 +243,7 @@ async function main(): Promise<void> {
// Register target adapters
registerTarget(new ClaudeAdapter());
registerTarget(new DroidAdapter());
registerTarget(new CodexAdapter());
const args = process.argv.slice(2);
@@ -372,6 +433,9 @@ async function main(): Promise<void> {
// Resolve non-claude target adapter once.
const targetAdapter = resolvedTarget !== 'claude' ? getTarget(resolvedTarget) : null;
let resolvedSettingsPath: string | undefined;
let resolvedSettings: ReturnType<typeof loadSettings> | undefined;
let resolvedCliproxyBridge: ReturnType<typeof resolveCliproxyBridgeMetadata> | undefined;
// Preflight unsupported profile/target combinations BEFORE binary detection,
// so users get the most actionable error even when the target CLI is not installed.
@@ -381,21 +445,47 @@ async function main(): Promise<void> {
process.exit(1);
}
if (profileInfo.type === 'cliproxy' && !targetAdapter.supportsProfileType('cliproxy')) {
console.error(fail(`${targetAdapter.displayName} does not support CLIProxy profiles`));
console.error(info('Use a settings-based profile with --target instead'));
process.exit(1);
}
if (profileInfo.type === 'copilot' && !targetAdapter.supportsProfileType('copilot')) {
console.error(fail(`${targetAdapter.displayName} does not support Copilot profiles`));
process.exit(1);
}
if (profileInfo.type === 'account' && !targetAdapter.supportsProfileType('account')) {
console.error(fail(`${targetAdapter.displayName} does not support account-based profiles`));
console.error(info('Use a settings-based profile with --target instead'));
process.exit(1);
if (profileInfo.type === 'settings') {
resolvedSettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name);
resolvedSettings = loadSettings(resolvedSettingsPath);
resolvedCliproxyBridge = resolveCliproxyBridgeMetadata(resolvedSettings);
const compatibility = evaluateTargetRuntimeCompatibility({
target: resolvedTarget,
profileType: profileInfo.type,
cliproxyBridgeProvider: resolvedCliproxyBridge?.provider ?? null,
});
if (!compatibility.supported) {
console.error(
fail(
compatibility.reason || `${targetAdapter.displayName} does not support this profile.`
)
);
if (compatibility.suggestion) {
console.error(info(compatibility.suggestion));
}
process.exit(1);
}
} else {
const compatibility = evaluateTargetRuntimeCompatibility({
target: resolvedTarget,
profileType: profileInfo.type,
cliproxyProvider: profileInfo.type === 'cliproxy' ? profileInfo.provider : undefined,
isComposite:
profileInfo.type === 'cliproxy' ? Boolean(profileInfo.isComposite) : undefined,
});
if (!compatibility.supported) {
console.error(
fail(
compatibility.reason || `${targetAdapter.displayName} does not support this profile.`
)
);
if (compatibility.suggestion) {
console.error(info(compatibility.suggestion));
}
process.exit(1);
}
}
if (profileInfo.type === 'default') {
@@ -428,6 +518,8 @@ async function main(): Promise<void> {
console.error(fail(`${displayName} CLI not found.`));
if (resolvedTarget === 'droid') {
console.error(info('Install: npm i -g @factory/cli'));
} else if (resolvedTarget === 'codex') {
console.error(info('Install a recent @openai/codex build, then retry.'));
}
process.exit(1);
}
@@ -446,24 +538,16 @@ async function main(): Promise<void> {
}
let targetRemainingArgs = remainingArgs;
let droidReasoningOverride: string | number | undefined;
let runtimeReasoningOverride: string | number | undefined;
if (resolvedTarget === 'droid') {
try {
const droidRoute = routeDroidCommandArgs(remainingArgs);
targetRemainingArgs = droidRoute.argsForDroid;
if (droidRoute.mode === 'interactive') {
const runtime = resolveDroidReasoningRuntime(remainingArgs, process.env.CCS_THINKING);
const runtime = resolveRuntimeReasoningFlags(remainingArgs, process.env.CCS_THINKING);
targetRemainingArgs = runtime.argsWithoutReasoningFlags;
droidReasoningOverride = runtime.reasoningOverride;
if (runtime.duplicateDisplays.length > 0) {
console.error(
warn(
`[!] Multiple reasoning flags detected. Using first occurrence: ${runtime.sourceDisplay || '<first-flag>'}`
)
);
}
runtimeReasoningOverride = runtime.reasoningOverride;
} else {
if (droidRoute.duplicateReasoningDisplays.length > 0) {
console.error(
@@ -480,11 +564,36 @@ async function main(): Promise<void> {
}
} catch (error) {
if (error instanceof DroidReasoningFlagError || error instanceof DroidCommandRouterError) {
console.error(fail(error.message));
console.error(' Examples: --thinking low, --thinking 8192, --thinking off');
console.error(' Codex alias: --effort medium|high|xhigh');
console.error(' Droid exec: --reasoning-effort high');
process.exit(1);
exitWithRuntimeReasoningFlagError(error.message, {
codexAliasLevels: 'minimal|low|medium|high|xhigh',
includeDroidExecExample: true,
});
}
throw error;
}
} else if (resolvedTarget === 'codex') {
try {
const runtime = resolveRuntimeReasoningFlags(remainingArgs, process.env.CCS_THINKING);
targetRemainingArgs = runtime.argsWithoutReasoningFlags;
const normalizedReasoning = normalizeCodexRuntimeReasoningOverride(
runtime.reasoningOverride
);
if (runtime.reasoningOverride !== undefined && !normalizedReasoning) {
if (runtime.reasoningSource === 'flag') {
throw new DroidReasoningFlagError(
'Codex target supports reasoning levels only: minimal, low, medium, high, xhigh.',
'--effort'
);
}
runtimeReasoningOverride = undefined;
} else {
runtimeReasoningOverride = normalizedReasoning;
}
} catch (error) {
if (error instanceof DroidReasoningFlagError) {
exitWithRuntimeReasoningFlagError(error.message, {
codexAliasLevels: 'minimal|low|medium|high|xhigh',
});
}
throw error;
}
@@ -625,7 +734,7 @@ async function main(): Promise<void> {
baseUrl: envVars['ANTHROPIC_BASE_URL'],
model: envVars['ANTHROPIC_MODEL'],
}),
reasoningOverride: droidReasoningOverride,
reasoningOverride: runtimeReasoningOverride,
envVars,
};
@@ -643,7 +752,11 @@ async function main(): Promise<void> {
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
@@ -718,10 +831,32 @@ async function main(): Promise<void> {
);
}
const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
const expandedSettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name);
const settings = loadSettings(expandedSettingsPath);
const expandedSettingsPath =
resolvedSettingsPath ??
(profileInfo.settingsPath
? expandPath(profileInfo.settingsPath)
: getSettingsPath(profileInfo.name));
const settings = resolvedSettings ?? loadSettings(expandedSettingsPath);
const cliproxyBridge = resolvedCliproxyBridge ?? resolveCliproxyBridgeMetadata(settings);
if (resolvedTarget !== 'claude') {
const compatibility = evaluateTargetRuntimeCompatibility({
target: resolvedTarget,
profileType: profileInfo.type,
cliproxyBridgeProvider: cliproxyBridge?.provider ?? null,
});
if (!compatibility.supported) {
console.error(
fail(
compatibility.reason ||
`${targetAdapter?.displayName || resolvedTarget} does not support this profile.`
)
);
if (compatibility.suggestion) {
console.error(info(compatibility.suggestion));
}
process.exit(1);
}
}
const rawSettingsEnv = profileInfo.env ?? settings.env ?? {};
const isDeprecatedGlmtProfile = isDeprecatedGlmtProfileName(profileInfo.name);
const glmtNormalization = isDeprecatedGlmtProfile
@@ -841,11 +976,15 @@ async function main(): Promise<void> {
baseUrl: directAnthropicBaseUrl,
model: settingsEnv['ANTHROPIC_MODEL'],
}),
reasoningOverride: droidReasoningOverride,
reasoningOverride: runtimeReasoningOverride,
envVars,
};
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs);
const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs, {
creds,
profileType: profileInfo.type,
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
@@ -938,9 +1077,9 @@ async function main(): Promise<void> {
baseUrl: process.env['ANTHROPIC_BASE_URL'],
model: process.env['ANTHROPIC_MODEL'],
}),
reasoningOverride: droidReasoningOverride,
reasoningOverride: runtimeReasoningOverride,
};
if (!creds.baseUrl || !creds.apiKey) {
if (resolvedTarget === 'droid' && (!creds.baseUrl || !creds.apiKey)) {
console.error(
fail(
`${adapter.displayName} default mode requires ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN`
@@ -950,7 +1089,11 @@ async function main(): Promise<void> {
process.exit(1);
}
await adapter.prepareCredentials(creds);
const targetArgs = adapter.buildArgs('default', targetRemainingArgs);
const targetArgs = adapter.buildArgs('default', targetRemainingArgs, {
creds,
profileType: 'default',
binaryInfo: targetBinaryInfo || undefined,
});
const targetEnv = adapter.buildEnv(creds, 'default');
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
return;
+1 -1
View File
@@ -292,7 +292,7 @@ export function getOfficialChannelsStateScopeMessage(): string {
}
export function getOfficialChannelsSupportMessage(): string {
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or Droid targets such as `ccs glm`, `ccs gemini`, `ccs codex`, or `ccs --target droid`.';
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or non-Claude targets such as `ccs glm`, `ccs gemini`, `ccs codex`, `ccs --target droid`, or `ccs --target codex`.';
}
export function getOfficialChannelsAccountStatusCaveat(): string {
@@ -417,6 +417,17 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
` ${color(`ccs ${result.name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
if (cliproxyProvider === 'codex') {
console.log(
` ${color(`ccs ${result.name} --target codex "your prompt"`, 'command')} ${dim('# native Codex runtime')}`
);
console.log(
` ${color(`ccs-codex ${result.name} "your prompt"`, 'command')} ${dim('# explicit Codex alias')}`
);
console.log(
` ${color(`ccsx ${result.name} "your prompt"`, 'command')} ${dim('# short alias')}`
);
}
console.log('');
console.log(dim('Manage provider accounts, keys, and models in: ccs cliproxy'));
return;
+4
View File
@@ -107,6 +107,10 @@ export async function showApiCommandHelp(writeLine: HelpWriter = console.log): P
writeLine(
` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}`
);
writeLine(` ${color('ccs api create codex-api --cliproxy-provider codex', 'command')}`);
writeLine(
` ${color('ccs codex-api --target codex', 'command')} ${dim('# runtime-only native Codex launch')}`
);
writeLine('');
writeLine(` ${dim('# Create with name')}`);
writeLine(` ${color('ccs api create myapi', 'command')}`);
+6 -3
View File
@@ -1,5 +1,6 @@
import type { ModelMapping } from '../../api/services';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import {
applyExtendedContextSuffix,
hasExtendedContextSuffix,
@@ -108,7 +109,7 @@ export function extractPositionalArgs(args: string[]): string[] {
function parseTargetValue(value: string): TargetType | null {
const normalized = value.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
return null;
@@ -132,7 +133,7 @@ export function parseOptionalTargetFlag(
if (!target) {
return {
remainingArgs: extracted.remainingArgs,
errors: [`Invalid --target value "${extracted.value}". Use: claude or droid`],
errors: [`Invalid --target value "${extracted.value}". Use: ${getPersistedTargetChoices()}`],
};
}
@@ -251,7 +252,9 @@ export function parseApiCommandArgs(
(value) => {
const target = parseTargetValue(value);
if (!target) {
result.errors.push(`Invalid --target value "${value}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${value}". Use: ${getPersistedTargetChoices()}`
);
return;
}
result.target = target;
+8 -3
View File
@@ -13,6 +13,7 @@ import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detec
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { initUI, header, color, ok, fail, warn, info, infoBox, dim } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt';
@@ -42,7 +43,7 @@ interface CliproxyProfileArgs {
function parseTargetValue(rawValue: string): TargetType | null {
const normalized = rawValue.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
return null;
@@ -73,7 +74,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
i += 1;
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
);
} else {
result.target = parsedTarget;
}
@@ -82,7 +85,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
const rawValue = arg.slice('--target='.length);
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
);
} else {
result.target = parsedTarget;
}
+14 -3
View File
@@ -236,7 +236,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
'ccs <provider> --thinking <value>',
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
],
['ccs codex --effort <level>', 'Set codex reasoning effort (medium/high/xhigh)'],
['ccs codex --effort <level>', 'Set codex reasoning effort (minimal/low/medium/high/xhigh)'],
['ccs <provider> --1m', 'Request explicit 1M context when the selected model supports [1m]'],
['ccs <provider> --no-1m', 'Force standard context / clear [1m]'],
['ccs <provider> --logout', 'Clear authentication'],
@@ -391,7 +391,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
'Flags',
[
['--config-dir <path>', 'Use custom CCS config directory'],
['--target <cli>', 'Target CLI: claude (default), droid'],
['--target <cli>', 'Target CLI: claude (default), droid, codex (runtime-only)'],
['-h, --help', 'Show this help message'],
['-v, --version', 'Show version and installation info'],
['-sc, --shell-completion', 'Install shell auto-completion'],
@@ -405,6 +405,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
[
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
['ccs-codex <profile> [args]', 'Explicit Codex runtime alias'],
['ccsx <profile> [args]', 'Short alias for: ccs-codex <profile> [args]'],
],
writeLine
);
@@ -416,6 +418,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
['ccs-droid glm', 'Same as above (explicit alias)'],
['ccsd glm', 'Legacy shortcut for ccs-droid'],
['ccs --target codex', 'Open a native Codex session with your existing ~/.codex setup'],
['ccs-codex', 'Same as above (explicit Codex alias)'],
['ccsx', 'Short alias for ccs-codex'],
['ccs codex --target codex', 'Run built-in CLIProxy Codex on native Codex CLI'],
[
'ccs api create codex-api --cliproxy-provider codex',
'Create a routed API bridge that can also run on Codex',
],
['ccs codex-api --target codex', 'Run a Codex bridge profile on native Codex CLI'],
['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'],
['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
[
@@ -512,7 +523,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['--thinking xhigh', '32K tokens - Maximum depth'],
['--thinking <number>', 'Custom token budget (512-100000)'],
['', ''],
['--effort <level>', 'Codex alias for reasoning effort (medium/high/xhigh)'],
['--effort <level>', 'Codex alias for reasoning effort (minimal/low/medium/high/xhigh)'],
['--effort xhigh', 'Pin Codex effort to xhigh for this run'],
['', ''],
['Droid exec:', 'Use native Droid flag: --reasoning-effort <level>'],
+215
View File
@@ -0,0 +1,215 @@
export interface CompatibleCliDocLink {
id: string;
label: string;
url: string;
category: 'overview' | 'configuration' | 'byok' | 'reference';
source: 'factory' | 'provider' | 'openai' | 'github';
description: string;
}
export interface CompatibleCliProviderDocLink {
provider: string;
label: string;
apiFormat: string;
url: string;
}
export interface CompatibleCliDocsReference {
providerValues: string[];
settingsHierarchy: string[];
notes: string[];
links: CompatibleCliDocLink[];
providerDocs: CompatibleCliProviderDocLink[];
}
export type CodexBinarySource = 'CCS_CODEX_PATH' | 'PATH' | 'missing';
export interface CodexBinaryDiagnostics {
installed: boolean;
path: string | null;
installDir: string | null;
source: CodexBinarySource;
version: string | null;
overridePath: string | null;
supportsConfigOverrides: boolean;
}
export interface CodexConfigFileDiagnostics {
label: string;
path: string;
resolvedPath: string;
exists: boolean;
isSymlink: boolean;
isRegularFile: boolean;
sizeBytes: number | null;
mtimeMs: number | null;
parseError: string | null;
readError: string | null;
}
export interface CodexModelProviderDiagnostics {
name: string;
baseUrl: string | null;
envKey: string | null;
wireApi: string | null;
requiresOpenaiAuth: boolean;
supportsWebsockets: boolean;
hasQueryParams: boolean;
hasHttpHeaders: boolean;
usesExperimentalBearerToken: boolean;
}
export interface CodexFeatureFlagDiagnostics {
name: string;
state: 'enabled' | 'disabled' | 'custom';
}
export interface CodexProjectTrustDiagnostics {
path: string;
trustLevel: string;
}
export interface CodexMcpServerDiagnostics {
name: string;
transport: 'stdio' | 'streamable-http' | 'unknown';
enabled: boolean;
required: boolean;
startupTimeoutSec: number | null;
toolTimeoutSec: number | null;
enabledToolsCount: number;
disabledToolsCount: number;
usesInlineBearerToken: boolean;
}
export interface CodexSupportMatrixEntry {
id: string;
label: string;
supported: boolean;
notes: string;
}
export interface CodexUserConfigDiagnostics {
model: string | null;
modelReasoningEffort: string | null;
modelProvider: string | null;
activeProfile: string | null;
approvalPolicy: string | null;
sandboxMode: string | null;
webSearch: string | null;
toolOutputTokenLimit: number | null;
personality: string | null;
topLevelKeys: string[];
profileCount: number;
profileNames: string[];
modelProviderCount: number;
modelProviders: CodexModelProviderDiagnostics[];
featureCount: number;
enabledFeatures: CodexFeatureFlagDiagnostics[];
disabledFeatures: CodexFeatureFlagDiagnostics[];
trustedProjectCount: number;
untrustedProjectCount: number;
projectTrust: CodexProjectTrustDiagnostics[];
mcpServerCount: number;
mcpServers: CodexMcpServerDiagnostics[];
}
export interface CodexDashboardDiagnostics {
binary: CodexBinaryDiagnostics;
file: CodexConfigFileDiagnostics;
workspacePath: string;
config: CodexUserConfigDiagnostics;
supportMatrix: CodexSupportMatrixEntry[];
warnings: string[];
docsReference: CompatibleCliDocsReference;
}
export interface CodexRawConfigResponse {
path: string;
resolvedPath: string;
exists: boolean;
mtime: number;
rawText: string;
config: Record<string, unknown> | null;
parseError: string | null;
readError: string | null;
}
export interface CodexTopLevelSettingsPatch {
model?: string | null;
modelReasoningEffort?: string | null;
modelProvider?: string | null;
approvalPolicy?: string | null;
sandboxMode?: string | null;
webSearch?: string | null;
toolOutputTokenLimit?: number | null;
personality?: string | null;
}
export interface CodexProfilePatchValues extends CodexTopLevelSettingsPatch {}
export interface CodexModelProviderPatchValues {
displayName?: string | null;
baseUrl?: string | null;
envKey?: string | null;
wireApi?: string | null;
requiresOpenaiAuth?: boolean | null;
supportsWebsockets?: boolean | null;
}
export interface CodexMcpServerPatchValues {
transport: 'stdio' | 'streamable-http';
command?: string | null;
args?: string[] | null;
url?: string | null;
enabled?: boolean | null;
required?: boolean | null;
startupTimeoutSec?: number | null;
toolTimeoutSec?: number | null;
enabledTools?: string[] | null;
disabledTools?: string[] | null;
}
export type CodexConfigPatchInput =
| {
kind: 'top-level';
expectedMtime?: number;
values: CodexTopLevelSettingsPatch;
}
| {
kind: 'project-trust';
expectedMtime?: number;
path: string;
trustLevel: string | null;
}
| {
kind: 'feature';
expectedMtime?: number;
feature: string;
enabled: boolean | null;
}
| {
kind: 'profile';
expectedMtime?: number;
action: 'set-active' | 'upsert' | 'delete';
name: string;
values?: CodexProfilePatchValues;
setAsActive?: boolean;
}
| {
kind: 'model-provider';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexModelProviderPatchValues;
}
| {
kind: 'mcp-server';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexMcpServerPatchValues;
};
export interface CodexConfigPatchResult extends CodexRawConfigResponse {
success: true;
}
+36
View File
@@ -0,0 +1,36 @@
import { parse } from 'smol-toml';
export interface SafeTomlObjectParseResult {
config: Record<string, unknown> | null;
parseError: string | null;
}
function isTomlObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function parseTomlObject(rawText: string): Record<string, unknown> {
const trimmed = rawText.trim();
if (!trimmed) return {};
const parsed = parse(rawText);
if (!isTomlObject(parsed)) {
throw new Error('TOML root must be a table.');
}
return parsed;
}
export function safeParseTomlObject(rawText: string): SafeTomlObjectParseResult {
try {
return {
config: parseTomlObject(rawText),
parseError: null,
};
} catch (error) {
return {
config: null,
parseError: (error as Error).message,
};
}
}
+243
View File
@@ -0,0 +1,243 @@
import { ChildProcess, spawn } from 'child_process';
import * as fs from 'fs';
import type { ProfileType } from '../types/profile';
import { runCleanup } from '../errors';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { escapeShellArg, stripAnthropicEnv } from '../utils/shell-executor';
import type {
TargetAdapter,
TargetBinaryInfo,
TargetCredentials,
TargetType,
} from './target-adapter';
import {
codexBinarySupportsConfigOverrides,
detectCodexCli,
getCodexBinaryInfo,
} from './codex-detector';
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']);
function formatTomlString(value: string): string {
return JSON.stringify(value);
}
function buildConfigOverrideArgs(overrides: string[]): string[] {
return overrides.flatMap((override) => ['-c', override]);
}
function buildConfigOverrideSupportError(binaryInfo?: TargetBinaryInfo): Error {
const versionSummary = binaryInfo?.version ? ` (${binaryInfo.version})` : '';
return new Error(
`Codex CLI${versionSummary} does not advertise --config overrides. Upgrade Codex before using CCS-backed Codex profiles or runtime reasoning overrides.`
);
}
function findDisallowedCodexManagedFlags(args: string[]): string[] {
const disallowed = new Set<string>();
for (const arg of args) {
if (arg === '-c' || arg === '--config' || arg.startsWith('--config=')) {
disallowed.add('--config/-c');
continue;
}
if (arg === '-p' || arg === '--profile' || arg.startsWith('--profile=')) {
disallowed.add('--profile/-p');
continue;
}
if (arg === '--oss') {
disallowed.add('--oss');
continue;
}
if (arg === '--local-provider' || arg.startsWith('--local-provider=')) {
disallowed.add('--local-provider');
}
}
return [...disallowed];
}
function normalizeCodexReasoningOverride(value: string | number | undefined): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value === 'string' && CODEX_REASONING_LEVELS.has(value)) {
return value;
}
throw new Error(
'Codex target supports reasoning levels only: minimal, low, medium, high, xhigh.'
);
}
export class CodexAdapter implements TargetAdapter {
readonly type: TargetType = 'codex';
readonly displayName = 'Codex CLI';
detectBinary(): TargetBinaryInfo | null {
return getCodexBinaryInfo();
}
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
// Codex uses transient -c overrides plus env_key injection.
}
buildArgs(
_profile: string,
userArgs: string[],
options?: {
creds?: TargetCredentials;
profileType?: ProfileType;
binaryInfo?: TargetBinaryInfo;
}
): string[] {
const profileType = options?.profileType || 'default';
const creds = options?.creds;
const reasoningOverride = normalizeCodexReasoningOverride(creds?.reasoningOverride);
if (profileType === 'default') {
if (reasoningOverride) {
if (!codexBinarySupportsConfigOverrides(options?.binaryInfo)) {
throw buildConfigOverrideSupportError(options?.binaryInfo);
}
return [
...buildConfigOverrideArgs([
`model_reasoning_effort=${formatTomlString(reasoningOverride)}`,
]),
...userArgs,
];
}
return userArgs;
}
if (!codexBinarySupportsConfigOverrides(options?.binaryInfo)) {
throw buildConfigOverrideSupportError(options?.binaryInfo);
}
if (!creds?.baseUrl?.trim() || !creds.apiKey?.trim()) {
throw new Error(
'Codex target requires base URL and API key for CCS-backed profile launches.'
);
}
const disallowedFlags = findDisallowedCodexManagedFlags(userArgs);
if (disallowedFlags.length > 0) {
throw new Error(
`Codex target does not allow ${disallowedFlags.join(', ')} when CCS manages the runtime provider. Remove native Codex provider selection flags and retry.`
);
}
const overrides = [
`model_provider=${formatTomlString(CODEX_RUNTIME_PROVIDER_ID)}`,
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.name=${formatTomlString('CCS Runtime')}`,
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.base_url=${formatTomlString(creds.baseUrl)}`,
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.env_key=${formatTomlString(CODEX_RUNTIME_ENV_KEY)}`,
`model_providers.${CODEX_RUNTIME_PROVIDER_ID}.wire_api=${formatTomlString('responses')}`,
];
if (creds.model?.trim()) {
overrides.push(`model=${formatTomlString(creds.model)}`);
}
if (reasoningOverride) {
overrides.push(`model_reasoning_effort=${formatTomlString(reasoningOverride)}`);
}
return [...buildConfigOverrideArgs(overrides), ...userArgs];
}
buildEnv(creds: TargetCredentials, profileType: ProfileType): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...stripAnthropicEnv(process.env) };
delete env[CODEX_RUNTIME_ENV_KEY];
if (profileType !== 'default') {
if (!creds.apiKey?.trim()) {
throw new Error('Codex target requires an API key for CCS-backed profile launches.');
}
env[CODEX_RUNTIME_ENV_KEY] = creds.apiKey;
}
return env;
}
exec(
args: string[],
env: NodeJS.ProcessEnv,
options?: { cwd?: string; binaryInfo?: TargetBinaryInfo }
): void {
const exitWithCleanup = (code: number): never => {
try {
runCleanup();
} catch {
// Cleanup is best-effort on launch errors.
}
process.exit(code);
};
const codexPath = options?.binaryInfo?.path || detectCodexCli();
if (!codexPath) {
console.error('[X] Codex CLI not found. Install a recent @openai/codex build first.');
return exitWithCleanup(1);
}
try {
const stat = fs.statSync(codexPath);
if (!stat.isFile()) {
console.error(`[X] Codex CLI path is not a file: ${codexPath}`);
return exitWithCleanup(1);
}
} catch (err) {
const error = err as NodeJS.ErrnoException;
console.error(
`[X] Codex CLI path is not accessible (${error.code || 'unknown'}): ${codexPath}`
);
return exitWithCleanup(1);
}
const isWindows = process.platform === 'win32';
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
let child: ChildProcess;
if (isPowerShellScript) {
child = spawn(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
{ stdio: 'inherit', windowsHide: true, env }
);
} else if (needsShell) {
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
child = spawn(cmdString, {
stdio: 'inherit',
windowsHide: true,
shell: true,
env,
});
} else {
child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env });
}
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
if (err.code === 'EACCES') {
console.error(`[X] Codex CLI is not executable: ${codexPath}`);
console.error(' Check file permissions and executable bit.');
} else if (err.code === 'ENOENT') {
if (isPowerShellScript) {
console.error('[X] PowerShell executable not found (required for .ps1 wrapper launch).');
} else if (needsShell) {
console.error('[X] Windows command shell not found for Codex wrapper launch.');
} else {
console.error(`[X] Codex CLI not found: ${codexPath}`);
}
} else {
console.error(`[X] Failed to start Codex CLI (${codexPath}): ${err.message}`);
}
return exitWithCleanup(1);
});
}
supportsProfileType(profileType: ProfileType): boolean {
// Bridge-backed settings profiles need additional compatibility context that the
// adapter contract does not receive, so keep the adapter-level claim conservative.
return profileType === 'default' || profileType === 'cliproxy';
}
}
+138
View File
@@ -0,0 +1,138 @@
import * as fs from 'fs';
import * as childProcess from 'child_process';
import { expandPath } from '../utils/helpers';
import { escapeShellArg } from '../utils/shell-executor';
import type { TargetBinaryInfo } from './target-adapter';
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
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);
try {
if (isPowerShellScript) {
return childProcess.execFileSync(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
{
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
windowsHide: true,
}
);
}
if (needsShell) {
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
return childProcess.execFileSync('cmd.exe', ['/d', '/s', '/c', cmdString], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
windowsHide: true,
});
}
return childProcess.execFileSync(codexPath, args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
});
} catch {
return undefined;
}
}
function readCodexVersion(codexPath: string): string | undefined {
return runCodexProbe(codexPath, ['--version'])?.trim();
}
function detectCodexFeatures(codexPath: string): readonly string[] {
const helpText = runCodexProbe(codexPath, ['--help']);
return helpText?.includes('--config <key=value>') ? [CODEX_CONFIG_OVERRIDE_FEATURE] : [];
}
export function detectCodexCli(): string | null {
if (process.env.CCS_CODEX_PATH) {
const customPath = expandPath(process.env.CCS_CODEX_PATH);
try {
if (fs.statSync(customPath).isFile()) {
return customPath;
}
console.warn('[!] CCS_CODEX_PATH points to a directory, not a file:', customPath);
console.warn(' Refusing PATH fallback while CCS_CODEX_PATH is explicitly set.');
return null;
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code === 'ENOENT') {
console.warn('[!] Warning: CCS_CODEX_PATH is set but file not found:', customPath);
} else {
console.warn(
`[!] Warning: CCS_CODEX_PATH is not accessible (${error.code || 'unknown error'}):`,
customPath
);
}
console.warn(' Refusing PATH fallback while CCS_CODEX_PATH is explicitly set.');
return null;
}
}
const isWindows = process.platform === 'win32';
try {
const cmd = isWindows ? 'where.exe codex' : 'which codex';
const result = childProcess
.execSync(cmd, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
})
.trim();
const matches = result
.split('\n')
.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;
for (const candidate of candidates) {
try {
if (fs.statSync(candidate).isFile()) {
return candidate;
}
} catch {
// Ignore disappearing PATH candidates.
}
}
} catch {
// codex not in PATH
}
return null;
}
export function getCodexBinaryInfo(): TargetBinaryInfo | null {
const codexPath = detectCodexCli();
if (!codexPath) return null;
const isWindows = process.platform === 'win32';
return {
path: codexPath,
needsShell: isWindows && /\.(cmd|bat|ps1)$/i.test(codexPath),
version: readCodexVersion(codexPath),
features: detectCodexFeatures(codexPath),
};
}
export function codexBinarySupportsConfigOverrides(
binaryInfo: TargetBinaryInfo | null | undefined
): boolean {
return Boolean(binaryInfo?.features?.includes(CODEX_CONFIG_OVERRIDE_FEATURE));
}
+16
View File
@@ -19,7 +19,13 @@ export {
} from './target-registry';
export { ClaudeAdapter } from './claude-adapter';
export { DroidAdapter } from './droid-adapter';
export { CodexAdapter } from './codex-adapter';
export { getDroidBinaryInfo, detectDroidCli, checkDroidVersion } from './droid-detector';
export {
codexBinarySupportsConfigOverrides,
getCodexBinaryInfo,
detectCodexCli,
} from './codex-detector';
export {
upsertCcsModel,
removeCcsModel,
@@ -30,3 +36,13 @@ export type { DroidCustomModel } from './droid-config-manager';
export { resolveDroidProvider, normalizeDroidProvider } from './droid-provider';
export type { DroidProvider } from './droid-provider';
export { resolveTargetType, stripTargetFlag } from './target-resolver';
export {
TARGET_METADATA,
RUNTIME_TARGET_TYPES,
PERSISTED_TARGET_TYPES,
getPersistedTargetChoices,
getRuntimeTargetChoices,
isPersistedTargetType,
isRuntimeTargetType,
} from './target-metadata';
export { evaluateTargetRuntimeCompatibility } from './target-runtime-compatibility';
+12 -2
View File
@@ -11,7 +11,7 @@
*/
import type { ProfileType } from '../types/profile';
export type TargetType = 'claude' | 'droid';
export type TargetType = 'claude' | 'droid' | 'codex';
/**
* Credentials resolved by CCS profile system, ready for delivery to target CLI.
@@ -39,6 +39,8 @@ export interface TargetCredentials {
export interface TargetBinaryInfo {
path: string;
needsShell: boolean; // Windows .cmd/.bat/.ps1
version?: string;
features?: readonly string[];
}
/**
@@ -72,7 +74,15 @@ export interface TargetAdapter {
* Build target-specific argument vector.
* `userArgs` are the arguments after CCS profile/flag parsing.
*/
buildArgs(profile: string, userArgs: string[]): string[];
buildArgs(
profile: string,
userArgs: string[],
options?: {
creds?: TargetCredentials;
profileType?: ProfileType;
binaryInfo?: TargetBinaryInfo;
}
): string[];
/**
* Build environment variables for process spawn.
+87
View File
@@ -0,0 +1,87 @@
import type { TargetType } from './target-adapter';
export interface TargetMetadata {
displayName: string;
runtimeAliases: readonly string[];
legacyAliasEnvVar?: string;
persistedTarget: boolean;
}
export const TARGET_METADATA: Record<TargetType, TargetMetadata> = {
claude: {
displayName: 'Claude Code',
runtimeAliases: [],
persistedTarget: true,
},
droid: {
displayName: 'Factory Droid',
runtimeAliases: ['ccs-droid', 'ccsd'],
legacyAliasEnvVar: 'CCS_DROID_ALIASES',
persistedTarget: true,
},
codex: {
displayName: 'Codex CLI',
runtimeAliases: ['ccs-codex', 'ccsx'],
legacyAliasEnvVar: 'CCS_CODEX_ALIASES',
persistedTarget: false,
},
} satisfies Record<TargetType, TargetMetadata>;
export const RUNTIME_TARGET_TYPES = Object.freeze(
Object.keys(TARGET_METADATA) as TargetType[]
) as readonly TargetType[];
export const PERSISTED_TARGET_TYPES = Object.freeze(
RUNTIME_TARGET_TYPES.filter((target) => TARGET_METADATA[target].persistedTarget)
) as readonly TargetType[];
const RUNTIME_TARGET_SET = new Set<TargetType>(RUNTIME_TARGET_TYPES);
const PERSISTED_TARGET_SET = new Set<TargetType>(PERSISTED_TARGET_TYPES);
export function isRuntimeTargetType(value: unknown): value is TargetType {
return typeof value === 'string' && RUNTIME_TARGET_SET.has(value as TargetType);
}
export function isPersistedTargetType(value: unknown): value is TargetType {
return typeof value === 'string' && PERSISTED_TARGET_SET.has(value as TargetType);
}
export function formatTargetChoices(
targets: readonly TargetType[],
conjunction: 'or' | 'comma' = 'comma'
): string {
if (targets.length === 0) return '';
if (targets.length === 1) return targets[0];
if (conjunction === 'comma') return targets.join(', ');
if (targets.length === 2) return `${targets[0]} or ${targets[1]}`;
return `${targets.slice(0, -1).join(', ')}, or ${targets[targets.length - 1]}`;
}
export function getPersistedTargetChoices(): string {
return formatTargetChoices(PERSISTED_TARGET_TYPES, 'or');
}
export function getRuntimeTargetChoices(): string {
return formatTargetChoices(RUNTIME_TARGET_TYPES, 'comma');
}
export function getBuiltinArgv0TargetMap(): Record<string, TargetType> {
const map: Record<string, TargetType> = {};
for (const target of RUNTIME_TARGET_TYPES) {
for (const alias of TARGET_METADATA[target].runtimeAliases) {
map[alias] = target;
}
}
return map;
}
export function getLegacyTargetAliasEnvVars(): Partial<Record<TargetType, string>> {
const result: Partial<Record<TargetType, string>> = {};
for (const target of RUNTIME_TARGET_TYPES) {
const envVar = TARGET_METADATA[target].legacyAliasEnvVar;
if (envVar) {
result[target] = envVar;
}
}
return result;
}
+18 -24
View File
@@ -10,21 +10,24 @@
import * as path from 'path';
import { TargetType } from './target-adapter';
import {
getBuiltinArgv0TargetMap,
getLegacyTargetAliasEnvVars,
getRuntimeTargetChoices,
isPersistedTargetType,
isRuntimeTargetType,
} from './target-metadata';
/**
* Built-in argv[0] aliases for explicit runtime entrypoints.
* `ccs-droid` is the transparent alias; `ccsd` remains as a legacy shortcut.
* Droid and Codex install dedicated runtime aliases alongside the base `ccs` bin.
*/
const BUILTIN_ARGV0_TARGET_MAP: Record<string, TargetType> = {
'ccs-droid': 'droid',
ccsd: 'droid',
};
const BUILTIN_ARGV0_TARGET_MAP: Record<string, TargetType> = getBuiltinArgv0TargetMap();
const ALIAS_NAME_REGEX = /^[a-z0-9._-]+$/;
const INTERNAL_ENTRY_TARGET_ENV_VAR = 'CCS_INTERNAL_ENTRY_TARGET';
const GENERIC_TARGET_ALIAS_ENV_VAR = 'CCS_TARGET_ALIASES';
const LEGACY_TARGET_ALIAS_ENV_VARS: Partial<Record<TargetType, string>> = {
droid: 'CCS_DROID_ALIASES',
};
const LEGACY_TARGET_ALIAS_ENV_VARS: Partial<Record<TargetType, string>> =
getLegacyTargetAliasEnvVars();
const RESERVED_BIN_NAMES = new Set<string>(['ccs', ...Object.keys(BUILTIN_ARGV0_TARGET_MAP)]);
function addAliasToMap(map: Record<string, TargetType>, alias: string, target: TargetType): void {
@@ -64,7 +67,7 @@ function parseGenericTargetAliasConfig(map: Record<string, TargetType>, rawConfi
const rawTarget = entry.slice(0, separatorIndex).trim().toLowerCase();
const rawAliases = entry.slice(separatorIndex + 1).trim();
if (!rawAliases || !isValidTarget(rawTarget)) {
if (!rawAliases || !isRuntimeTargetType(rawTarget)) {
continue;
}
@@ -100,30 +103,21 @@ function resolveEntrypointTarget(): TargetType | null {
}
const normalizedTarget = rawTarget.trim().toLowerCase();
return isValidTarget(normalizedTarget) ? normalizedTarget : null;
return isRuntimeTargetType(normalizedTarget) ? normalizedTarget : null;
}
/**
* Valid target types for --target flag validation.
*/
const VALID_TARGETS: ReadonlySet<string> = new Set<TargetType>(['claude', 'droid']);
interface ParsedTargetFlags {
targetOverride?: TargetType;
cleanedArgs: string[];
}
function isValidTarget(target: unknown): target is TargetType {
return typeof target === 'string' && VALID_TARGETS.has(target as TargetType);
}
function normalizeTargetValue(value: string): TargetType {
const normalized = value.toLowerCase();
if (isValidTarget(normalized)) {
if (isRuntimeTargetType(normalized)) {
return normalized as TargetType;
}
const available = Array.from(VALID_TARGETS).join(', ');
const available = getRuntimeTargetChoices();
throw new Error(`Unknown target "${value}". Available: ${available}`);
}
@@ -148,7 +142,7 @@ function parseTargetFlags(args: string[]): ParsedTargetFlags {
if (arg === '--target') {
const value = args[i + 1];
if (!value || value.startsWith('-')) {
throw new Error('--target requires a value (claude or droid)');
throw new Error(`--target requires a value (${getRuntimeTargetChoices()})`);
}
targetOverride = normalizeTargetValue(value);
i += 1; // Skip value
@@ -158,7 +152,7 @@ function parseTargetFlags(args: string[]): ParsedTargetFlags {
if (arg.startsWith('--target=')) {
const value = arg.slice('--target='.length).trim();
if (!value) {
throw new Error('--target requires a value (claude or droid)');
throw new Error(`--target requires a value (${getRuntimeTargetChoices()})`);
}
targetOverride = normalizeTargetValue(value);
continue;
@@ -204,7 +198,7 @@ export function resolveTargetType(
// 3. Check per-profile config
if (profileConfig?.target !== undefined) {
return isValidTarget(profileConfig.target) ? profileConfig.target : 'claude';
return isPersistedTargetType(profileConfig.target) ? profileConfig.target : 'claude';
}
// 4. Default
@@ -0,0 +1,91 @@
import type { CLIProxyProvider } from '../cliproxy/types';
import type { ProfileType } from '../types/profile';
import type { TargetType } from './target-adapter';
export interface TargetRuntimeCompatibilityInput {
target: TargetType;
profileType: ProfileType;
cliproxyProvider?: CLIProxyProvider;
cliproxyBridgeProvider?: CLIProxyProvider | null;
isComposite?: boolean;
}
export interface TargetRuntimeCompatibilityResult {
supported: boolean;
reason?: string;
suggestion?: string;
}
function unsupported(reason: string, suggestion?: string): TargetRuntimeCompatibilityResult {
return { supported: false, reason, suggestion };
}
export function evaluateTargetRuntimeCompatibility(
input: TargetRuntimeCompatibilityInput
): TargetRuntimeCompatibilityResult {
if (input.target === 'claude') {
return { supported: true };
}
if (input.target === 'droid') {
if (input.profileType === 'account') {
return unsupported(
'Factory Droid does not support account-based Claude profiles.',
'Use a settings-based profile with --target droid instead.'
);
}
if (input.profileType === 'copilot') {
return unsupported('Factory Droid does not support Copilot profiles.');
}
return { supported: true };
}
if (input.profileType === 'account') {
return unsupported(
'Codex CLI does not support Claude account-based profiles.',
'Use native Codex auth with: ccs --target codex'
);
}
if (input.profileType === 'copilot') {
return unsupported('Codex CLI does not support Copilot profiles.');
}
if (input.profileType === 'default') {
return { supported: true };
}
if (input.profileType === 'cliproxy') {
if (input.isComposite) {
return unsupported(
'Codex CLI currently does not support composite CLIProxy variants.',
'Use a Codex-only CLIProxy profile or stay on Claude/Droid for composite variants.'
);
}
if (input.cliproxyProvider !== 'codex') {
return unsupported(
`Codex CLI only supports CLIProxy provider "codex". This profile routes to "${input.cliproxyProvider || 'unknown'}".`,
'Use: ccs codex --target codex, ccs-codex codex, or stay on Claude/Droid for other providers.'
);
}
return { supported: true };
}
if (input.profileType === 'settings') {
if (input.cliproxyBridgeProvider === 'codex') {
return { supported: true };
}
if (input.cliproxyBridgeProvider) {
return unsupported(
`Codex CLI only supports CLIProxy Codex bridge profiles. This API profile bridges "${input.cliproxyBridgeProvider}".`,
'Create a Codex bridge with: ccs api create --cliproxy-provider codex'
);
}
return unsupported(
'Codex CLI currently supports native default sessions and Codex-routed CLIProxy sessions only.',
'Use Claude/Droid for generic API profiles, or create a Codex bridge with: ccs api create --cliproxy-provider codex'
);
}
return unsupported('Unsupported Codex runtime combination.');
}
+89
View File
@@ -0,0 +1,89 @@
import type { Request, Response } from 'express';
import { Router } from 'express';
import {
CodexRawConfigConflictError,
CodexRawConfigValidationError,
getCodexDashboardDiagnostics,
getCodexRawConfig,
patchCodexConfig,
saveCodexRawConfig,
} from '../services/codex-dashboard-service';
const router = Router();
router.get('/diagnostics', async (_req: Request, res: Response): Promise<void> => {
try {
res.json(await getCodexDashboardDiagnostics());
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
router.get('/config/raw', async (_req: Request, res: Response): Promise<void> => {
try {
res.json(await getCodexRawConfig());
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
router.put('/config/raw', async (req: Request, res: Response): Promise<void> => {
try {
const { rawText, expectedMtime } = req.body ?? {};
if (typeof rawText !== 'string') {
res.status(400).json({ error: 'rawText must be a string.' });
return;
}
if (
expectedMtime !== undefined &&
(typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime))
) {
res.status(400).json({ error: 'expectedMtime must be a finite number when provided.' });
return;
}
res.json(await saveCodexRawConfig({ rawText, expectedMtime }));
} catch (error) {
if (error instanceof CodexRawConfigValidationError) {
res.status(400).json({ error: error.message });
return;
}
if (error instanceof CodexRawConfigConflictError) {
res.status(409).json({ error: error.message, mtime: error.mtime });
return;
}
res.status(500).json({ error: (error as Error).message });
}
});
router.patch('/config/patch', async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body ?? {};
if (typeof body.kind !== 'string' || body.kind.trim().length === 0) {
res.status(400).json({ error: 'kind is required.' });
return;
}
if (
body.expectedMtime !== undefined &&
(typeof body.expectedMtime !== 'number' || !Number.isFinite(body.expectedMtime))
) {
res.status(400).json({ error: 'expectedMtime must be a finite number when provided.' });
return;
}
res.json(await patchCodexConfig(body));
} catch (error) {
if (error instanceof CodexRawConfigValidationError) {
res.status(400).json({ error: error.message });
return;
}
if (error instanceof CodexRawConfigConflictError) {
res.status(409).json({ error: error.message, mtime: error.mtime });
return;
}
res.status(500).json({ error: (error as Error).message });
}
});
export default router;
+4
View File
@@ -24,6 +24,7 @@ import aiProviderRoutes from './ai-provider-routes';
import copilotRoutes from './copilot-routes';
import cursorRoutes from './cursor-routes';
import droidRoutes from './droid-routes';
import codexRoutes from './codex-routes';
import miscRoutes from './misc-routes';
import cliproxyServerRoutes from './proxy-routes';
import authRoutes from './auth-routes';
@@ -77,6 +78,9 @@ apiRoutes.use('/cursor', cursorRoutes);
// ==================== Droid ====================
apiRoutes.use('/droid', droidRoutes);
// ==================== Codex ====================
apiRoutes.use('/codex', codexRoutes);
// ==================== CLIProxy Server Settings ====================
apiRoutes.use('/cliproxy-server', cliproxyServerRoutes);
+10 -7
View File
@@ -23,6 +23,7 @@ import {
validateApiName,
} from '../../api/services';
import { normalizeDroidProvider } from '../../targets/droid-provider';
import { getPersistedTargetChoices } from '../../targets/target-metadata';
import { isCLIProxyProvider } from '../../cliproxy/provider-capabilities';
import { isAnthropicDirectProfile, updateSettingsFile, parseTarget } from './route-helpers';
@@ -100,7 +101,7 @@ router.post('/cliproxy-bridge', (req: Request, res: Response): void => {
const target = parseTarget(shape.payload.target);
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -155,7 +156,7 @@ router.post('/', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(target);
if (target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
if (providerHint !== undefined && parsedProvider === null) {
@@ -265,7 +266,7 @@ router.post('/orphans/register', (req: Request, res: Response): void => {
const force = payload.force === true;
if (payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -306,7 +307,7 @@ router.post('/:name/copy', (req: Request, res: Response): void => {
return;
}
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -357,7 +358,7 @@ router.post('/import', (req: Request, res: Response): void => {
const target = parseTarget(shape.payload.target);
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -368,7 +369,9 @@ router.post('/import', (req: Request, res: Response): void => {
}
const bundleTarget = (bundle as { profile?: { target?: unknown } }).profile?.target;
if (bundleTarget !== undefined && parseTarget(bundleTarget) === null) {
res.status(400).json({ error: 'Invalid bundle profile target. Expected: claude or droid' });
res.status(400).json({
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}`,
});
return;
}
@@ -418,7 +421,7 @@ router.put('/:name', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(target);
if (target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
if (providerHint !== undefined && parsedProvider === null) {
+3 -2
View File
@@ -18,6 +18,7 @@ import {
import type { CLIProxyProvider } from '../../cliproxy/types';
import type { Config, Settings } from '../../types/config';
import type { TargetType } from '../../targets/target-adapter';
import { isPersistedTargetType } from '../../targets/target-metadata';
import { ValidationError } from '../../errors/error-types';
/** Model mapping for API profiles */
@@ -438,7 +439,7 @@ export function validateFilePath(filePath: string): {
}
/**
* Parse and validate a target param (claude/droid). Returns null if invalid/absent.
* Parse and validate a persisted target param. Returns null if invalid/absent.
* Shared by profile-routes and variant-routes.
*/
export function parseTarget(rawTarget: unknown): TargetType | null {
@@ -451,7 +452,7 @@ export function parseTarget(rawTarget: unknown): TargetType | null {
}
const normalized = rawTarget.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
+3 -2
View File
@@ -7,6 +7,7 @@
import { Router, Request, Response } from 'express';
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { getPersistedTargetChoices } from '../../targets/target-metadata';
import { parseTarget } from './route-helpers';
import {
createVariant,
@@ -55,7 +56,7 @@ router.post('/', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(req.body.target);
if (req.body.target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -178,7 +179,7 @@ router.put('/:name', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(req.body.target);
if (req.body.target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -0,0 +1,873 @@
import * as os from 'os';
import * as path from 'path';
import { expandPath } from '../../utils/helpers';
import {
codexBinarySupportsConfigOverrides,
getCodexBinaryInfo,
} from '../../targets/codex-detector';
import type {
CodexConfigPatchInput,
CodexConfigPatchResult,
CodexDashboardDiagnostics,
CodexFeatureFlagDiagnostics,
CodexMcpServerDiagnostics,
CodexModelProviderDiagnostics,
CodexProjectTrustDiagnostics,
CodexRawConfigResponse,
CodexSupportMatrixEntry,
} from './compatible-cli-types';
import {
TomlFileConflictError,
TomlFileValidationError,
probeTomlObjectFile,
stringifyTomlObject,
writeTomlFileAtomic,
} from './compatible-cli-toml-file-service';
import { getCompatibleCliDocsReference } from './compatible-cli-docs-registry';
interface CodexConfigPaths {
configPath: string;
configDisplayPath: string;
baseDir: string;
baseDirDisplay: string;
}
interface SaveCodexRawConfigInput {
rawText: string;
expectedMtime?: number;
}
interface SaveCodexRawConfigResult {
success: true;
mtime: number;
}
export {
TomlFileConflictError as CodexRawConfigConflictError,
TomlFileValidationError as CodexRawConfigValidationError,
};
const KNOWN_CODEX_FEATURES = new Set([
'apps',
'apply_patch_freeform',
'codex_hooks',
'fast_mode',
'js_repl',
'multi_agent',
'personality',
'prevent_idle_sleep',
'runtime_metrics',
'shell_snapshot',
'shell_tool',
'smart_approvals',
'unified_exec',
'undo',
'web_search',
'web_search_cached',
'web_search_request',
]);
const MODEL_REASONING_EFFORT_VALUES = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
const APPROVAL_POLICY_VALUES = new Set(['on-request', 'never', 'untrusted']);
const SANDBOX_MODE_VALUES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
const WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']);
const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']);
const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'untrusted']);
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function asObject(value: unknown): Record<string, unknown> | null {
return isObject(value) ? value : null;
}
function asString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}
function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function hasOwn(obj: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
const existing = asObject(target[key]);
if (existing) return existing;
const next: Record<string, unknown> = {};
target[key] = next;
return next;
}
function deleteIfEmpty(target: Record<string, unknown>, key: string) {
const value = asObject(target[key]);
if (value && Object.keys(value).length === 0) {
delete target[key];
}
}
function shouldPreserveUnsupportedValue(value: unknown): boolean {
return Array.isArray(value) || isObject(value);
}
function deleteFieldUnlessUnsupported(target: Record<string, unknown>, key: string) {
if (shouldPreserveUnsupportedValue(target[key])) {
return;
}
delete target[key];
}
function setStringField(target: Record<string, unknown>, key: string, value: unknown) {
if (!isNonEmptyString(value)) {
deleteFieldUnlessUnsupported(target, key);
return;
}
target[key] = value.trim();
}
function setEnumStringField(
target: Record<string, unknown>,
key: string,
value: unknown,
allowedValues: Set<string>,
label: string
) {
if (!isNonEmptyString(value)) {
deleteFieldUnlessUnsupported(target, key);
return;
}
const normalized = value.trim();
if (!allowedValues.has(normalized)) {
throw new TomlFileValidationError(
`${label} must be one of: ${Array.from(allowedValues).join(', ')}.`
);
}
target[key] = normalized;
}
function setBooleanField(target: Record<string, unknown>, key: string, value: unknown) {
if (typeof value !== 'boolean') {
deleteFieldUnlessUnsupported(target, key);
return;
}
target[key] = value;
}
function setNumberField(
target: Record<string, unknown>,
key: string,
value: unknown,
options: { integer?: boolean; min?: number } = {}
) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
deleteFieldUnlessUnsupported(target, key);
return;
}
if (options.integer && !Number.isInteger(value)) {
throw new TomlFileValidationError(`${key} must be an integer.`);
}
if (typeof options.min === 'number' && value < options.min) {
throw new TomlFileValidationError(`${key} must be >= ${options.min}.`);
}
target[key] = value;
}
function normalizeStringArray(value: unknown, label: string): string[] | null {
if (value === null || value === undefined) return null;
if (!Array.isArray(value)) {
throw new TomlFileValidationError(`${label} must be an array of strings.`);
}
const normalized = value
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
.filter((entry) => entry.length > 0);
return normalized.length > 0 ? normalized : [];
}
function assertPatchableToml(fileProbe: {
diagnostics: { parseError: string | null; readError: string | null };
config: Record<string, unknown> | null;
}): Record<string, unknown> {
if (fileProbe.diagnostics.readError) {
throw new TomlFileValidationError(fileProbe.diagnostics.readError);
}
if (fileProbe.diagnostics.parseError) {
throw new TomlFileValidationError(
'config.toml contains invalid TOML. Fix the raw file before using guided controls.'
);
}
return asObject(fileProbe.config) ?? {};
}
function summarizeApprovalPolicy(value: unknown): string | null {
const stringValue = asString(value);
if (stringValue) {
return stringValue;
}
const objectValue = asObject(value);
if (!objectValue) {
return null;
}
if (hasOwn(objectValue, 'granular')) {
return 'granular (custom)';
}
return 'custom object';
}
function applyTopLevelSettingsPatch(
target: Record<string, unknown>,
values: Extract<CodexConfigPatchInput, { kind: 'top-level' }>['values']
) {
if (hasOwn(values, 'model')) setStringField(target, 'model', values.model);
if (hasOwn(values, 'modelReasoningEffort')) {
setEnumStringField(
target,
'model_reasoning_effort',
values.modelReasoningEffort,
MODEL_REASONING_EFFORT_VALUES,
'model_reasoning_effort'
);
}
if (hasOwn(values, 'modelProvider')) {
setStringField(target, 'model_provider', values.modelProvider);
}
if (hasOwn(values, 'approvalPolicy')) {
setEnumStringField(
target,
'approval_policy',
values.approvalPolicy,
APPROVAL_POLICY_VALUES,
'approval_policy'
);
}
if (hasOwn(values, 'sandboxMode')) {
setEnumStringField(
target,
'sandbox_mode',
values.sandboxMode,
SANDBOX_MODE_VALUES,
'sandbox_mode'
);
}
if (hasOwn(values, 'webSearch')) {
setEnumStringField(target, 'web_search', values.webSearch, WEB_SEARCH_VALUES, 'web_search');
}
if (hasOwn(values, 'toolOutputTokenLimit')) {
setNumberField(target, 'tool_output_token_limit', values.toolOutputTokenLimit, {
integer: true,
min: 1,
});
}
if (hasOwn(values, 'personality')) {
setEnumStringField(
target,
'personality',
values.personality,
PERSONALITY_VALUES,
'personality'
);
}
}
function applyProjectTrustPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'project-trust' }>
) {
if (!isNonEmptyString(input.path)) {
throw new TomlFileValidationError('Project path is required.');
}
const expandedPath = expandPath(input.path.trim());
if (!path.isAbsolute(expandedPath)) {
throw new TomlFileValidationError('Project path must be absolute or use ~/... expansion.');
}
const canonicalPath = path.resolve(expandedPath);
const projects = ensureObject(target, 'projects');
if (!isNonEmptyString(input.trustLevel)) {
delete projects[canonicalPath];
deleteIfEmpty(target, 'projects');
return;
}
const trustLevel = input.trustLevel.trim();
if (!PROJECT_TRUST_LEVEL_VALUES.has(trustLevel)) {
throw new TomlFileValidationError(
`trust_level must be one of: ${Array.from(PROJECT_TRUST_LEVEL_VALUES).join(', ')}.`
);
}
projects[canonicalPath] = {
...(asObject(projects[canonicalPath]) ?? {}),
trust_level: trustLevel,
};
}
function applyFeaturePatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'feature' }>
) {
const feature = input.feature.trim();
const currentFeatures = asObject(target.features);
if (
!feature ||
(!KNOWN_CODEX_FEATURES.has(feature) && !(currentFeatures && hasOwn(currentFeatures, feature)))
) {
throw new TomlFileValidationError(`Unsupported feature key "${input.feature}".`);
}
if (input.enabled !== null && typeof input.enabled !== 'boolean') {
throw new TomlFileValidationError('Feature enabled must be boolean or null.');
}
const features = ensureObject(target, 'features');
if (input.enabled === null) {
delete features[feature];
} else {
features[feature] = input.enabled;
}
deleteIfEmpty(target, 'features');
}
function applyProfilePatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'profile' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('Profile name is required.');
}
const profileName = input.name.trim();
if (!['set-active', 'upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported profile action.');
}
if (input.action === 'set-active') {
setStringField(target, 'profile', profileName);
return;
}
const profiles = ensureObject(target, 'profiles');
if (input.action === 'delete') {
delete profiles[profileName];
if (asString(target.profile) === profileName) {
delete target.profile;
}
deleteIfEmpty(target, 'profiles');
return;
}
const nextProfile = { ...(asObject(profiles[profileName]) ?? {}) };
applyTopLevelSettingsPatch(nextProfile, input.values ?? {});
if (Object.keys(nextProfile).length === 0) {
throw new TomlFileValidationError('Profile patch must include at least one saved field.');
}
profiles[profileName] = nextProfile;
if (input.setAsActive === true) {
target.profile = profileName;
}
}
function applyModelProviderPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'model-provider' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('Model provider name is required.');
}
const providerName = input.name.trim();
const providers = ensureObject(target, 'model_providers');
if (!['upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported model provider action.');
}
if (input.action === 'delete') {
delete providers[providerName];
if (asString(target.model_provider) === providerName) {
delete target.model_provider;
}
deleteIfEmpty(target, 'model_providers');
return;
}
const values = input.values;
if (!values) {
throw new TomlFileValidationError('Model provider values are required.');
}
const nextProvider = { ...(asObject(providers[providerName]) ?? {}) };
if (hasOwn(values, 'displayName')) setStringField(nextProvider, 'name', values.displayName);
if (hasOwn(values, 'baseUrl')) setStringField(nextProvider, 'base_url', values.baseUrl);
if (hasOwn(values, 'envKey')) setStringField(nextProvider, 'env_key', values.envKey);
if (hasOwn(values, 'wireApi')) {
if (values.wireApi !== null && values.wireApi !== undefined && values.wireApi !== 'responses') {
throw new TomlFileValidationError('wire_api must be "responses" for Codex model providers.');
}
setStringField(nextProvider, 'wire_api', values.wireApi);
}
if (hasOwn(values, 'requiresOpenaiAuth')) {
setBooleanField(nextProvider, 'requires_openai_auth', values.requiresOpenaiAuth);
}
if (hasOwn(values, 'supportsWebsockets')) {
setBooleanField(nextProvider, 'supports_websockets', values.supportsWebsockets);
}
if (Object.keys(nextProvider).length === 0) {
throw new TomlFileValidationError(
'Model provider patch must include at least one saved field.'
);
}
providers[providerName] = nextProvider;
}
function applyMcpServerPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'mcp-server' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('MCP server name is required.');
}
const serverName = input.name.trim();
const servers = ensureObject(target, 'mcp_servers');
if (!['upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported MCP server action.');
}
if (input.action === 'delete') {
delete servers[serverName];
deleteIfEmpty(target, 'mcp_servers');
return;
}
const values = input.values;
if (!values) {
throw new TomlFileValidationError('MCP server values are required.');
}
if (values.transport !== 'stdio' && values.transport !== 'streamable-http') {
throw new TomlFileValidationError('MCP transport must be "stdio" or "streamable-http".');
}
const nextServer = { ...(asObject(servers[serverName]) ?? {}) };
if (values.transport === 'stdio') {
if (!isNonEmptyString(values.command)) {
throw new TomlFileValidationError('Stdio MCP servers require a command.');
}
nextServer.command = values.command.trim();
const nextArgs = normalizeStringArray(values.args, 'args');
if (nextArgs === null) {
delete nextServer.args;
} else {
nextServer.args = nextArgs;
}
delete nextServer.url;
} else {
if (!isNonEmptyString(values.url)) {
throw new TomlFileValidationError('HTTP MCP servers require a URL.');
}
nextServer.url = values.url.trim();
delete nextServer.command;
delete nextServer.args;
}
if (hasOwn(values, 'enabled')) setBooleanField(nextServer, 'enabled', values.enabled);
if (hasOwn(values, 'required')) setBooleanField(nextServer, 'required', values.required);
if (hasOwn(values, 'startupTimeoutSec')) {
delete nextServer.startup_timeout_ms;
setNumberField(nextServer, 'startup_timeout_sec', values.startupTimeoutSec, { min: 1 });
}
if (hasOwn(values, 'toolTimeoutSec')) {
setNumberField(nextServer, 'tool_timeout_sec', values.toolTimeoutSec, { min: 1 });
}
if (hasOwn(values, 'enabledTools')) {
const nextEnabledTools = normalizeStringArray(values.enabledTools, 'enabledTools');
if (nextEnabledTools === null) {
delete nextServer.enabled_tools;
} else {
nextServer.enabled_tools = nextEnabledTools;
}
}
if (hasOwn(values, 'disabledTools')) {
const nextDisabledTools = normalizeStringArray(values.disabledTools, 'disabledTools');
if (nextDisabledTools === null) {
delete nextServer.disabled_tools;
} else {
nextServer.disabled_tools = nextDisabledTools;
}
}
servers[serverName] = nextServer;
}
function parseTransport(server: Record<string, unknown>): CodexMcpServerDiagnostics['transport'] {
if (asString(server.command)) return 'stdio';
if (asString(server.url)) return 'streamable-http';
return 'unknown';
}
export function resolveCodexConfigPaths(
options: {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
homeDir?: string;
} = {}
): CodexConfigPaths {
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? os.homedir();
const baseDir = path.resolve(
env.CODEX_HOME ? expandPath(env.CODEX_HOME) : path.join(homeDir, '.codex')
);
const baseDirDisplay = env.CODEX_HOME ? '$CODEX_HOME' : '~/.codex';
return {
baseDir,
baseDirDisplay,
configPath: path.join(baseDir, 'config.toml'),
configDisplayPath: `${baseDirDisplay}/config.toml`,
};
}
export function summarizeCodexModelProviders(value: unknown): CodexModelProviderDiagnostics[] {
const providers = asObject(value);
if (!providers) return [];
return Object.entries(providers)
.map(([name, providerValue]) => {
const provider = asObject(providerValue);
if (!provider) return null;
return {
name,
baseUrl: asString(provider.base_url),
envKey: asString(provider.env_key),
wireApi: asString(provider.wire_api),
requiresOpenaiAuth: provider.requires_openai_auth === true,
supportsWebsockets: provider.supports_websockets === true,
hasQueryParams:
isObject(provider.query_params) && Object.keys(provider.query_params).length > 0,
hasHttpHeaders:
(isObject(provider.http_headers) && Object.keys(provider.http_headers).length > 0) ||
(isObject(provider.env_http_headers) &&
Object.keys(provider.env_http_headers).length > 0),
usesExperimentalBearerToken: asString(provider.experimental_bearer_token) !== null,
} satisfies CodexModelProviderDiagnostics;
})
.filter((provider): provider is CodexModelProviderDiagnostics => provider !== null)
.sort((left, right) => left.name.localeCompare(right.name));
}
export function summarizeCodexFeatureFlags(value: unknown): {
all: CodexFeatureFlagDiagnostics[];
enabled: CodexFeatureFlagDiagnostics[];
disabled: CodexFeatureFlagDiagnostics[];
} {
const features = asObject(value);
if (!features) {
return { all: [], enabled: [], disabled: [] };
}
const all = Object.entries(features)
.map(([name, rawValue]) => {
const state = rawValue === true ? 'enabled' : rawValue === false ? 'disabled' : 'custom';
return { name, state } satisfies CodexFeatureFlagDiagnostics;
})
.sort((left, right) => left.name.localeCompare(right.name));
return {
all,
enabled: all.filter((feature) => feature.state === 'enabled'),
disabled: all.filter((feature) => feature.state === 'disabled'),
};
}
export function summarizeCodexProjectTrust(value: unknown): CodexProjectTrustDiagnostics[] {
const projects = asObject(value);
if (!projects) return [];
return Object.entries(projects)
.map(([projectPath, projectValue]) => {
const project = asObject(projectValue);
const trustLevel = project ? asString(project.trust_level) : null;
if (!trustLevel) return null;
return { path: projectPath, trustLevel } satisfies CodexProjectTrustDiagnostics;
})
.filter((project): project is CodexProjectTrustDiagnostics => project !== null)
.sort((left, right) => left.path.localeCompare(right.path));
}
export function summarizeCodexMcpServers(value: unknown): CodexMcpServerDiagnostics[] {
const servers = asObject(value);
if (!servers) return [];
return Object.entries(servers)
.map(([name, serverValue]) => {
const server = asObject(serverValue);
if (!server) return null;
const startupTimeoutMs = asNumber(server.startup_timeout_ms);
const startupTimeoutSec =
asNumber(server.startup_timeout_sec) ??
(startupTimeoutMs !== null ? startupTimeoutMs / 1000 : null);
return {
name,
transport: parseTransport(server),
enabled: server.enabled !== false,
required: server.required === true,
startupTimeoutSec,
toolTimeoutSec: asNumber(server.tool_timeout_sec),
enabledToolsCount: Array.isArray(server.enabled_tools) ? server.enabled_tools.length : 0,
disabledToolsCount: Array.isArray(server.disabled_tools) ? server.disabled_tools.length : 0,
usesInlineBearerToken: hasOwn(server, 'bearer_token'),
} satisfies CodexMcpServerDiagnostics;
})
.filter((server): server is CodexMcpServerDiagnostics => server !== null)
.sort((left, right) => left.name.localeCompare(right.name));
}
function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
return [
{
id: 'default',
label: 'default',
supported: true,
notes: 'Uses the local Codex CLI with existing native auth and config.',
},
{
id: 'cliproxy-provider-codex',
label: 'cliproxy provider=codex',
supported: true,
notes: 'Routed through the CLIProxy Codex Responses bridge.',
},
{
id: 'settings-with-bridge',
label: 'settings with bridge metadata',
supported: true,
notes: 'Supported when the resolved API profile points at a Codex CLIProxy bridge.',
},
{
id: 'cliproxy-composite',
label: 'cliproxy composite',
supported: false,
notes: 'Not yet proven safe for native Codex routing in CCS v1.',
},
{
id: 'settings-generic-api',
label: 'settings generic API profile',
supported: false,
notes: 'Generic API profiles stay on Claude or Droid targets.',
},
{
id: 'account',
label: 'account',
supported: false,
notes: 'Account isolation remains a Claude-oriented concept.',
},
{
id: 'copilot',
label: 'copilot',
supported: false,
notes: 'GitHub Copilot flow is not a native Codex target path.',
},
];
}
export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiagnostics> {
const paths = resolveCodexConfigPaths();
const binaryInfo = getCodexBinaryInfo();
const docsReference = getCompatibleCliDocsReference('codex');
const fileProbe = await probeTomlObjectFile(
paths.configPath,
'Codex user config',
paths.configDisplayPath
);
const config = asObject(fileProbe.config);
const topLevelKeys = config
? Object.keys(config).sort((left, right) => left.localeCompare(right))
: [];
const activeProfile = asString(config?.profile);
const profileNames = Object.keys(asObject(config?.profiles) ?? {}).sort((left, right) =>
left.localeCompare(right)
);
const modelProviders = summarizeCodexModelProviders(config?.model_providers);
const features = summarizeCodexFeatureFlags(config?.features);
const projectTrust = summarizeCodexProjectTrust(config?.projects);
const mcpServers = summarizeCodexMcpServers(config?.mcp_servers);
const supportMatrix = getCodexSupportMatrix();
const warnings: string[] = [];
if (!binaryInfo) {
warnings.push('Codex binary is not detected in PATH or CCS_CODEX_PATH.');
} else if (!codexBinarySupportsConfigOverrides(binaryInfo)) {
warnings.push(
'This Codex build does not expose --config overrides required for CCS-backed Codex routing.'
);
}
if (fileProbe.diagnostics.parseError) {
warnings.push(`${paths.configDisplayPath} contains invalid TOML.`);
}
if (activeProfile && !profileNames.includes(activeProfile)) {
warnings.push(`Active profile "${activeProfile}" is selected but missing from [profiles].`);
}
if (modelProviders.some((provider) => provider.usesExperimentalBearerToken)) {
warnings.push(
'One or more model_providers entries use experimental_bearer_token; prefer env_key-backed auth.'
);
}
if (mcpServers.some((server) => server.usesInlineBearerToken)) {
warnings.push(
'One or more mcp_servers entries include inline bearer_token; prefer bearer_token_env_var.'
);
}
return {
binary: {
installed: !!binaryInfo,
path: binaryInfo?.path ?? null,
installDir: binaryInfo?.path ? path.dirname(binaryInfo.path) : null,
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),
},
file: fileProbe.diagnostics,
workspacePath: process.cwd(),
config: {
model: asString(config?.model),
modelReasoningEffort: asString(config?.model_reasoning_effort),
modelProvider: asString(config?.model_provider),
activeProfile,
approvalPolicy: summarizeApprovalPolicy(config?.approval_policy),
sandboxMode: asString(config?.sandbox_mode),
webSearch: asString(config?.web_search),
toolOutputTokenLimit: asNumber(config?.tool_output_token_limit),
personality: asString(config?.personality),
topLevelKeys,
profileCount: profileNames.length,
profileNames,
modelProviderCount: modelProviders.length,
modelProviders,
featureCount: features.all.length,
enabledFeatures: features.enabled,
disabledFeatures: features.disabled,
trustedProjectCount: projectTrust.filter((entry) => entry.trustLevel === 'trusted').length,
untrustedProjectCount: projectTrust.filter((entry) => entry.trustLevel !== 'trusted').length,
projectTrust,
mcpServerCount: mcpServers.length,
mcpServers,
},
supportMatrix,
warnings,
docsReference,
};
}
export async function getCodexRawConfig(): Promise<CodexRawConfigResponse> {
const paths = resolveCodexConfigPaths();
const fileProbe = await probeTomlObjectFile(
paths.configPath,
'Codex user config',
paths.configDisplayPath
);
return {
path: paths.configDisplayPath,
resolvedPath: paths.configPath,
exists: fileProbe.diagnostics.exists,
mtime: fileProbe.diagnostics.mtimeMs ?? Date.now(),
rawText: fileProbe.rawText,
config: fileProbe.config,
parseError: fileProbe.diagnostics.parseError,
readError: fileProbe.diagnostics.readError,
};
}
export async function saveCodexRawConfig(
input: SaveCodexRawConfigInput
): Promise<SaveCodexRawConfigResult> {
const paths = resolveCodexConfigPaths();
if (typeof input.rawText !== 'string') {
throw new TomlFileValidationError('rawText must be a string.');
}
const saved = await writeTomlFileAtomic({
filePath: paths.configPath,
rawText: input.rawText,
expectedMtime: input.expectedMtime,
fileLabel: 'config.toml',
});
return { success: true, mtime: saved.mtime };
}
export async function patchCodexConfig(
input: CodexConfigPatchInput
): Promise<CodexConfigPatchResult> {
const paths = resolveCodexConfigPaths();
const fileProbe = await probeTomlObjectFile(
paths.configPath,
'Codex user config',
paths.configDisplayPath
);
const nextConfig = { ...assertPatchableToml(fileProbe) };
switch (input.kind) {
case 'top-level':
applyTopLevelSettingsPatch(nextConfig, input.values);
break;
case 'project-trust':
applyProjectTrustPatch(nextConfig, input);
break;
case 'feature':
applyFeaturePatch(nextConfig, input);
break;
case 'profile':
applyProfilePatch(nextConfig, input);
break;
case 'model-provider':
applyModelProviderPatch(nextConfig, input);
break;
case 'mcp-server':
applyMcpServerPatch(nextConfig, input);
break;
default:
throw new TomlFileValidationError('Unsupported Codex config patch.');
}
const rawText = stringifyTomlObject(nextConfig);
const saved = await writeTomlFileAtomic({
filePath: paths.configPath,
rawText,
expectedMtime: input.expectedMtime ?? fileProbe.diagnostics.mtimeMs ?? undefined,
fileLabel: 'config.toml',
});
return {
success: true,
path: paths.configDisplayPath,
resolvedPath: paths.configPath,
exists: true,
mtime: saved.mtime,
rawText,
config: nextConfig,
parseError: null,
readError: null,
};
}
@@ -3,7 +3,7 @@ export interface CompatibleCliDocLink {
label: string;
url: string;
category: 'overview' | 'configuration' | 'byok' | 'reference';
source: 'factory' | 'provider';
source: 'factory' | 'provider' | 'openai' | 'github';
description: string;
}
@@ -96,6 +96,72 @@ const COMPATIBLE_CLI_DOCS_REGISTRY: Record<string, CompatibleCliDocsRegistryEntr
],
},
},
codex: {
cliId: 'codex',
displayName: 'Codex CLI',
docsReference: {
providerValues: ['openai', 'oss', 'custom model_providers'],
settingsHierarchy: [
'system managed config',
'user config ($CODEX_HOME/config.toml)',
'cwd config',
'tree/repo config',
'CLI -c overrides and environment variables',
],
notes: [
'User config lives at ~/.codex/config.toml unless CODEX_HOME overrides the base directory',
'Codex merges multiple config layers; this dashboard edits only the user layer',
'CLI --profile selects a named [profiles.<name>] overlay on top of base config',
'CCS-backed Codex launches may apply transient -c overrides and CCS_CODEX_API_KEY',
'Official docs treat model_providers, mcp_servers, features, and project trust as schema-backed config surfaces',
],
links: [
{
id: 'codex-config-basic',
label: 'Codex Config Basics',
url: 'https://developers.openai.com/codex/config-basic',
category: 'overview',
source: 'openai',
description:
'Official user-layer setup, config location, and basic configuration guidance.',
},
{
id: 'codex-config-advanced',
label: 'Codex Config Advanced',
url: 'https://developers.openai.com/codex/config-advanced',
category: 'configuration',
source: 'openai',
description: 'Advanced layering, project trust, profiles, and stricter config behaviors.',
},
{
id: 'codex-config-reference',
label: 'Codex Config Reference',
url: 'https://developers.openai.com/codex/config-reference',
category: 'reference',
source: 'openai',
description:
'Canonical upstream config schema surface for model providers, features, MCP, and more.',
},
{
id: 'codex-releases',
label: 'Codex GitHub Releases',
url: 'https://github.com/openai/codex/releases',
category: 'reference',
source: 'github',
description:
'Track CLI release notes and upstream behavior changes across stable and prerelease builds.',
},
],
providerDocs: [
{
provider: 'openai',
label: 'OpenAI Responses API',
apiFormat: 'Responses API',
url: 'https://platform.openai.com/docs/api-reference/responses',
},
],
},
},
};
export function getCompatibleCliDocsReference(cliId: string): CompatibleCliDocsReference {
@@ -0,0 +1,323 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { stringify } from 'smol-toml';
import { parseTomlObject } from '../../shared/toml-object';
export interface TomlFileDiagnostics {
label: string;
path: string;
resolvedPath: string;
exists: boolean;
isSymlink: boolean;
isRegularFile: boolean;
sizeBytes: number | null;
mtimeMs: number | null;
parseError: string | null;
readError: string | null;
}
export interface TomlFileProbe {
diagnostics: TomlFileDiagnostics;
config: Record<string, unknown> | null;
rawText: string;
}
interface WriteTomlFileInput {
filePath: string;
rawText: string;
expectedMtime?: number;
fileLabel?: string;
dirMode?: number;
fileMode?: number;
}
interface WriteTomlFileResult {
mtime: number;
}
export class TomlFileValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'TomlFileValidationError';
}
}
export class TomlFileConflictError extends Error {
readonly code = 'CONFLICT';
readonly mtime: number;
constructor(message: string, mtime: number) {
super(message);
this.name = 'TomlFileConflictError';
this.mtime = mtime;
}
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
async function statPath(filePath: string): Promise<import('fs').Stats | null> {
try {
return await fs.lstat(filePath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw error;
}
}
async function resolveConflictMtime(filePath: string): Promise<number> {
const stat = await statPath(filePath);
return stat?.mtimeMs ?? Date.now();
}
async function acquireWriteLock(
lockPath: string,
targetPath: string,
fileLabel: string
): Promise<() => Promise<void>> {
let handle: Awaited<ReturnType<typeof fs.open>> | null = null;
try {
handle = await fs.open(lockPath, 'wx', 0o600);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'EEXIST') {
const existingLock = await statPath(lockPath);
if (existingLock?.isSymbolicLink()) {
throw new Error(`Refusing to write: ${fileLabel}.lock is a symlink.`);
}
if (existingLock && !existingLock.isFile()) {
throw new Error(`Refusing to write: ${fileLabel}.lock is not a regular file.`);
}
throw new TomlFileConflictError(
'File is currently being written by another request. Refresh and retry.',
await resolveConflictMtime(targetPath)
);
}
throw error;
}
return async () => {
if (!handle) return;
try {
await handle.close();
} finally {
try {
await fs.unlink(lockPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
}
};
}
function ensureWritableTarget(
targetStat: import('fs').Stats | null,
fileLabel: string
): import('fs').Stats | null {
if (!targetStat) return null;
if (targetStat.isSymbolicLink()) {
throw new Error(`Refusing to write: ${fileLabel} is a symlink.`);
}
if (!targetStat.isFile()) {
throw new Error(`Refusing to write: ${fileLabel} is not a regular file.`);
}
return targetStat;
}
function assertExpectedMtime(
targetStat: import('fs').Stats | null,
expectedMtime: number | undefined
): void {
if (!targetStat) {
if (expectedMtime !== undefined) {
throw new TomlFileConflictError('File modified externally.', Date.now());
}
return;
}
if (typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime)) {
throw new TomlFileConflictError(
'File metadata not loaded. Refresh and retry.',
targetStat.mtimeMs
);
}
if (targetStat.mtimeMs !== expectedMtime) {
throw new TomlFileConflictError('File modified externally.', targetStat.mtimeMs);
}
}
async function verifyTargetUnchanged(
targetPath: string,
initialTargetStat: import('fs').Stats | null,
fileLabel: string
): Promise<void> {
const currentTargetStat = ensureWritableTarget(await statPath(targetPath), fileLabel);
if (!initialTargetStat) {
if (currentTargetStat) {
throw new TomlFileConflictError('File modified externally.', currentTargetStat.mtimeMs);
}
return;
}
if (!currentTargetStat || currentTargetStat.mtimeMs !== initialTargetStat.mtimeMs) {
throw new TomlFileConflictError(
'File modified externally.',
currentTargetStat?.mtimeMs ?? Date.now()
);
}
}
export function parseTomlObjectText(
rawText: string,
fieldName = 'rawText'
): Record<string, unknown> {
if (typeof rawText !== 'string') {
throw new TomlFileValidationError(`${fieldName} must be a string.`);
}
try {
return parseTomlObject(rawText);
} catch (error) {
const message = (error as Error).message;
if (message === 'TOML root must be a table.') {
throw new TomlFileValidationError(`${fieldName} TOML root must be a table.`);
}
throw new TomlFileValidationError(`Invalid TOML in ${fieldName}: ${message}`);
}
}
export function stringifyTomlObject(config: Record<string, unknown>): string {
if (!isObject(config)) {
throw new TomlFileValidationError('config TOML root must be a table.');
}
const text = stringify(config).trimEnd();
return text ? `${text}\n` : '';
}
export async function probeTomlObjectFile(
filePath: string,
label: string,
displayPath: string
): Promise<TomlFileProbe> {
const stat = await statPath(filePath);
if (!stat) {
return {
diagnostics: {
label,
path: displayPath,
resolvedPath: filePath,
exists: false,
isSymlink: false,
isRegularFile: false,
sizeBytes: null,
mtimeMs: null,
parseError: null,
readError: null,
},
config: null,
rawText: '',
};
}
const diagnostics: TomlFileDiagnostics = {
label,
path: displayPath,
resolvedPath: filePath,
exists: true,
isSymlink: stat.isSymbolicLink(),
isRegularFile: stat.isFile(),
sizeBytes: stat.size,
mtimeMs: stat.mtimeMs,
parseError: null,
readError: null,
};
if (diagnostics.isSymlink) {
diagnostics.readError = 'Refusing symlink file for safety.';
return { diagnostics, config: null, rawText: '' };
}
if (!diagnostics.isRegularFile) {
diagnostics.readError = 'Target is not a regular file.';
return { diagnostics, config: null, rawText: '' };
}
try {
const rawText = await fs.readFile(filePath, 'utf8');
try {
const config = parseTomlObjectText(rawText, displayPath);
return { diagnostics, config, rawText };
} catch (error) {
diagnostics.parseError = (error as Error).message;
return { diagnostics, config: null, rawText };
}
} catch (error) {
diagnostics.readError = (error as Error).message;
return { diagnostics, config: null, rawText: '' };
}
}
export async function writeTomlFileAtomic(input: WriteTomlFileInput): Promise<WriteTomlFileResult> {
const fileLabel = input.fileLabel || path.basename(input.filePath);
parseTomlObjectText(input.rawText, fileLabel);
const targetPath = input.filePath;
const targetDir = path.dirname(targetPath);
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
const lockPath = `${targetPath}.lock`;
const dirMode = input.dirMode ?? 0o700;
const fileMode = input.fileMode ?? 0o600;
await fs.mkdir(targetDir, { recursive: true, mode: dirMode });
const releaseLock = await acquireWriteLock(lockPath, targetPath, fileLabel);
let wroteTemp = false;
try {
const targetStat = ensureWritableTarget(await statPath(targetPath), fileLabel);
assertExpectedMtime(targetStat, input.expectedMtime);
await fs.writeFile(tempPath, input.rawText, { mode: fileMode, flag: 'wx' });
wroteTemp = true;
const tempStat = await fs.lstat(tempPath);
if (tempStat.isSymbolicLink()) {
throw new Error(`Refusing to write: ${fileLabel}.tmp is a symlink.`);
}
if (!tempStat.isFile()) {
throw new Error(`Refusing to write: ${fileLabel}.tmp is not a regular file.`);
}
await verifyTargetUnchanged(targetPath, targetStat, fileLabel);
await fs.rename(tempPath, targetPath);
wroteTemp = false;
try {
await fs.chmod(targetPath, fileMode);
} catch {
// Best-effort permission hardening.
}
const stat = await fs.stat(targetPath);
return { mtime: stat.mtimeMs };
} finally {
if (wroteTemp) {
try {
await fs.unlink(tempPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
}
await releaseLock();
}
}
+25 -24
View File
@@ -1,3 +1,28 @@
import type { CompatibleCliDocsReference } from '../../shared/compatible-cli-contracts';
export type {
CompatibleCliDocLink,
CompatibleCliDocsReference,
CompatibleCliProviderDocLink,
CodexBinaryDiagnostics,
CodexBinarySource,
CodexConfigFileDiagnostics,
CodexConfigPatchInput,
CodexConfigPatchResult,
CodexDashboardDiagnostics,
CodexFeatureFlagDiagnostics,
CodexMcpServerDiagnostics,
CodexMcpServerPatchValues,
CodexModelProviderDiagnostics,
CodexModelProviderPatchValues,
CodexProfilePatchValues,
CodexProjectTrustDiagnostics,
CodexRawConfigResponse,
CodexSupportMatrixEntry,
CodexTopLevelSettingsPatch,
CodexUserConfigDiagnostics,
} from '../../shared/compatible-cli-contracts';
export type DroidBinarySource = 'CCS_DROID_PATH' | 'PATH' | 'missing';
export interface DroidBinaryDiagnostics {
@@ -44,30 +69,6 @@ export interface DroidByokDiagnostics {
customModels: DroidCustomModelDiagnostics[];
}
export interface CompatibleCliDocLink {
id: string;
label: string;
url: string;
category: 'overview' | 'configuration' | 'byok' | 'reference';
source: 'factory' | 'provider';
description: string;
}
export interface CompatibleCliProviderDocLink {
provider: string;
label: string;
apiFormat: string;
url: string;
}
export interface CompatibleCliDocsReference {
providerValues: string[];
settingsHierarchy: string[];
notes: string[];
links: CompatibleCliDocLink[];
providerDocs: CompatibleCliProviderDocLink[];
}
export interface DroidDashboardDiagnostics {
binary: DroidBinaryDiagnostics;
files: {