From ebc9acf8e41fccc9ae968d40be75f1f7e4382929 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 29 Mar 2026 11:32:47 -0400 Subject: [PATCH] fix(codex): harden dashboard config editing --- src/shared/compatible-cli-contracts.ts | 214 ++++++ src/shared/toml-object.ts | 36 + .../services/codex-dashboard-service.ts | 3 +- .../compatible-cli-toml-file-service.ts | 165 +++-- .../services/compatible-cli-types.ts | 229 +----- .../codex-dashboard-service.test.ts | 66 +- .../codex-control-center-tab.tsx | 165 +++++ .../compatible-cli/codex-docs-tab.tsx | 184 +++++ .../compatible-cli/codex-overview-tab.tsx | 315 ++++++++ ui/src/hooks/use-codex-types.ts | 234 +----- ui/src/hooks/use-codex.ts | 28 +- ui/src/pages/codex.tsx | 685 +----------------- 12 files changed, 1182 insertions(+), 1142 deletions(-) create mode 100644 src/shared/compatible-cli-contracts.ts create mode 100644 src/shared/toml-object.ts create mode 100644 ui/src/components/compatible-cli/codex-control-center-tab.tsx create mode 100644 ui/src/components/compatible-cli/codex-docs-tab.tsx create mode 100644 ui/src/components/compatible-cli/codex-overview-tab.tsx diff --git a/src/shared/compatible-cli-contracts.ts b/src/shared/compatible-cli-contracts.ts new file mode 100644 index 00000000..8d9583c5 --- /dev/null +++ b/src/shared/compatible-cli-contracts.ts @@ -0,0 +1,214 @@ +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 | null; + parseError: 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; +} diff --git a/src/shared/toml-object.ts b/src/shared/toml-object.ts new file mode 100644 index 00000000..0e393db2 --- /dev/null +++ b/src/shared/toml-object.ts @@ -0,0 +1,36 @@ +import { parse } from 'smol-toml'; + +export interface SafeTomlObjectParseResult { + config: Record | null; + parseError: string | null; +} + +function isTomlObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function parseTomlObject(rawText: string): Record { + 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, + }; + } +} diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index ca98b630..3ebabb1f 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -134,8 +134,7 @@ function setEnumStringField( } const normalized = value.trim(); - const currentValue = asString(target[key]); - if (!allowedValues.has(normalized) && normalized !== currentValue) { + if (!allowedValues.has(normalized)) { throw new TomlFileValidationError( `${label} must be one of: ${Array.from(allowedValues).join(', ')}.` ); diff --git a/src/web-server/services/compatible-cli-toml-file-service.ts b/src/web-server/services/compatible-cli-toml-file-service.ts index 312a04c3..95613852 100644 --- a/src/web-server/services/compatible-cli-toml-file-service.ts +++ b/src/web-server/services/compatible-cli-toml-file-service.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { parse, stringify } from 'smol-toml'; +import { stringify } from 'smol-toml'; +import { parseTomlObject } from '../../shared/toml-object'; export interface TomlFileDiagnostics { label: string; @@ -67,6 +68,111 @@ async function statPath(filePath: string): Promise { } } +async function resolveConflictMtime(filePath: string): Promise { + const stat = await statPath(filePath); + return stat?.mtimeMs ?? Date.now(); +} + +async function acquireWriteLock( + lockPath: string, + targetPath: string, + fileLabel: string +): Promise<() => Promise> { + let handle: Awaited> | 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 { + 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' @@ -75,21 +181,15 @@ export function parseTomlObjectText( throw new TomlFileValidationError(`${fieldName} must be a string.`); } - const trimmed = rawText.trim(); - if (!trimmed) return {}; - - let parsed: unknown; try { - parsed = parse(rawText); + return parseTomlObject(rawText); } catch (error) { - throw new TomlFileValidationError(`Invalid TOML in ${fieldName}: ${(error as Error).message}`); + 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}`); } - - if (!isObject(parsed)) { - throw new TomlFileValidationError(`${fieldName} TOML root must be a table.`); - } - - return parsed; } export function stringifyTomlObject(config: Record): string { @@ -170,45 +270,21 @@ export async function writeTomlFileAtomic(input: WriteTomlFileInput): Promise 1000) { - throw new TomlFileConflictError('File modified externally.', targetStat.mtimeMs); - } - } + const releaseLock = await acquireWriteLock(lockPath, targetPath, fileLabel); let wroteTemp = false; try { - const existingTempStat = await statPath(tempPath); - if (existingTempStat) { - if (existingTempStat.isSymbolicLink()) { - throw new Error(`Refusing to write: ${fileLabel}.tmp is a symlink.`); - } - if (!existingTempStat.isFile()) { - throw new Error(`Refusing to write: ${fileLabel}.tmp is not a regular file.`); - } - } + const targetStat = ensureWritableTarget(await statPath(targetPath), fileLabel); + assertExpectedMtime(targetStat, input.expectedMtime); - await fs.writeFile(tempPath, input.rawText, { mode: fileMode }); + await fs.writeFile(tempPath, input.rawText, { mode: fileMode, flag: 'wx' }); wroteTemp = true; const tempStat = await fs.lstat(tempPath); @@ -219,6 +295,7 @@ export async function writeTomlFileAtomic(input: WriteTomlFileInput): Promise | null; parseError: string | null; } - -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 type CodexConfigFileDiagnostics = DroidConfigFileDiagnostics; - -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 | null; - parseError: 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; -} diff --git a/tests/unit/web-server/codex-dashboard-service.test.ts b/tests/unit/web-server/codex-dashboard-service.test.ts index 77e837c8..ac09c04b 100644 --- a/tests/unit/web-server/codex-dashboard-service.test.ts +++ b/tests/unit/web-server/codex-dashboard-service.test.ts @@ -21,7 +21,8 @@ const codexHome = path.join(testRoot, '.codex-home'); const codexStubPath = path.join(testRoot, 'codex'); function writeCodexStub(options?: { helpText?: string; version?: string }) { - const helpText = options?.helpText ?? ' -c, --config \n -p, --profile \n'; + const helpText = + options?.helpText ?? ' -c, --config \n -p, --profile \n'; const version = options?.version ?? 'codex-cli 0.118.0-alpha.3'; fs.writeFileSync( @@ -264,6 +265,20 @@ bearer_token = "secret" ).rejects.toThrow(CodexRawConfigConflictError); }); + it('rejects writes when expectedMtime differs by even 1ms', async () => { + const configPath = path.join(codexHome, 'config.toml'); + fs.writeFileSync(configPath, 'model = "gpt-5.4"\n'); + + const current = await getCodexRawConfig(); + + await expect( + saveCodexRawConfig({ + rawText: 'model = "gpt-5.4"\nprofile = "work"\n', + expectedMtime: current.mtime + 1, + }) + ).rejects.toThrow(CodexRawConfigConflictError); + }); + it('patches top-level settings and project trust through structured controls', async () => { const result = await patchCodexConfig({ kind: 'top-level', @@ -369,6 +384,39 @@ bearer_token = "secret" expect(profileResult.config?.profile).toBe('deep-review'); }); + it('patches streamable-http mcp servers through structured controls', async () => { + const result = await patchCodexConfig({ + kind: 'mcp-server', + action: 'upsert', + name: 'remote', + values: { + transport: 'streamable-http', + url: 'https://example.test/mcp', + enabled: true, + required: true, + toolTimeoutSec: 45, + enabledTools: ['browser_snapshot'], + disabledTools: ['slow_tool'], + }, + }); + + expect(result.rawText).toContain('[mcp_servers.remote]'); + expect(result.rawText).toContain('url = "https://example.test/mcp"'); + expect(result.rawText).toContain('required = true'); + + const diagnostics = await getCodexDashboardDiagnostics(); + expect(diagnostics.config.mcpServers).toEqual([ + expect.objectContaining({ + name: 'remote', + transport: 'streamable-http', + required: true, + toolTimeoutSec: 45, + enabledToolsCount: 1, + disabledToolsCount: 1, + }), + ]); + }); + it('rejects structured patches when config.toml is invalid', async () => { fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n[features\n'); @@ -431,4 +479,20 @@ bearer_token = "secret" }) ).rejects.toThrow(CodexRawConfigValidationError); }); + + it('rejects invalid enum values even when they already exist in config.toml', async () => { + fs.writeFileSync( + path.join(codexHome, 'config.toml'), + 'model = "gpt-5.4"\napproval_policy = "legacy"\n' + ); + + await expect( + patchCodexConfig({ + kind: 'top-level', + values: { + approvalPolicy: 'legacy' as unknown as 'on-request' | 'never' | 'untrusted' | null, + }, + }) + ).rejects.toThrow(CodexRawConfigValidationError); + }); }); diff --git a/ui/src/components/compatible-cli/codex-control-center-tab.tsx b/ui/src/components/compatible-cli/codex-control-center-tab.tsx new file mode 100644 index 00000000..9b701521 --- /dev/null +++ b/ui/src/components/compatible-cli/codex-control-center-tab.tsx @@ -0,0 +1,165 @@ +import { Route } from 'lucide-react'; +import { CodexFeaturesCard } from '@/components/compatible-cli/codex-features-card'; +import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card'; +import { CodexModelProvidersCard } from '@/components/compatible-cli/codex-model-providers-card'; +import { CodexProfilesCard } from '@/components/compatible-cli/codex-profiles-card'; +import { CodexProjectTrustCard } from '@/components/compatible-cli/codex-project-trust-card'; +import { CodexTopLevelControlsCard } from '@/components/compatible-cli/codex-top-level-controls-card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import type { + CodexConfigPatchInput, + CodexProfilePatchValues, + CodexTopLevelSettingsPatch, +} from '@/hooks/use-codex-types'; +import type { + CodexFeatureCatalogEntry, + CodexMcpServerEntry, + CodexModelProviderEntry, + CodexProfileEntry, + CodexProjectTrustEntry, + CodexTopLevelSettingsView, +} from '@/lib/codex-config'; + +interface CodexControlCenterTabProps { + workspacePath: string; + activeProfile: string | null; + topLevelSettings: CodexTopLevelSettingsView; + projectTrustEntries: CodexProjectTrustEntry[]; + profileEntries: CodexProfileEntry[]; + modelProviderEntries: CodexModelProviderEntry[]; + mcpServerEntries: CodexMcpServerEntry[]; + featureCatalog: CodexFeatureCatalogEntry[]; + featureState: Record; + disabled: boolean; + disabledReason: string | null; + saving: boolean; + onPatch: (patch: CodexConfigPatchInput, successMessage: string) => Promise; +} + +export function CodexControlCenterTab({ + workspacePath, + activeProfile, + topLevelSettings, + projectTrustEntries, + profileEntries, + modelProviderEntries, + mcpServerEntries, + featureCatalog, + featureState, + disabled, + disabledReason, + saving, + onPatch, +}: CodexControlCenterTabProps) { + return ( + +
+ + + + + Structured controls boundary + + + +

+ Guided controls write only the user-layer config.toml. They do not model + the full effective Codex runtime once trusted repo layers and CCS transient{' '} + -c overrides are involved. +

+

+ Structured saves normalize TOML formatting and strip comments. Use the raw editor on + the right when exact layout matters. +

+
+
+ + entry.name)} + disabled={disabled} + disabledReason={disabledReason} + saving={saving} + onSave={(values: CodexTopLevelSettingsPatch) => + onPatch({ kind: 'top-level', values }, 'Saved top-level Codex settings.') + } + /> + + + onPatch( + { kind: 'project-trust', path: projectPath, trustLevel }, + trustLevel ? 'Saved project trust entry.' : 'Removed project trust entry.' + ) + } + /> + + entry.name)} + disabled={disabled} + disabledReason={disabledReason} + saving={saving} + onSave={(name, values: CodexProfilePatchValues, setAsActive) => + onPatch( + { kind: 'profile', action: 'upsert', name, values, setAsActive }, + 'Saved profile.' + ) + } + onDelete={(name) => + onPatch({ kind: 'profile', action: 'delete', name }, 'Deleted profile.') + } + onSetActive={(name) => + onPatch({ kind: 'profile', action: 'set-active', name }, 'Set active profile.') + } + /> + + + onPatch( + { kind: 'model-provider', action: 'upsert', name, values }, + 'Saved model provider.' + ) + } + onDelete={(name) => + onPatch({ kind: 'model-provider', action: 'delete', name }, 'Deleted model provider.') + } + /> + + + onPatch({ kind: 'mcp-server', action: 'upsert', name, values }, 'Saved MCP server.') + } + onDelete={(name) => + onPatch({ kind: 'mcp-server', action: 'delete', name }, 'Deleted MCP server.') + } + /> + + + onPatch({ kind: 'feature', feature, enabled }, 'Saved feature toggle.') + } + /> +
+
+ ); +} diff --git a/ui/src/components/compatible-cli/codex-docs-tab.tsx b/ui/src/components/compatible-cli/codex-docs-tab.tsx new file mode 100644 index 00000000..facb4277 --- /dev/null +++ b/ui/src/components/compatible-cli/codex-docs-tab.tsx @@ -0,0 +1,184 @@ +import { type ReactNode } from 'react'; +import { ExternalLink, ShieldCheck } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import type { + CompatibleCliProviderDocLink, + CodexDashboardDiagnostics, +} from '@/hooks/use-codex-types'; + +const DEFAULT_CODEX_DOC_LINKS = [ + { + id: 'codex-config-basic', + label: 'Codex Config Basics', + url: 'https://developers.openai.com/codex/config-basic', + description: 'Official user-layer setup, config location, and baseline configuration behavior.', + }, + { + id: 'codex-config-advanced', + label: 'Codex Config Advanced', + url: 'https://developers.openai.com/codex/config-advanced', + description: 'Layering, trust, profiles, and advanced config behavior.', + }, + { + id: 'codex-config-reference', + label: 'Codex Config Reference', + url: 'https://developers.openai.com/codex/config-reference', + description: 'Canonical upstream config surface for providers, MCP, features, and trust.', + }, + { + id: 'codex-releases', + label: 'Codex GitHub Releases', + url: 'https://github.com/openai/codex/releases', + description: 'Track upstream release notes and fast-moving CLI changes.', + }, +]; + +const DEFAULT_PROVIDER_DOCS: CompatibleCliProviderDocLink[] = [ + { + provider: 'openai', + label: 'OpenAI Responses API', + apiFormat: 'Responses API', + url: 'https://platform.openai.com/docs/api-reference/responses', + }, +]; + +function renderTextWithLinks(text: string): ReactNode[] { + const urlPattern = /https?:\/\/[^\s)]+/g; + const nodes: ReactNode[] = []; + let cursor = 0; + let match: RegExpExecArray | null; + + while ((match = urlPattern.exec(text)) !== null) { + const [url] = match; + const index = match.index; + + if (index > cursor) { + nodes.push(text.slice(cursor, index)); + } + + nodes.push( + + {url} + + ); + cursor = index + url.length; + } + + if (cursor < text.length) { + nodes.push(text.slice(cursor)); + } + + return nodes.length > 0 ? nodes : [text]; +} + +interface CodexDocsTabProps { + diagnostics: CodexDashboardDiagnostics; +} + +export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) { + const docsReference = diagnostics.docsReference ?? { + notes: [], + links: [], + providerDocs: [], + providerValues: [], + settingsHierarchy: [], + }; + const docsLinks = docsReference.links.length > 0 ? docsReference.links : DEFAULT_CODEX_DOC_LINKS; + const providerDocs = + docsReference.providerDocs.length > 0 ? docsReference.providerDocs : DEFAULT_PROVIDER_DOCS; + + return ( + +
+ + + + + Upstream notes + + + + {docsReference.notes.map((note, index) => ( +

+ - {renderTextWithLinks(note)} +

+ ))} + +
+

Codex docs

+ +
+ +
+

+ Provider / bridge reference +

+ +
+ {docsReference.providerValues.length > 0 && ( + <> + +

+ Provider values: {docsReference.providerValues.join(', ')} +

+ + )} + {docsReference.settingsHierarchy.length > 0 && ( +

+ Settings hierarchy: {docsReference.settingsHierarchy.join(' -> ')} +

+ )} +
+
+
+
+ ); +} diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx new file mode 100644 index 00000000..9a2f34ee --- /dev/null +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -0,0 +1,315 @@ +import { + AlertTriangle, + CheckCircle2, + Folder, + Info, + Route, + ShieldCheck, + TerminalSquare, + XCircle, +} from 'lucide-react'; +import { QuickCommands } from '@/components/shared'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types'; +import { cn } from '@/lib/utils'; + +function formatTimestamp(value: number | null | undefined): string { + if (!value || !Number.isFinite(value)) return 'N/A'; + return new Date(value).toLocaleString(); +} + +function formatBytes(value: number | null | undefined): string { + if (!value || value <= 0) return '0 B'; + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / (1024 * 1024)).toFixed(2)} MB`; +} + +function DetailRow({ + label, + value, + mono = false, +}: { + label: string; + value: string; + mono?: boolean; +}) { + return ( +
+ {label} + {value} +
+ ); +} + +interface CodexOverviewTabProps { + diagnostics: CodexDashboardDiagnostics; +} + +export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { + return ( + +
+ + + + + How Codex works in CCS + + + +

Codex is a first-class runtime target in CCS, but it stays runtime-only in v1.

+

+ Saved default targets for API profiles and variants still remain on Claude or Droid. +

+

+ CCS-backed Codex launches can apply transient -c overrides and inject + CCS_CODEX_API_KEY, so effective runtime values may not match this file + exactly. +

+
+
+ + + + + + Runtime install + + + +
+ Status + + {diagnostics.binary.installed ? 'Detected' : 'Not found'} + +
+ + + + + +
+ --config override support + + {diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'} + +
+
+
+ + + + + + Config file + + + +
+
+ User config + {diagnostics.file.exists ? ( + + ) : ( + + )} +
+ + + + + {diagnostics.file.parseError && ( +

+ TOML warning: {diagnostics.file.parseError} +

+ )} + {diagnostics.file.readError && ( +

+ Read warning: {diagnostics.file.readError} +

+ )} +
+
+
+ + + + + + Current user-layer summary + + + + + + + + + + +
+ + providers: {diagnostics.config.modelProviderCount} + + + profiles: {diagnostics.config.profileCount} + + + enabled features: {diagnostics.config.enabledFeatures.length} + + + MCP servers: {diagnostics.config.mcpServerCount} + +
+ {diagnostics.config.topLevelKeys.length > 0 && ( +
+

+ User-layer keys present +

+
+ {diagnostics.config.topLevelKeys.map((key) => ( + + {key} + + ))} +
+
+ )} +
+
+ + + + + + + + Runtime vs provider + + + +
+

Native Codex runtime

+

+ Use ccs-codex, ccsx, or --target codex. CCS + launches the local Codex CLI and depends on native Codex capabilities such as{' '} + --config overrides. +

+
+
+

Codex provider / bridge

+

+ CCS can route provider credentials transiently through CLIProxy. That is not the + same as editing local config.toml, and some routed values may never + persist here. +

+
+
+
+ + + + Supported flows + + + + + + Flow + Status + Notes + + + + {diagnostics.supportMatrix.map((entry) => ( + + {entry.label} + + + {entry.supported ? 'Yes' : 'No'} + + + {entry.notes} + + ))} + +
+
+
+ + {diagnostics.warnings.length > 0 && ( + + + + + Warnings + + + + {diagnostics.warnings.map((warning) => ( +

+ - {warning} +

+ ))} +
+
+ )} +
+
+ ); +} diff --git a/ui/src/hooks/use-codex-types.ts b/ui/src/hooks/use-codex-types.ts index af554937..9bfeab27 100644 --- a/ui/src/hooks/use-codex-types.ts +++ b/ui/src/hooks/use-codex-types.ts @@ -1,212 +1,22 @@ -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 interface CodexBinaryDiagnostics { - installed: boolean; - path: string | null; - installDir: string | null; - source: 'CCS_CODEX_PATH' | 'PATH' | 'missing'; - 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 | null; - parseError: 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 type CodexProfilePatchValues = 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; -} +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'; diff --git a/ui/src/hooks/use-codex.ts b/ui/src/hooks/use-codex.ts index 4123f272..2291ea1d 100644 --- a/ui/src/hooks/use-codex.ts +++ b/ui/src/hooks/use-codex.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import { parse as parseToml } from 'smol-toml'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApiConflictError, withApiBase } from '@/lib/api-client'; +import { safeParseTomlObject } from '@shared/toml-object'; import type { CodexConfigPatchInput, CodexConfigPatchResult, @@ -23,30 +23,6 @@ interface SaveCodexRawConfigResponse { type PatchCodexConfigResponse = CodexConfigPatchResult; -function parseCodexRawConfigText(rawText: string): { - config: Record | null; - parseError: string | null; -} { - try { - const parsed = rawText.trim() ? parseToml(rawText) : {}; - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - return { - config: null, - parseError: 'TOML root must be a table.', - }; - } - return { - config: parsed as Record, - parseError: null, - }; - } catch (error) { - return { - config: null, - parseError: (error as Error).message, - }; - } -} - async function fetchCodexDiagnostics(): Promise { const res = await fetch(withApiBase('/codex/diagnostics')); if (!res.ok) throw new Error('Failed to fetch Codex diagnostics'); @@ -111,7 +87,7 @@ export function useCodex() { queryClient.setQueryData(['codex-raw-config'], (current) => { const path = current?.path ?? '$CODEX_HOME/config.toml'; const resolvedPath = current?.resolvedPath ?? path; - const parsed = parseCodexRawConfigText(variables.rawText); + const parsed = safeParseTomlObject(variables.rawText); return { path, diff --git a/ui/src/pages/codex.tsx b/ui/src/pages/codex.tsx index d7449554..556c3dab 100644 --- a/ui/src/pages/codex.tsx +++ b/ui/src/pages/codex.tsx @@ -1,42 +1,13 @@ -import { type ReactNode, useMemo, useState } from 'react'; -import { parse as parseToml } from 'smol-toml'; +import { useMemo, useState } from 'react'; import { toast } from 'sonner'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { - AlertTriangle, - CheckCircle2, - ExternalLink, - Folder, - GripVertical, - Info, - Loader2, - Route, - ShieldCheck, - TerminalSquare, - XCircle, -} from 'lucide-react'; +import { GripVertical, Loader2 } from 'lucide-react'; +import { CodexControlCenterTab } from '@/components/compatible-cli/codex-control-center-tab'; +import { CodexDocsTab } from '@/components/compatible-cli/codex-docs-tab'; import { useCodex } from '@/hooks/use-codex'; import { isApiConflictError } from '@/lib/api-client'; +import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab'; import { RawConfigEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel'; -import { CodexFeaturesCard } from '@/components/compatible-cli/codex-features-card'; -import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card'; -import { CodexModelProvidersCard } from '@/components/compatible-cli/codex-model-providers-card'; -import { CodexProfilesCard } from '@/components/compatible-cli/codex-profiles-card'; -import { CodexProjectTrustCard } from '@/components/compatible-cli/codex-project-trust-card'; -import { CodexTopLevelControlsCard } from '@/components/compatible-cli/codex-top-level-controls-card'; -import { QuickCommands } from '@/components/shared'; -import { Badge } from '@/components/ui/badge'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Separator } from '@/components/ui/separator'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { KNOWN_CODEX_FEATURES, @@ -47,121 +18,7 @@ import { readCodexProjectTrust, readCodexTopLevelSettings, } from '@/lib/codex-config'; -import { cn } from '@/lib/utils'; - -const DEFAULT_CODEX_DOC_LINKS = [ - { - id: 'codex-config-basic', - label: 'Codex Config Basics', - url: 'https://developers.openai.com/codex/config-basic', - description: 'Official user-layer setup, config location, and baseline configuration behavior.', - }, - { - id: 'codex-config-advanced', - label: 'Codex Config Advanced', - url: 'https://developers.openai.com/codex/config-advanced', - description: 'Layering, trust, profiles, and advanced config behavior.', - }, - { - id: 'codex-config-reference', - label: 'Codex Config Reference', - url: 'https://developers.openai.com/codex/config-reference', - description: 'Canonical upstream config surface for providers, MCP, features, and trust.', - }, - { - id: 'codex-releases', - label: 'Codex GitHub Releases', - url: 'https://github.com/openai/codex/releases', - description: 'Track upstream release notes and fast-moving CLI changes.', - }, -]; - -const DEFAULT_PROVIDER_DOCS = [ - { - provider: 'openai', - label: 'OpenAI Responses API', - apiFormat: 'Responses API', - url: 'https://platform.openai.com/docs/api-reference/responses', - }, -]; - -function renderTextWithLinks(text: string): ReactNode[] { - const urlPattern = /https?:\/\/[^\s)]+/g; - const nodes: ReactNode[] = []; - let cursor = 0; - let match: RegExpExecArray | null; - - while ((match = urlPattern.exec(text)) !== null) { - const [url] = match; - const index = match.index; - - if (index > cursor) { - nodes.push(text.slice(cursor, index)); - } - - nodes.push( - - {url} - - ); - cursor = index + url.length; - } - - if (cursor < text.length) { - nodes.push(text.slice(cursor)); - } - - return nodes.length > 0 ? nodes : [text]; -} - -function formatTimestamp(value: number | null | undefined): string { - if (!value || !Number.isFinite(value)) return 'N/A'; - return new Date(value).toLocaleString(); -} - -function formatBytes(value: number | null | undefined): string { - if (!value || value <= 0) return '0 B'; - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; - return `${(value / (1024 * 1024)).toFixed(2)} MB`; -} - -function parseTomlObjectText( - text: string -): { valid: true; value: Record } | { valid: false; error: string } { - try { - const parsed = text.trim() ? parseToml(text) : {}; - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - return { valid: false, error: 'TOML root must be a table.' }; - } - return { valid: true, value: parsed as Record }; - } catch (error) { - return { valid: false, error: (error as Error).message }; - } -} - -function DetailRow({ - label, - value, - mono = false, -}: { - label: string; - value: string; - mono?: boolean; -}) { - return ( -
- {label} - {value} -
- ); -} +import { safeParseTomlObject } from '@shared/toml-object'; export function CodexPage() { const { @@ -183,10 +40,10 @@ export function CodexPage() { const rawBaseText = rawConfig?.rawText ?? ''; const rawEditorText = rawDraftText ?? rawBaseText; const rawConfigDirty = rawDraftText !== null && rawDraftText !== rawBaseText; - const rawEditorParsed = parseTomlObjectText(rawEditorText); - const rawEditorValidation = rawEditorParsed.valid - ? { valid: true as const } - : { valid: false as const, error: rawEditorParsed.error }; + const rawEditorParsed = safeParseTomlObject(rawEditorText); + const rawEditorValidation = rawEditorParsed.parseError + ? { valid: false as const, error: rawEditorParsed.parseError } + : { valid: true as const }; const controlsConfig = rawConfig?.config ?? null; const structuredControlsDisabled = rawConfigLoading || !rawConfig || rawConfigDirty || rawConfig?.parseError !== null; @@ -271,7 +128,9 @@ export function CodexPage() { } }; - const renderOverview = () => { + const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden'; + + const renderSidebar = () => { if (diagnosticsLoading) { return (
@@ -289,19 +148,6 @@ export function CodexPage() { ); } - const docsReference = diagnostics.docsReference ?? { - notes: [], - links: [], - providerDocs: [], - providerValues: [], - settingsHierarchy: [], - }; - const docsLinks = - docsReference.links.length > 0 ? docsReference.links : DEFAULT_CODEX_DOC_LINKS; - const providerDocs = - docsReference.providerDocs.length > 0 ? docsReference.providerDocs : DEFAULT_PROVIDER_DOCS; - const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden'; - return (
@@ -314,498 +160,29 @@ export function CodexPage() {
- -
- - - - - How Codex works in CCS - - - -

- Codex is a first-class runtime target in CCS, but it stays runtime-only in v1. -

-

- Saved default targets for API profiles and variants still remain on Claude or - Droid. -

-

- CCS-backed Codex launches can apply transient -c overrides and - inject CCS_CODEX_API_KEY, so effective runtime values may not - match this file exactly. -

-
-
- - - - - - Runtime install - - - -
- Status - - {diagnostics.binary.installed ? 'Detected' : 'Not found'} - -
- - - - - -
- - --config override support - - - {diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'} - -
-
-
- - - - - - Config file - - - -
-
- User config - {diagnostics.file.exists ? ( - - ) : ( - - )} -
- - - - - {diagnostics.file.parseError && ( -

- TOML warning: {diagnostics.file.parseError} -

- )} - {diagnostics.file.readError && ( -

- Read warning: {diagnostics.file.readError} -

- )} -
-
-
- - - - - - Current user-layer summary - - - - - - - - - - -
- - providers: {diagnostics.config.modelProviderCount} - - - profiles: {diagnostics.config.profileCount} - - - enabled features: {diagnostics.config.enabledFeatures.length} - - - MCP servers: {diagnostics.config.mcpServerCount} - -
- {diagnostics.config.topLevelKeys.length > 0 && ( -
-

- User-layer keys present -

-
- {diagnostics.config.topLevelKeys.map((key) => ( - - {key} - - ))} -
-
- )} -
-
- - - - - - - - Runtime vs provider - - - -
-

Native Codex runtime

-

- Use ccs-codex, ccsx, or{' '} - --target codex. CCS launches the local Codex CLI and depends on - native Codex capabilities such as --config overrides. -

-
-
-

Codex provider / bridge

-

- CCS can route provider credentials transiently through CLIProxy. That is not - the same as editing local config.toml, and some routed values - may never persist here. -

-
-
-
- - - - Supported flows - - - - - - Flow - Status - Notes - - - - {diagnostics.supportMatrix.map((entry) => ( - - {entry.label} - - - {entry.supported ? 'Yes' : 'No'} - - - - {entry.notes} - - - ))} - -
-
-
- - {diagnostics.warnings.length > 0 && ( - - - - - Warnings - - - - {diagnostics.warnings.map((warning) => ( -

- - {warning} -

- ))} -
-
- )} -
-
+
- -
- - - - - Structured controls boundary - - - -

- Guided controls write only the user-layer config.toml. They do - not model the full effective Codex runtime once trusted repo layers and CCS - transient -c overrides are involved. -

-

- Structured saves normalize TOML formatting and strip comments. Use the raw - editor on the right when exact layout matters. -

-
-
- - entry.name)} - disabled={structuredControlsDisabled} - disabledReason={controlsDisabledReason} - saving={isPatchingConfig} - onSave={(values) => - runConfigPatch({ kind: 'top-level', values }, 'Saved top-level Codex settings.') - } - /> - - - runConfigPatch( - { kind: 'project-trust', path: projectPath, trustLevel }, - trustLevel ? 'Saved project trust entry.' : 'Removed project trust entry.' - ) - } - /> - - entry.name)} - disabled={structuredControlsDisabled} - disabledReason={controlsDisabledReason} - saving={isPatchingConfig} - onSave={(name, values, setAsActive) => - runConfigPatch( - { kind: 'profile', action: 'upsert', name, values, setAsActive }, - 'Saved profile.' - ) - } - onDelete={(name) => - runConfigPatch({ kind: 'profile', action: 'delete', name }, 'Deleted profile.') - } - onSetActive={(name) => - runConfigPatch( - { kind: 'profile', action: 'set-active', name }, - 'Set active profile.' - ) - } - /> - - - runConfigPatch( - { kind: 'model-provider', action: 'upsert', name, values }, - 'Saved model provider.' - ) - } - onDelete={(name) => - runConfigPatch( - { kind: 'model-provider', action: 'delete', name }, - 'Deleted model provider.' - ) - } - /> - - - runConfigPatch( - { kind: 'mcp-server', action: 'upsert', name, values }, - 'Saved MCP server.' - ) - } - onDelete={(name) => - runConfigPatch( - { kind: 'mcp-server', action: 'delete', name }, - 'Deleted MCP server.' - ) - } - /> - - - runConfigPatch({ kind: 'feature', feature, enabled }, 'Saved feature toggle.') - } - /> -
-
+
- -
- - - - - Upstream notes - - - - {docsReference.notes.map((note, index) => ( -

- - {renderTextWithLinks(note)} -

- ))} - -
-

- Codex docs -

- -
- -
-

- Provider / bridge reference -

- -
- {docsReference.providerValues.length > 0 && ( - <> - -

- Provider values: {docsReference.providerValues.join(', ')} -

- - )} - {docsReference.settingsHierarchy.length > 0 && ( -

- Settings hierarchy: {docsReference.settingsHierarchy.join(' -> ')} -

- )} -
-
-
-
+
@@ -816,7 +193,7 @@ export function CodexPage() {
-
{renderOverview()}
+
{renderSidebar()}