diff --git a/docs/research/codex-native-runtime-integration-decision.md b/docs/research/codex-native-runtime-integration-decision.md index 145e4692..b90fe412 100644 --- a/docs/research/codex-native-runtime-integration-decision.md +++ b/docs/research/codex-native-runtime-integration-decision.md @@ -18,6 +18,32 @@ Record the chosen direction for improving Codex integration in the multimodel ru - Assessment: `๐ŸŽฏ 9 ๐Ÿ›ก๏ธ 9 ๐Ÿง  7` - Estimated first serious wave: `2200-4500` lines across `agent_teams_orchestrator`, `claude_team`, and `plugin-kit-ai` +## Current Status As Of 2026-04-19 + +The staged cutover is now complete through Phase 4. + +- Phase 0 - implementation-complete and evidence-backed +- Phase 1 - rollout-state preparation complete +- Phase 2 - limited internal unlock completed +- Phase 3 - native-first default switch completed +- Phase 4 - legacy Codex lane removal completed + +Current product truth: + +- Codex now runs only through the `codex-native` lane in normal product flows +- legacy `adapter` and `api` Codex runtime lanes have been removed from active runtime selection and launch paths +- runtime status now exposes a single native Codex backend option +- stored legacy Codex backend values normalize forward to `codex-native` +- the remaining supported credential surface for native Codex is: + - `CODEX_API_KEY` + - `OPENAI_API_KEY` + +Repo-visible evidence: + +- [codex-native-runtime-phase-0-signoff-evidence.md](./codex-native-runtime-phase-0-signoff-evidence.md) +- [codex-native-runtime-phase-1-signoff-evidence.md](./codex-native-runtime-phase-1-signoff-evidence.md) +- [codex-native-runtime-phase-4-signoff-evidence.md](./codex-native-runtime-phase-4-signoff-evidence.md) + ## One-Page Summary We are **not** doing a one-shot swap from the current Codex backend to `@openai/codex-sdk / codex exec`. diff --git a/docs/research/codex-native-runtime-phase-4-signoff-evidence.md b/docs/research/codex-native-runtime-phase-4-signoff-evidence.md new file mode 100644 index 00000000..27cffb6a --- /dev/null +++ b/docs/research/codex-native-runtime-phase-4-signoff-evidence.md @@ -0,0 +1,199 @@ +# Codex Native Runtime - Phase 4 Sign-off Evidence + +Captured on 2026-04-19. + +This file records the repo-visible evidence package for the final native-only Codex cutover. + +Related documents: + +- [codex-native-runtime-integration-decision.md](./codex-native-runtime-integration-decision.md) +- [codex-native-runtime-phase-1-signoff-evidence.md](./codex-native-runtime-phase-1-signoff-evidence.md) + +## Verdict + +Phase 4 legacy removal is now complete. + +What this proves: + +- `codex-native` is now the only Codex runtime lane +- old `adapter` and `api` Codex lanes are no longer launchable through active runtime code paths +- Codex runtime status now exposes a single native option instead of a mixed legacy/native selector +- stored legacy backend values normalize forward to `codex-native` +- UI-facing Codex status, model availability, launch identity, replay parsing, and provisioning all remain truthful after legacy removal + +What this does **not** mean: + +- plugin execution parity is now guaranteed for multimodel Codex sessions +- broader app-server or interactive-request parity has been added +- Codex runtime failures silently fall back to another hidden Codex implementation + +## Command Package + +### `agent_teams_orchestrator` + +Executed: + +```bash +bun test src/services/runtimeBackends/codexBackendResolver.test.ts \ + src/services/runtimeBackends/registry.codexNativeStates.test.ts \ + src/services/runtimeBackends/registry.agentTeams.test.ts \ + src/utils/swarm/spawnUtils.test.ts +``` + +Observed result: + +- `23 pass` +- `0 fail` + +Executed: + +```bash +bun run signoff:codex-native-phase4 +``` + +Observed result: + +- exit code `0` +- four live CLI native-only scenarios verified: + - `ready` + - `authentication-required` + - `runtime-missing` + - `openai-api-key-also-works` + +### `claude_team` + +Executed: + +```bash +pnpm exec vitest run \ + test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts \ + test/main/services/runtime/providerAwareCliEnv.test.ts \ + test/main/services/runtime/ProviderConnectionService.test.ts \ + test/main/ipc/configValidation.test.ts \ + test/main/services/team/TeamProvisioningService.test.ts \ + test/main/services/parsing/CodexNativePhase0Smoke.test.ts \ + test/main/services/parsing/SessionParser.test.ts \ + test/main/services/team/BoardTaskExactLogStrictParser.test.ts \ + test/renderer/components/runtime/providerConnectionUi.test.ts \ + test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts \ + test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts \ + test/renderer/components/cli/CliStatusVisibility.test.ts \ + test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts \ + test/renderer/components/team/dialogs/launchDialogPrefill.test.ts \ + test/renderer/utils/memberRuntimeSummary.test.ts \ + test/renderer/utils/teamModelAvailability.test.ts +``` + +Observed result: + +- `16` files passed +- `180` tests passed +- `0` failures + +## Live Native-only Status Evidence + +Runner: + +```bash +runtime status --provider codex --json +``` + +Observed live scenarios: + +### Ready + +- selected backend: `codex-native` +- resolved backend: `codex-native` +- provider status: `Codex native runtime ready` +- native option: + - `selectable=true` + - `available=true` + - `state=ready` + - `audience=general` + - `statusMessage=Ready` + +### Authentication required + +- selected backend: `codex-native` +- resolved backend: `null` +- provider status: `Codex native runtime unavailable` +- native option: + - `selectable=false` + - `available=false` + - `state=authentication-required` + - `audience=general` + - `statusMessage=Authentication required` + +### Runtime missing + +- selected backend: `codex-native` +- resolved backend: `null` +- provider status: `Codex native runtime unavailable` +- native option: + - `selectable=false` + - `available=false` + - `state=runtime-missing` + - `audience=general` + - `statusMessage=Codex CLI not found` + +### `OPENAI_API_KEY` also works + +- selected backend: `codex-native` +- resolved backend: `codex-native` +- provider status: `Codex native runtime ready` +- explicit proof that the native lane still accepts: + - `CODEX_API_KEY` + - or `OPENAI_API_KEY` + +This is the explicit proof that the final cutover no longer depends on a legacy adapter/API runtime seam while still preserving the supported credential surface. + +## App-facing Native-only Truth Proof + +Covered by green targeted tests: + +- `test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts` +- `test/main/services/runtime/providerAwareCliEnv.test.ts` +- `test/main/services/runtime/ProviderConnectionService.test.ts` +- `test/main/ipc/configValidation.test.ts` +- `test/main/services/team/TeamProvisioningService.test.ts` +- `test/main/services/parsing/CodexNativePhase0Smoke.test.ts` +- `test/main/services/parsing/SessionParser.test.ts` +- `test/main/services/team/BoardTaskExactLogStrictParser.test.ts` +- `test/renderer/components/runtime/providerConnectionUi.test.ts` +- `test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts` +- `test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts` +- `test/renderer/components/cli/CliStatusVisibility.test.ts` +- `test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts` +- `test/renderer/components/team/dialogs/launchDialogPrefill.test.ts` +- `test/renderer/utils/memberRuntimeSummary.test.ts` +- `test/renderer/utils/teamModelAvailability.test.ts` + +These tests prove: + +- legacy Codex backend values normalize forward to `codex-native` +- settings and dashboard now describe Codex as native-first, not adapter/API-first +- provider backend identity survives team launch, relaunch, and launch-prefill flows +- parser and exact-log readers stay truthful for native transcript authority rows +- provisioning summaries and member runtime summaries no longer flatten native truth into old Codex copy +- team model availability is keyed to the native runtime path instead of old ChatGPT-subscription heuristics + +## Legacy Removal Proof + +Covered by green targeted tests and runtime sign-off: + +- orchestrator runtime backend resolver now exposes only `codex-native` +- runtime registry now exposes a single Codex backend option +- no active runtime branch launches Codex through: + - `adapter` + - `api` +- old transport-only smoke/signoff scripts tied to legacy Codex runtime were removed + +This is the explicit proof that Phase 4 is a real cutover, not just a UI relabeling. + +## Sign-off Conclusion + +โœ… The Phase 4 exit gate is satisfied. + +Codex inside the multimodel runtime is now native-only. + +There is no longer a product-supported legacy Codex runtime lane to roll back to inside normal UI flows. diff --git a/src/main/ipc/configValidation.ts b/src/main/ipc/configValidation.ts index 5abe1d38..baf2a434 100644 --- a/src/main/ipc/configValidation.ts +++ b/src/main/ipc/configValidation.ts @@ -5,6 +5,8 @@ import * as path from 'path'; +import { migrateProviderBackendId } from '@shared/utils/providerBackend'; + import type { AppConfig, DisplayConfig, @@ -450,11 +452,13 @@ function validateRuntimeSection(data: unknown): ValidationSuccess<'runtime'> | V ) { return { valid: false, - error: - 'runtime.providerBackends.codex must be one of: auto, adapter, api, codex-native', + error: 'runtime.providerBackends.codex must be one of: codex-native', }; } - providerBackends.codex = backendId; + providerBackends.codex = migrateProviderBackendId( + 'codex', + backendId + ) as RuntimeConfig['providerBackends']['codex']; continue; } diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index a20fcc09..6540def1 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -91,6 +91,7 @@ import { PROTECTED_CLI_FLAGS, } from '@shared/utils/cliArgsParser'; import { createLogger } from '@shared/utils/logger'; +import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { isRateLimitMessage } from '@shared/utils/rateLimitDetector'; import { buildStandaloneSlashCommandMeta, @@ -1434,6 +1435,17 @@ async function handleLaunchTeam( const membersMeta = await membersStore.getMeta(tn); const members = membersMeta?.members ?? []; + const resolvedProviderId = + payload.providerId === 'codex' + ? 'codex' + : payload.providerId === 'gemini' + ? 'gemini' + : meta?.providerId === 'codex' + ? 'codex' + : meta?.providerId === 'gemini' + ? 'gemini' + : 'anthropic'; + const createRequest: TeamCreateRequest = { teamName: tn, displayName: meta?.displayName, @@ -1441,20 +1453,11 @@ async function handleLaunchTeam( color: meta?.color, cwd, prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined, - providerId: - payload.providerId === 'codex' - ? 'codex' - : payload.providerId === 'gemini' - ? 'gemini' - : meta?.providerId === 'codex' - ? 'codex' - : meta?.providerId === 'gemini' - ? 'gemini' - : 'anthropic', - providerBackendId: - providerBackendValidation.value ?? - meta?.providerBackendId ?? - membersMeta?.providerBackendId, + providerId: resolvedProviderId, + providerBackendId: migrateProviderBackendId( + resolvedProviderId, + providerBackendValidation.value ?? meta?.providerBackendId ?? membersMeta?.providerBackendId + ), model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined, effort: isValidEffort(payload.effort) ? payload.effort : undefined, limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined, @@ -3926,6 +3929,8 @@ async function handleGetSavedRequest( const membersMeta = await membersStore.getMeta(tn); const members = membersMeta?.members ?? []; + const resolvedProviderId = meta.providerId ?? 'anthropic'; + return { success: true, data: { @@ -3935,8 +3940,11 @@ async function handleGetSavedRequest( color: meta.color, cwd: meta.cwd, prompt: meta.prompt, - providerId: meta.providerId ?? 'anthropic', - providerBackendId: meta.providerBackendId ?? membersMeta?.providerBackendId, + providerId: resolvedProviderId, + providerBackendId: migrateProviderBackendId( + resolvedProviderId, + meta.providerBackendId ?? membersMeta?.providerBackendId + ), model: meta.model, effort: meta.effort as TeamCreateRequest['effort'], skipPermissions: meta.skipPermissions, diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts index 56317c7a..7d663f10 100644 --- a/src/main/services/infrastructure/ConfigManager.ts +++ b/src/main/services/infrastructure/ConfigManager.ts @@ -11,6 +11,7 @@ import { getClaudeBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder'; import { validateRegexPattern } from '@main/utils/regexValidation'; +import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { createLogger } from '@shared/utils/logger'; import * as fs from 'fs'; import * as fsp from 'fs/promises'; @@ -226,7 +227,7 @@ export interface GeneralConfig { export interface RuntimeConfig { providerBackends: { gemini: 'auto' | 'api' | 'cli-sdk'; - codex: 'auto' | 'adapter' | 'api' | 'codex-native'; + codex: 'codex-native'; }; } @@ -575,6 +576,10 @@ export class ConfigManager { providerBackends: { ...DEFAULT_CONFIG.runtime.providerBackends, ...(loaded.runtime?.providerBackends ?? {}), + codex: migrateProviderBackendId( + 'codex', + loaded.runtime?.providerBackends?.codex + ) as RuntimeConfig['providerBackends']['codex'], }, }, display: { diff --git a/src/main/services/runtime/ProviderConnectionService.ts b/src/main/services/runtime/ProviderConnectionService.ts index 08c1451a..1d7e51a7 100644 --- a/src/main/services/runtime/ProviderConnectionService.ts +++ b/src/main/services/runtime/ProviderConnectionService.ts @@ -25,7 +25,7 @@ const PROVIDER_CAPABILITIES: Record< configurableAuthModes: ['auto', 'oauth', 'api_key'], }, codex: { - supportsOAuth: true, + supportsOAuth: false, supportsApiKey: true, configurableAuthModes: [], }, @@ -42,7 +42,6 @@ const PROVIDER_API_KEY_ENV_VARS: Partial> = { gemini: 'GEMINI_API_KEY', }; -const CODEX_API_KEY_BETA_ENV_VAR = 'CLAUDE_CODE_CODEX_API_KEY_BETA'; const CODEX_NATIVE_API_KEY_ENV_VAR = 'CODEX_API_KEY'; const CODEX_NATIVE_BACKEND_ID = 'codex-native'; @@ -65,8 +64,7 @@ export class ProviderConnectionService { } if (providerId === 'codex') { - const codexConnection = this.configManager.getConfig().providerConnections.codex; - return this.shouldExposeCodexConnectionModes() ? codexConnection.authMode : null; + return null; } return null; @@ -109,27 +107,7 @@ export class ProviderConnectionService { return env; } - const codexConnection = this.configManager.getConfig().providerConnections.codex; const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride); - if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) { - delete env[CODEX_API_KEY_BETA_ENV_VAR]; - delete env.OPENAI_API_KEY; - delete env[CODEX_NATIVE_API_KEY_ENV_VAR]; - return env; - } - - if (codexConnection.apiKeyBetaEnabled) { - env[CODEX_API_KEY_BETA_ENV_VAR] = '1'; - } else { - delete env[CODEX_API_KEY_BETA_ENV_VAR]; - } - - if (codexConnection.authMode === 'oauth') { - delete env.OPENAI_API_KEY; - delete env[CODEX_NATIVE_API_KEY_ENV_VAR]; - return env; - } - const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY'); const existingOpenAiKey = typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim() @@ -147,11 +125,7 @@ export class ProviderConnectionService { if (resolvedApiKey) { env.OPENAI_API_KEY = resolvedApiKey; - if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) { - env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey; - } else { - delete env[CODEX_NATIVE_API_KEY_ENV_VAR]; - } + env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey; return env; } @@ -192,22 +166,7 @@ export class ProviderConnectionService { return env; } - const codexConnection = this.configManager.getConfig().providerConnections.codex; const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride); - if (!this.shouldExposeCodexConnectionModes(runtimeBackendOverride)) { - return env; - } - - if (codexConnection.apiKeyBetaEnabled) { - env[CODEX_API_KEY_BETA_ENV_VAR] = '1'; - } else { - delete env[CODEX_API_KEY_BETA_ENV_VAR]; - } - - if (codexConnection.authMode !== 'api_key') { - return env; - } - const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY'); const existingOpenAiKey = typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim() @@ -272,30 +231,18 @@ export class ProviderConnectionService { return null; } - const codexConnection = this.configManager.getConfig().providerConnections.codex; const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride); if ( - !this.shouldExposeCodexConnectionModes(runtimeBackendOverride) || - codexConnection.authMode !== 'api_key' - ) { - return null; - } - - if (typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()) { - return null; - } - - if ( - codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID && - typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' && - env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim() + (typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()) || + (typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' && + env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim()) ) { return null; } return codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID - ? 'Codex API key mode is enabled for codex-native, but no OPENAI_API_KEY or CODEX_API_KEY is configured. Add a stored/environment API key or switch Codex auth mode back to OAuth.' - : 'Codex API key mode is enabled, but no OPENAI_API_KEY is configured. Add a stored/environment API key or switch Codex auth mode back to OAuth.'; + ? 'Codex native requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.' + : 'Codex requires OPENAI_API_KEY or CODEX_API_KEY. Add a stored or environment API key before launching Codex.'; } async getConfiguredConnectionIssues( @@ -336,27 +283,17 @@ export class ProviderConnectionService { const codexRuntimeBackend = providerId === 'codex' ? this.getConfiguredCodexRuntimeBackend() : null; const externalCredential = this.getExternalCredential(providerId, codexRuntimeBackend); - const codexBetaEnabled = - providerId === 'codex' - ? this.configManager.getConfig().providerConnections.codex.apiKeyBetaEnabled - : undefined; const configurableAuthModes = - providerId === 'codex' && - (codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) - ? (['oauth', 'api_key'] as CliProviderAuthMode[]) - : capabilities.configurableAuthModes; + providerId === 'codex' ? ([] as CliProviderAuthMode[]) : capabilities.configurableAuthModes; const configuredAuthMode = - providerId === 'codex' && - !(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) - ? null - : this.getConfiguredAuthMode(providerId); + providerId === 'codex' ? null : this.getConfiguredAuthMode(providerId); return { ...capabilities, configurableAuthModes, configuredAuthMode, - apiKeyBetaAvailable: providerId === 'codex' ? true : undefined, - apiKeyBetaEnabled: codexBetaEnabled, + apiKeyBetaAvailable: providerId === 'codex' ? undefined : undefined, + apiKeyBetaEnabled: providerId === 'codex' ? undefined : undefined, apiKeyConfigured: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim()), apiKeySource: storedApiKey?.value.trim() ? 'stored' @@ -380,31 +317,16 @@ export class ProviderConnectionService { return this.apiKeyService.lookupPreferred(envVarName); } - private getConfiguredCodexRuntimeBackend( - runtimeBackendOverride?: string | null - ): 'auto' | 'adapter' | 'api' | 'codex-native' { - if ( - runtimeBackendOverride === 'auto' || - runtimeBackendOverride === 'adapter' || - runtimeBackendOverride === 'api' || - runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID - ) { + private getConfiguredCodexRuntimeBackend(runtimeBackendOverride?: string | null): 'codex-native' { + if (runtimeBackendOverride === CODEX_NATIVE_BACKEND_ID) { return runtimeBackendOverride; } - return this.configManager.getConfig().runtime.providerBackends.codex; - } - - private shouldExposeCodexConnectionModes(runtimeBackendOverride?: string | null): boolean { - const config = this.configManager.getConfig(); - return ( - config.providerConnections.codex.apiKeyBetaEnabled || - this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride) === CODEX_NATIVE_BACKEND_ID - ); + return CODEX_NATIVE_BACKEND_ID; } private getExternalCredential( providerId: CliProviderId, - codexRuntimeBackend: 'auto' | 'adapter' | 'api' | 'codex-native' | null = null + codexRuntimeBackend: 'codex-native' | null = null ): ExternalCredential { const shellEnv = getCachedShellEnv() ?? {}; const sources = [shellEnv, process.env]; @@ -440,14 +362,12 @@ export class ProviderConnectionService { } if (providerId === 'codex') { - if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID) { - const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR); - if (nativeApiKey) { - return { - label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`, - value: nativeApiKey, - }; - } + const nativeApiKey = findEnvValue(CODEX_NATIVE_API_KEY_ENV_VAR); + if (nativeApiKey) { + return { + label: `Detected from ${CODEX_NATIVE_API_KEY_ENV_VAR}`, + value: nativeApiKey, + }; } const apiKey = findEnvValue('OPENAI_API_KEY'); diff --git a/src/main/services/team/TeamMetaStore.ts b/src/main/services/team/TeamMetaStore.ts index a27fc165..2f9553a0 100644 --- a/src/main/services/team/TeamMetaStore.ts +++ b/src/main/services/team/TeamMetaStore.ts @@ -1,4 +1,5 @@ import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead'; +import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { getTeamsBasePath } from '@main/utils/pathDecoder'; import * as fs from 'fs'; import * as path from 'path'; @@ -83,6 +84,11 @@ export class TeamMetaStore { return null; } + const providerId = + file.providerId === 'anthropic' || file.providerId === 'codex' || file.providerId === 'gemini' + ? file.providerId + : undefined; + return { version: 1, displayName: @@ -92,13 +98,11 @@ export class TeamMetaStore { color: typeof file.color === 'string' ? file.color.trim() || undefined : undefined, cwd: file.cwd.trim(), prompt: typeof file.prompt === 'string' ? file.prompt.trim() || undefined : undefined, - providerId: - file.providerId === 'anthropic' || - file.providerId === 'codex' || - file.providerId === 'gemini' - ? file.providerId - : undefined, - providerBackendId: normalizeOptionalBackendId(file.providerBackendId), + providerId, + providerBackendId: migrateProviderBackendId( + providerId, + normalizeOptionalBackendId(file.providerBackendId) + ), model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined, effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined, skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined, @@ -119,7 +123,10 @@ export class TeamMetaStore { cwd: data.cwd.trim(), prompt: data.prompt?.trim() || undefined, providerId: data.providerId, - providerBackendId: normalizeOptionalBackendId(data.providerBackendId), + providerBackendId: migrateProviderBackendId( + data.providerId, + normalizeOptionalBackendId(data.providerBackendId) + ), model: data.model?.trim() || undefined, effort: data.effort?.trim() || undefined, skipPermissions: data.skipPermissions, diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 5c655f93..5ca9aec3 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -48,6 +48,7 @@ import { } from '@shared/utils/inboxNoise'; import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection'; import { createLogger } from '@shared/utils/logger'; +import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { @@ -390,7 +391,7 @@ function getConfiguredRuntimeBackend(providerId: TeamProviderId): string | null case 'gemini': return runtimeConfig.gemini; case 'codex': - return runtimeConfig.codex; + return migrateProviderBackendId('codex', runtimeConfig.codex) ?? 'codex-native'; case 'anthropic': default: return null; @@ -420,7 +421,9 @@ function buildRuntimeLaunchWarning( const providerLabel = getTeamProviderLabel(providerId); const modelLabel = request.model?.trim() || 'default'; const effortLabel = request.effort ?? 'default'; - const backend = request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId); + const backend = + migrateProviderBackendId(providerId, request.providerBackendId?.trim()) || + getConfiguredRuntimeBackend(providerId); const flags: string[] = []; if (env.CLAUDE_CODE_USE_GEMINI === '1') flags.push('USE_GEMINI'); if (env.CLAUDE_CODE_USE_OPENAI === '1') flags.push('USE_OPENAI'); @@ -466,10 +469,12 @@ function logRuntimeLaunchSnapshot( const providerId = resolveTeamProviderId(request.providerId); const snapshot = { providerId, - providerBackendId: request.providerBackendId ?? null, + providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null, model: request.model ?? null, effort: request.effort ?? null, - configuredBackend: request.providerBackendId?.trim() || getConfiguredRuntimeBackend(providerId), + configuredBackend: + migrateProviderBackendId(providerId, request.providerBackendId?.trim()) || + getConfiguredRuntimeBackend(providerId), promptSize: options?.promptSize ?? null, expectedMembersCount: options?.expectedMembersCount ?? null, geminiRuntimeAuth: @@ -1159,8 +1164,10 @@ function shouldSkipResumeForProviderRuntimeChange( return { skip: false }; } - const requestedBackendId = request.providerBackendId?.trim() || null; - const previousBackendId = persistedProviderBackendId?.trim() || null; + const requestedBackendId = + migrateProviderBackendId(providerId, request.providerBackendId?.trim()) || null; + const previousBackendId = + migrateProviderBackendId(providerId, persistedProviderBackendId?.trim()) || null; if (requestedBackendId && previousBackendId && requestedBackendId !== previousBackendId) { return { skip: true, diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index b477f040..ee030652 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -244,7 +244,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): { return { args: ['auth', 'login', '--provider', provider.providerId], env: { - CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto', + CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native', }, }; } @@ -272,7 +272,7 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): { return { args: ['auth', 'logout', '--provider', provider.providerId], env: { - CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto', + CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native', }, }; } diff --git a/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx b/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx index bd999fae..3a75c4c9 100644 --- a/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx @@ -11,10 +11,7 @@ import { TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; -import { - formatProviderBackendLabel, - isLegacyCodexProviderBackendId, -} from '@renderer/utils/providerBackendIdentity'; +import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdentity'; import type { CliProviderStatus } from '@shared/types'; @@ -60,15 +57,7 @@ export function getProviderRuntimeBackendAudienceLabel( export function getVisibleProviderRuntimeBackendOptions( provider: CliProviderStatus ): NonNullable { - const options = provider.availableBackends ?? []; - if (provider.providerId !== 'codex') { - return options; - } - - const selectedBackendId = provider.selectedBackendId ?? null; - return options.filter( - (option) => !isLegacyCodexProviderBackendId(option.id) || option.id === selectedBackendId - ); + return provider.availableBackends ?? []; } export function getOptionDisplayLabel( @@ -77,17 +66,6 @@ export function getOptionDisplayLabel( resolvedOption: NonNullable[number] | null ): string { if (provider.providerId === 'codex') { - if (option.id === 'auto') { - const currentLabel = - resolvedOption && resolvedOption.id !== 'auto' - ? (formatProviderBackendLabel(provider.providerId, resolvedOption.id) ?? - resolvedOption.label) - : null; - return currentLabel - ? `Legacy auto fallback (currently: ${currentLabel})` - : 'Legacy auto fallback'; - } - const legacyLabel = formatProviderBackendLabel(provider.providerId, option.id); if (legacyLabel) { return legacyLabel; diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index 8e658291..fe75f3dd 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -30,6 +30,7 @@ import { isConnectionManagedRuntimeProvider, } from './providerConnectionUi'; import { + getVisibleProviderRuntimeBackendOptions, getProviderRuntimeBackendSummary, ProviderRuntimeBackendSelector, } from './ProviderRuntimeBackendSelector'; @@ -38,13 +39,7 @@ import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@sha import type { ApiKeyEntry } from '@shared/types/extensions'; type ApiKeyProviderId = 'anthropic' | 'codex' | 'gemini'; -type PendingConnectionAction = - | 'auto' - | 'oauth' - | 'api_key' - | 'codex-beta-on' - | 'codex-beta-off' - | null; +type PendingConnectionAction = 'auto' | 'oauth' | 'api_key' | null; interface ConnectionMethodCardOption { readonly authMode: CliProviderAuthMode; readonly title: string; @@ -86,7 +81,7 @@ const API_KEY_PROVIDER_CONFIG: Record< name: 'OpenAI API Key', title: 'API key', description: - 'Use `OPENAI_API_KEY` for Codex runs that need API-key billing. Codex native stays the primary runtime path while your subscription session remains available when you switch back.', + 'Codex native requires API-key credentials. Save OPENAI_API_KEY here and the app will mirror it into the native CODEX_API_KEY environment when launching Codex.', placeholder: 'sk-proj-...', }, gemini: { @@ -103,10 +98,6 @@ function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProv return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini'; } -function hasExplicitRuntimeBackends(provider: CliProviderStatus): boolean { - return (provider.availableBackends?.length ?? 0) > 0; -} - function isCodexNativeLane(provider: CliProviderStatus): boolean { return ( provider.providerId === 'codex' && @@ -124,11 +115,7 @@ function getConnectionDescription(provider: CliProviderStatus): string { case 'anthropic': return 'Choose how app-launched Anthropic sessions authenticate.'; case 'codex': - return hasExplicitRuntimeBackends(provider) - ? 'Choose which credentials app-launched Codex sessions should use. Codex native remains the primary runtime path unless you intentionally keep a legacy fallback selected.' - : provider.connection?.apiKeyBetaEnabled - ? 'Choose whether app-launched Codex sessions use your Codex subscription or API-key billing.' - : 'Codex native uses your subscription session by default. Enable API key mode only if you want native Codex launches to consume API-key credentials.'; + return 'Codex launches always use the native runtime now. Manage API-key credentials here before launching teams or one-shot Codex runs.'; case 'gemini': return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.'; } @@ -139,9 +126,7 @@ function getRuntimeDescription(provider: CliProviderStatus): string { case 'anthropic': return 'Anthropic currently has no separate runtime backend selector.'; case 'codex': - return hasExplicitRuntimeBackends(provider) - ? 'Choose which Codex runtime backend multimodel should use. Codex native is the default. Legacy fallbacks stay hidden unless they are already selected.' - : 'Codex native is the default runtime path. Connection method only controls which credentials the runtime can consume.'; + return 'Codex now runs only through the native runtime path.'; case 'gemini': return 'Choose which Gemini runtime backend multimodel should use.'; } @@ -160,9 +145,7 @@ function getAuthModeDescription(providerId: CliProviderId, authMode: CliProvider } if (providerId === 'codex') { - return authMode === 'api_key' - ? 'Use API-key credentials for app-launched Codex sessions. Codex native remains the primary runtime path and will consume those credentials when needed.' - : 'Use your Codex subscription session. API-key-only fallback paths remain unavailable until you switch this credential mode.'; + return 'Codex always launches through the native runtime and requires API-key credentials.'; } return ''; @@ -172,7 +155,6 @@ function getConnectionAlert(provider: CliProviderStatus): string | null { const authMode = provider.connection?.configuredAuthMode; const hasAnthropicSubscriptionSession = provider.authMethod === 'oauth_token' || provider.authMethod === 'claude.ai'; - const hasCodexSubscriptionSession = provider.authMethod === 'oauth_token'; if ( provider.providerId === 'anthropic' && @@ -198,26 +180,10 @@ function getConnectionAlert(provider: CliProviderStatus): string | null { return 'A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.'; } - if ( - provider.providerId === 'codex' && - authMode === 'api_key' && - !provider.connection?.apiKeyConfigured - ) { + if (provider.providerId === 'codex' && !provider.connection?.apiKeyConfigured) { return isCodexNativeLane(provider) - ? 'API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.' - : 'API key mode is selected, but no OPENAI_API_KEY credential is available yet.'; - } - - if (provider.providerId === 'codex' && authMode === 'oauth' && !hasCodexSubscriptionSession) { - return 'Codex subscription mode is selected. Sign in with Codex to use this provider.'; - } - - if ( - provider.providerId === 'codex' && - authMode === 'oauth' && - provider.connection?.apiKeySource === 'stored' - ) { - return 'A saved OPENAI_API_KEY is available, but Codex uses it only after you switch to API key mode.'; + ? 'No OPENAI_API_KEY or CODEX_API_KEY credential is available yet.' + : 'No OPENAI_API_KEY credential is available yet.'; } if ( @@ -253,22 +219,7 @@ function getConnectionMethodCardOptions( }, ]; case 'codex': - if (!provider.connection?.apiKeyBetaEnabled) { - return null; - } - - return [ - { - authMode: 'oauth', - title: 'Codex subscription', - description: 'Use your Codex sign-in session and subscription access.', - }, - { - authMode: 'api_key', - title: 'OpenAI API key', - description: 'Use OPENAI_API_KEY and OpenAI API billing.', - }, - ]; + return null; default: return null; } @@ -276,9 +227,7 @@ function getConnectionMethodCardOptions( function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null { if (provider.providerId === 'codex') { - return hasExplicitRuntimeBackends(provider) - ? 'Connection method controls credentials only. Runtime backend selection is independent.' - : 'Runtime follows your connection method automatically.'; + return 'Codex uses saved or environment API-key credentials for the native runtime.'; } if (provider.providerId === 'anthropic') { @@ -461,15 +410,6 @@ export const ProviderRuntimeSettingsDialog = ({ statusSelectedProvider.connection.configuredAuthMode; } - if (statusSelectedProvider.providerId === 'codex') { - nextConnection.configuredAuthMode = - appConfig?.providerConnections?.codex.authMode ?? - statusSelectedProvider.connection.configuredAuthMode; - nextConnection.apiKeyBetaEnabled = - appConfig?.providerConnections?.codex.apiKeyBetaEnabled ?? - statusSelectedProvider.connection.apiKeyBetaEnabled; - } - if (statusApiKeyConfig) { if (nextConnection.apiKeySource === 'stored') { nextConnection.apiKeyConfigured = Boolean(selectedApiKey); @@ -488,8 +428,6 @@ export const ProviderRuntimeSettingsDialog = ({ }; }, [ appConfig?.providerConnections?.anthropic.authMode, - appConfig?.providerConnections?.codex.apiKeyBetaEnabled, - appConfig?.providerConnections?.codex.authMode, selectedApiKey, statusApiKeyConfig, statusSelectedProvider, @@ -517,7 +455,10 @@ export const ProviderRuntimeSettingsDialog = ({ : false; const hideConnectionMethodMeta = showConnectionMethodCards; const canConfigureRuntime = - !connectionManagedRuntime && (selectedProvider?.availableBackends?.length ?? 0) > 0; + !connectionManagedRuntime && + (selectedProvider + ? getVisibleProviderRuntimeBackendOptions(selectedProvider).length > 1 + : false); const apiKeyConfig = selectedProvider && isApiKeyProviderId(selectedProvider.providerId) @@ -527,9 +468,9 @@ export const ProviderRuntimeSettingsDialog = ({ selectedProvider && isApiKeyProviderId(selectedProvider.providerId) && activeApiKeyFormProviderId === selectedProvider.providerId; - const codexApiKeyBetaEnabled = selectedProvider?.connection?.apiKeyBetaEnabled === true; const showApiKeySection = Boolean( - apiKeyConfig && (selectedProvider?.providerId !== 'codex' || codexApiKeyBetaEnabled) + apiKeyConfig && + (selectedProvider?.providerId !== 'codex' || !selectedProvider.connection?.supportsOAuth) ); const connectionAlert = selectedProvider ? getConnectionAlert(selectedProvider) : null; const connectionLoading = selectedProviderLoading || connectionSaving; @@ -541,9 +482,7 @@ export const ProviderRuntimeSettingsDialog = ({ const hasSubscriptionSession = selectedProvider?.providerId === 'anthropic' ? selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai' - : selectedProvider?.providerId === 'codex' - ? selectedProvider.authMethod === 'oauth_token' - : false; + : false; const canRequestSubscriptionLogin = Boolean(selectedProvider?.connection?.supportsOAuth && onRequestLogin) && configuredAuthMode !== 'api_key' && @@ -567,21 +506,6 @@ export const ProviderRuntimeSettingsDialog = ({ } if (connectionSaving) { - if (selectedProvider.providerId === 'codex') { - switch (pendingConnectionAction) { - case 'codex-beta-on': - return 'Enabling API key mode...'; - case 'codex-beta-off': - return 'Disabling API key mode...'; - case 'api_key': - return 'Switching to OpenAI API key...'; - case 'oauth': - return 'Switching to Codex subscription...'; - default: - return 'Applying connection changes...'; - } - } - if (selectedProvider.providerId === 'anthropic') { switch (pendingConnectionAction) { case 'api_key': @@ -679,7 +603,7 @@ export const ProviderRuntimeSettingsDialog = ({ }; const handleAuthModeChange = async (authMode: string): Promise => { - if (selectedProvider?.providerId !== 'anthropic' && selectedProvider?.providerId !== 'codex') { + if (selectedProvider?.providerId !== 'anthropic') { return; } @@ -692,54 +616,10 @@ export const ProviderRuntimeSettingsDialog = ({ setPendingConnectionAction(nextAuthMode); setConnectionError(null); let updateSucceeded = false; - try { - if (selectedProvider.providerId === 'anthropic') { - await updateConfig('providerConnections', { - anthropic: { - authMode: nextAuthMode, - }, - }); - } else { - await updateConfig('providerConnections', { - codex: { - authMode: nextAuthMode === 'api_key' ? 'api_key' : 'oauth', - }, - }); - } - updateSucceeded = true; - } catch (error) { - setConnectionError(error instanceof Error ? error.message : 'Failed to update connection'); - } finally { - if (updateSucceeded) { - try { - await onRefreshProvider?.(selectedProvider.providerId); - } catch { - setConnectionError('Connection updated, but failed to refresh provider status.'); - } - } - - setConnectionSaving(false); - setPendingConnectionAction(null); - } - }; - - const handleCodexBetaToggle = async (enabled: boolean): Promise => { - const fallbackApiKeyScope = selectedApiKey?.scope ?? 'user'; - const shouldOpenApiKeyForm = - enabled && - selectedProvider?.providerId === 'codex' && - !selectedProvider.connection?.apiKeyConfigured && - !selectedApiKey; - - setConnectionSaving(true); - setPendingConnectionAction(enabled ? 'codex-beta-on' : 'codex-beta-off'); - setConnectionError(null); - let updateSucceeded = false; try { await updateConfig('providerConnections', { - codex: { - apiKeyBetaEnabled: enabled, - authMode: enabled ? 'api_key' : 'oauth', + anthropic: { + authMode: nextAuthMode, }, }); updateSucceeded = true; @@ -747,15 +627,8 @@ export const ProviderRuntimeSettingsDialog = ({ setConnectionError(error instanceof Error ? error.message : 'Failed to update connection'); } finally { if (updateSucceeded) { - if (shouldOpenApiKeyForm) { - setActiveApiKeyFormProviderId('codex'); - setApiKeyScope(fallbackApiKeyScope); - setApiKeyValue(''); - setApiKeyError(null); - } - try { - await onRefreshProvider?.('codex'); + await onRefreshProvider?.(selectedProvider.providerId); } catch { setConnectionError('Connection updated, but failed to refresh provider status.'); } @@ -901,78 +774,12 @@ export const ProviderRuntimeSettingsDialog = ({ {selectedProvider.authenticated && (selectedProvider.authMethod === 'oauth_token' || selectedProvider.authMethod === 'claude.ai') - ? selectedProvider.providerId === 'codex' - ? 'Reconnect Codex' - : 'Reconnect Anthropic' + ? 'Reconnect Anthropic' : getProviderConnectLabel(selectedProvider)} ) : null} - {selectedProvider.providerId === 'codex' && - selectedProvider.connection?.apiKeyBetaAvailable && - !selectedProvider.connection.apiKeyBetaEnabled && - !showConnectionMethodCards ? ( -
-
-
-
- Codex subscription -
-
- Use your Codex sign-in session and subscription access. -
-
- Current -
-
-
-
- OpenAI API key (Beta) -
-
- Use OPENAI_API_KEY and OpenAI API billing for Codex. -
-
- -
-
-
-
- ) : null} - {showConnectionMethodCards ? (
@@ -1056,20 +863,6 @@ export const ProviderRuntimeSettingsDialog = ({ {selectedProvider.connection.apiKeySourceLabel} ) : null} - {selectedProvider.providerId === 'codex' && - selectedProvider.connection?.apiKeyBetaEnabled ? ( - - ) : null}
{showApiKeySection && apiKeyConfig ? ( diff --git a/src/renderer/components/runtime/providerConnectionUi.ts b/src/renderer/components/runtime/providerConnectionUi.ts index ec04902d..55619229 100644 --- a/src/renderer/components/runtime/providerConnectionUi.ts +++ b/src/renderer/components/runtime/providerConnectionUi.ts @@ -1,7 +1,6 @@ import type { CliProviderAuthMode, CliProviderStatus } from '@shared/types'; -const CODEX_SUBSCRIPTION_LABEL = 'Codex subscription'; -const CODEX_API_KEY_LABEL = 'OpenAI API key'; +const CODEX_NATIVE_LABEL = 'Codex native'; const ANTHROPIC_SUBSCRIPTION_LABEL = 'Anthropic subscription'; const AUTH_MODE_LABELS: Record = { @@ -23,7 +22,7 @@ export function formatProviderAuthModeLabelForProvider( } if (providerId === 'codex' && authMode === 'oauth') { - return CODEX_SUBSCRIPTION_LABEL; + return CODEX_NATIVE_LABEL; } if (providerId === 'anthropic' && authMode === 'oauth') { @@ -59,7 +58,7 @@ export function formatProviderAuthMethodLabelForProvider( authMethod: string | null ): string { if (providerId === 'codex' && authMethod === 'oauth_token') { - return CODEX_SUBSCRIPTION_LABEL; + return CODEX_NATIVE_LABEL; } if (providerId === 'anthropic' && (authMethod === 'oauth_token' || authMethod === 'claude.ai')) { @@ -95,23 +94,11 @@ function getSelectedRuntimeBackendOption( } export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean { - return provider.providerId === 'codex' && (provider.availableBackends?.length ?? 0) === 0; + return false; } function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string { - if (isCodexNativeLane(provider) && provider.backend?.label) { - return provider.backend.label; - } - - if (provider.authenticated) { - return provider.authMethod === 'api_key' ? CODEX_API_KEY_LABEL : CODEX_SUBSCRIPTION_LABEL; - } - - if (provider.connection?.configuredAuthMode === 'api_key') { - return CODEX_API_KEY_LABEL; - } - - return CODEX_SUBSCRIPTION_LABEL; + return provider.backend?.label ?? CODEX_NATIVE_LABEL; } export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null { @@ -126,6 +113,15 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s export function formatProviderStatusText(provider: CliProviderStatus): string { const selectedBackendOption = getSelectedRuntimeBackendOption(provider); + if (provider.providerId === 'codex') { + if (selectedBackendOption?.statusMessage) { + return selectedBackendOption.statusMessage; + } + return ( + provider.statusMessage ?? (provider.authenticated ? 'Codex native ready' : 'Not connected') + ); + } + if ( isCodexNativeLane(provider) && selectedBackendOption && @@ -168,11 +164,7 @@ export function getProviderConnectionModeSummary(provider: CliProviderStatus): s return null; } - if (provider.providerId === 'codex') { - return null; - } - - if (provider.providerId === 'anthropic' && provider.authenticated) { + if (provider.authenticated) { return null; } @@ -212,22 +204,10 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin : (provider.connection.apiKeySourceLabel ?? 'API key is configured'); } - if (provider.providerId === 'codex' && provider.connection?.apiKeyBetaEnabled !== true) { - if (isCodexNativeLane(provider)) { - return provider.connection.apiKeySource === 'stored' - ? 'Saved API key available in Manage' - : (provider.connection.apiKeySourceLabel ?? 'API key is configured'); - } - + if (provider.providerId === 'codex') { return provider.connection.apiKeySource === 'stored' - ? 'OpenAI API key is saved in Manage. Enable API key mode to use it.' - : 'OpenAI API key detected. Enable API key mode in Manage to use it.'; - } - - if (provider.authMethod !== 'api_key' && provider.providerId === 'codex') { - return provider.connection.apiKeySource === 'stored' - ? 'OpenAI API key is also configured in Manage' - : (provider.connection.apiKeySourceLabel ?? 'OpenAI API key is configured'); + ? 'Saved API key available in Manage' + : (provider.connection.apiKeySourceLabel ?? 'API key is configured'); } return provider.connection.apiKeySourceLabel ?? null; @@ -258,17 +238,6 @@ export function getProviderDisconnectAction(provider: CliProviderStatus): { }; } - if (provider.providerId === 'codex' && provider.authMethod === 'oauth_token') { - return { - label: 'Disconnect', - confirmLabel: 'Disconnect', - title: 'Disconnect Codex subscription?', - message: provider.connection?.apiKeyConfigured - ? 'This removes the local Codex subscription session from the Claude CLI runtime. Saved OPENAI_API_KEY credentials in Manage stay available.' - : 'This removes the local Codex subscription session from the Claude CLI runtime.', - }; - } - if (provider.providerId === 'gemini' && provider.authMethod === 'cli_oauth_personal') { return { label: 'Disconnect', @@ -288,7 +257,7 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string { } if (provider.providerId === 'codex') { - return 'Connect Codex'; + return 'Configure API key'; } if (provider.providerId === 'gemini') { @@ -299,6 +268,10 @@ export function getProviderConnectLabel(provider: CliProviderStatus): string { } export function shouldShowProviderConnectAction(provider: CliProviderStatus): boolean { + if (provider.providerId === 'codex') { + return false; + } + if (!provider.canLoginFromUi || provider.authenticated) { return false; } diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx index 597634b4..d9132190 100644 --- a/src/renderer/components/settings/sections/CliStatusSection.tsx +++ b/src/renderer/components/settings/sections/CliStatusSection.tsx @@ -109,7 +109,7 @@ function getProviderTerminalCommand(provider: CliProviderStatus): { return { args: ['auth', 'login', '--provider', provider.providerId], env: { - CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto', + CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native', }, }; } @@ -137,7 +137,7 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): { return { args: ['auth', 'logout', '--provider', provider.providerId], env: { - CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto', + CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'codex-native', }, }; } diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx index 757cd92d..b0eb827c 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx @@ -50,9 +50,7 @@ export function getProvisioningProviderBackendSummary( const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null; const inferredProviderId = provider.providerId ?? - (effectiveBackendId === 'codex-native' || - effectiveBackendId === 'adapter' || - options.some((option) => option.id === 'codex-native' || option.id === 'adapter') + (effectiveBackendId === 'codex-native' || options.some((option) => option.id === 'codex-native') ? 'codex' : undefined); const normalizedLabel = diff --git a/src/renderer/utils/providerBackendIdentity.ts b/src/renderer/utils/providerBackendIdentity.ts index 89287e3b..4bbdcd0d 100644 --- a/src/renderer/utils/providerBackendIdentity.ts +++ b/src/renderer/utils/providerBackendIdentity.ts @@ -1,3 +1,9 @@ +import { + formatProviderBackendLabel, + getDefaultProviderBackendId, + migrateProviderBackendId, +} from '@shared/utils/providerBackend'; + import type { CliProviderStatus, TeamProviderId } from '@shared/types'; function normalizeOptionalBackendId(value: string | null | undefined): string | undefined { @@ -5,22 +11,7 @@ function normalizeOptionalBackendId(value: string | null | undefined): string | return trimmed ? trimmed : undefined; } -export function getDefaultProviderBackendId( - providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined -): string | undefined { - return providerId === 'codex' ? 'codex-native' : undefined; -} - -export function isLegacyCodexProviderBackendId( - providerBackendId: string | null | undefined -): boolean { - const normalizedBackendId = normalizeOptionalBackendId(providerBackendId); - return ( - normalizedBackendId === 'auto' || - normalizedBackendId === 'adapter' || - normalizedBackendId === 'api' - ); -} +export { formatProviderBackendLabel, getDefaultProviderBackendId }; export function resolveEffectiveProviderBackendId( provider: Pick | null | undefined @@ -32,57 +23,10 @@ export function resolveUiOwnedProviderBackendId( providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined, provider: Pick | null | undefined ): string | undefined { - const normalizedProviderId = providerId ?? undefined; - if (normalizedProviderId === 'codex') { - const selectedBackendId = normalizeOptionalBackendId(provider?.selectedBackendId); - if (!selectedBackendId || selectedBackendId === 'auto') { - return 'codex-native'; - } - return selectedBackendId; - } - - return resolveEffectiveProviderBackendId(provider); -} - -export function formatProviderBackendLabel( - providerId: TeamProviderId | undefined, - providerBackendId: string | undefined -): string | undefined { - const normalizedProviderId = providerId ?? 'anthropic'; - const normalizedBackendId = normalizeOptionalBackendId(providerBackendId); - if (!normalizedBackendId) { - return undefined; - } - - if (normalizedProviderId === 'codex') { - switch (normalizedBackendId) { - case 'codex-native': - return 'Codex native'; - case 'adapter': - return 'Legacy adapter fallback'; - case 'api': - return 'Legacy OpenAI fallback'; - case 'auto': - return 'Legacy auto fallback'; - default: - return normalizedBackendId; - } - } - - if (normalizedProviderId === 'gemini') { - switch (normalizedBackendId) { - case 'cli-sdk': - return 'CLI SDK'; - case 'api': - return 'API'; - case 'auto': - return undefined; - default: - return normalizedBackendId; - } - } - - return normalizedBackendId; + return migrateProviderBackendId( + providerId, + provider?.selectedBackendId ?? provider?.resolvedBackendId + ); } export function formatTeamProviderBackendLabel( diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index 4f593561..5ac8d7b8 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -323,16 +323,8 @@ export function sortTeamProviderModels( export function isCodexChatGptSubscriptionProviderStatus( providerStatus?: RuntimeAwareProviderStatus | null ): boolean { - if (providerStatus?.providerId !== 'codex') { - return false; - } - - const endpointLabel = providerStatus.backend?.endpointLabel?.toLowerCase() ?? ''; - return ( - providerStatus.authMethod === 'oauth_token' && - (providerStatus.backend?.kind === 'adapter' || - endpointLabel.includes('chatgpt.com/backend-api/codex/responses')) - ); + void providerStatus; + return false; } function isRuntimeHiddenTeamModel( diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts index a5d7e63e..158b822e 100644 --- a/src/shared/types/notifications.ts +++ b/src/shared/types/notifications.ts @@ -337,7 +337,7 @@ export interface AppConfig { runtime: { providerBackends: { gemini: 'auto' | 'api' | 'cli-sdk'; - codex: 'auto' | 'adapter' | 'api' | 'codex-native'; + codex: 'codex-native'; }; }; /** Display and UI settings */ diff --git a/src/shared/utils/providerBackend.ts b/src/shared/utils/providerBackend.ts new file mode 100644 index 00000000..7d5c3ad0 --- /dev/null +++ b/src/shared/utils/providerBackend.ts @@ -0,0 +1,76 @@ +import type { TeamProviderId } from '@shared/types'; + +type RuntimeProviderId = TeamProviderId; + +function normalizeOptionalBackendId(value: unknown): string | undefined { + if (typeof value !== 'string') { + return undefined; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +export function getDefaultProviderBackendId( + providerId: TeamProviderId | RuntimeProviderId | undefined +): string | undefined { + return providerId === 'codex' ? 'codex-native' : undefined; +} + +export function isLegacyCodexProviderBackendId( + providerBackendId: string | null | undefined +): boolean { + const normalizedBackendId = normalizeOptionalBackendId(providerBackendId); + return ( + normalizedBackendId === 'auto' || + normalizedBackendId === 'adapter' || + normalizedBackendId === 'api' + ); +} + +export function migrateProviderBackendId( + providerId: TeamProviderId | RuntimeProviderId | undefined, + providerBackendId: string | null | undefined +): string | undefined { + const normalizedBackendId = normalizeOptionalBackendId(providerBackendId); + if (providerId !== 'codex') { + return normalizedBackendId; + } + + if (!normalizedBackendId || isLegacyCodexProviderBackendId(normalizedBackendId)) { + return 'codex-native'; + } + + return normalizedBackendId; +} + +export function formatProviderBackendLabel( + providerId: TeamProviderId | undefined, + providerBackendId: string | undefined +): string | undefined { + const normalizedBackendId = migrateProviderBackendId(providerId, providerBackendId); + if (!normalizedBackendId) { + return undefined; + } + + if ((providerId ?? 'anthropic') === 'codex') { + if (normalizedBackendId === 'codex-native') { + return 'Codex native'; + } + return normalizedBackendId; + } + + if ((providerId ?? 'anthropic') === 'gemini') { + switch (normalizedBackendId) { + case 'cli-sdk': + return 'CLI SDK'; + case 'api': + return 'API'; + case 'auto': + return undefined; + default: + return normalizedBackendId; + } + } + + return normalizedBackendId; +} diff --git a/test/main/ipc/configValidation.test.ts b/test/main/ipc/configValidation.test.ts index 0f4da5e8..01577536 100644 --- a/test/main/ipc/configValidation.test.ts +++ b/test/main/ipc/configValidation.test.ts @@ -240,7 +240,7 @@ describe('configValidation', () => { } }); - it('accepts Codex runtime backend updates for api and codex-native', () => { + it('normalizes legacy Codex runtime backend updates to codex-native', () => { const apiResult = validateConfigUpdatePayload('runtime', { providerBackends: { codex: 'api', @@ -251,7 +251,7 @@ describe('configValidation', () => { if (apiResult.valid) { expect(apiResult.data).toEqual({ providerBackends: { - codex: 'api', + codex: 'codex-native', }, }); } @@ -281,7 +281,7 @@ describe('configValidation', () => { expect(result.valid).toBe(false); if (!result.valid) { - expect(result.error).toContain('auto, adapter, api, codex-native'); + expect(result.error).toContain('runtime.providerBackends.codex must be one of: codex-native'); } }); }); diff --git a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts index fabf3d7a..fca9b397 100644 --- a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts +++ b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts @@ -325,31 +325,24 @@ describe('ClaudeMultimodelBridgeService', () => { codex: { supported: true, authenticated: true, - authMethod: 'oauth_token', + authMethod: 'api_key', verificationState: 'verified', - canLoginFromUi: true, - statusMessage: 'Codex native lane is wired but remains locked for normal selection.', - detailMessage: 'Use the fallback adapter/API lane unless the experimental native lane is explicitly enabled.', + canLoginFromUi: false, + statusMessage: 'Codex native runtime ready', + detailMessage: 'Codex native runtime is ready through the local codex exec seam.', selectedBackendId: 'codex-native', resolvedBackendId: 'codex-native', availableBackends: [ { - id: 'auto', - label: 'Auto', + id: 'codex-native', + label: 'Codex native', selectable: true, recommended: true, available: true, - }, - { - id: 'codex-native', - label: 'Codex native', - selectable: false, - recommended: false, - available: true, - state: 'locked', - audience: 'internal', - statusMessage: 'Experimental native lane', - detailMessage: 'Phase 0 keeps the lane locked behind rollout policy.', + state: 'ready', + audience: 'general', + statusMessage: 'Ready', + detailMessage: 'Codex native runtime is ready through the local codex exec seam.', }, ], externalRuntimeDiagnostics: [ @@ -368,7 +361,7 @@ describe('ClaudeMultimodelBridgeService', () => { plugins: { status: 'unsupported', ownership: 'shared', - reason: 'codex-native phase 0 keeps plugin execution disabled.', + reason: 'Plugins are not currently guaranteed for codex-native sessions in the multimodel runtime.', }, mcp: { status: 'unsupported', @@ -416,17 +409,13 @@ describe('ClaudeMultimodelBridgeService', () => { label: 'Codex native', }, availableBackends: [ - expect.objectContaining({ - id: 'auto', - selectable: true, - }), expect.objectContaining({ id: 'codex-native', - selectable: false, + selectable: true, available: true, - state: 'locked', - audience: 'internal', - statusMessage: 'Experimental native lane', + state: 'ready', + audience: 'general', + statusMessage: 'Ready', }), ], externalRuntimeDiagnostics: [ @@ -444,7 +433,7 @@ describe('ClaudeMultimodelBridgeService', () => { expect(getProviderCurrentRuntimeSummary(codex!)).toBeNull(); }); - it('preserves codex-native internal unlock readiness from runtime status payloads', async () => { + it('preserves codex-native ready truth from runtime status payloads', async () => { execCliMock.mockResolvedValue({ stdout: JSON.stringify({ providers: { @@ -461,12 +450,12 @@ describe('ClaudeMultimodelBridgeService', () => { id: 'codex-native', label: 'Codex native', selectable: true, - recommended: false, + recommended: true, available: true, state: 'ready', - audience: 'internal', - statusMessage: 'Ready for internal use', - detailMessage: 'Internal rollout only.', + audience: 'general', + statusMessage: 'Ready', + detailMessage: 'Codex native runtime is ready through the local codex exec seam.', }, ], capabilities: { @@ -502,8 +491,8 @@ describe('ClaudeMultimodelBridgeService', () => { selectable: true, available: true, state: 'ready', - audience: 'internal', - statusMessage: 'Ready for internal use', + audience: 'general', + statusMessage: 'Ready', }); }); @@ -517,8 +506,8 @@ describe('ClaudeMultimodelBridgeService', () => { authMethod: null, verificationState: 'unknown', canLoginFromUi: false, - statusMessage: 'Codex native runtime not ready', - detailMessage: 'Codex native runtime requires the codex CLI binary to be installed.', + statusMessage: 'Codex native runtime unavailable', + detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.', selectedBackendId: 'codex-native', resolvedBackendId: null, availableBackends: [ @@ -529,9 +518,9 @@ describe('ClaudeMultimodelBridgeService', () => { recommended: false, available: false, state: 'runtime-missing', - audience: 'internal', + audience: 'general', statusMessage: 'Codex CLI not found', - detailMessage: 'Install the codex CLI before enabling the lane.', + detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.', }, ], capabilities: { @@ -563,7 +552,7 @@ describe('ClaudeMultimodelBridgeService', () => { selectable: false, available: false, state: 'runtime-missing', - audience: 'internal', + audience: 'general', statusMessage: 'Codex CLI not found', }); }); diff --git a/test/main/services/runtime/ProviderConnectionService.test.ts b/test/main/services/runtime/ProviderConnectionService.test.ts index dc65a3a6..0127b015 100644 --- a/test/main/services/runtime/ProviderConnectionService.test.ts +++ b/test/main/services/runtime/ProviderConnectionService.test.ts @@ -11,28 +11,21 @@ describe('ProviderConnectionService', () => { const originalOpenAiApiKey = process.env.OPENAI_API_KEY; const originalCodexApiKey = process.env.CODEX_API_KEY; - function createConfig( - authMode: 'auto' | 'oauth' | 'api_key' = 'auto', - overrides?: { - codexAuthMode?: 'oauth' | 'api_key'; - codexApiKeyBetaEnabled?: boolean; - codexRuntimeBackend?: 'auto' | 'adapter' | 'api' | 'codex-native'; - } - ) { + function createConfig(authMode: 'auto' | 'oauth' | 'api_key' = 'auto') { return { providerConnections: { anthropic: { authMode, }, codex: { - apiKeyBetaEnabled: overrides?.codexApiKeyBetaEnabled ?? false, - authMode: overrides?.codexAuthMode ?? ('oauth' as const), + apiKeyBetaEnabled: false, + authMode: 'oauth' as const, }, }, runtime: { providerBackends: { gemini: 'auto' as const, - codex: overrides?.codexRuntimeBackend ?? ('auto' as const), + codex: 'codex-native' as const, }, }, }; @@ -55,10 +48,9 @@ describe('ProviderConnectionService', () => { if (originalCodexApiKey === undefined) { delete process.env.CODEX_API_KEY; - return; + } else { + process.env.CODEX_API_KEY = originalCodexApiKey; } - - process.env.CODEX_API_KEY = originalCodexApiKey; }); it('removes Anthropic environment credentials when OAuth mode is selected', async () => { @@ -116,30 +108,6 @@ describe('ProviderConnectionService', () => { expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); }); - it('does not treat ANTHROPIC_AUTH_TOKEN as an API key in api_key mode', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => createConfig('api_key'), - } as never - ); - - const result = await service.applyConfiguredConnectionEnv( - { - ANTHROPIC_AUTH_TOKEN: 'oauth-token', - }, - 'anthropic' - ); - - expect(result.ANTHROPIC_API_KEY).toBeUndefined(); - expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); - }); - it('reports a missing Anthropic API key when api_key mode is selected', async () => { const { ProviderConnectionService } = await import('@main/services/runtime/ProviderConnectionService'); @@ -159,57 +127,7 @@ describe('ProviderConnectionService', () => { expect(issue).toContain('ANTHROPIC_API_KEY'); }); - it('does not report a missing Anthropic API key once env is populated', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => createConfig('api_key'), - } as never - ); - - const issue = await service.getConfiguredConnectionIssue( - { - ANTHROPIC_API_KEY: 'env-key', - }, - 'anthropic' - ); - - expect(issue).toBeNull(); - }); - - it('augments PTY env with stored Anthropic API key without stripping auth token', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue({ - envVarName: 'ANTHROPIC_API_KEY', - value: 'stored-key', - }), - } as never, - { - getConfig: () => createConfig('api_key'), - } as never - ); - - const result = await service.augmentConfiguredConnectionEnv( - { - ANTHROPIC_AUTH_TOKEN: 'oauth-token', - }, - 'anthropic' - ); - - expect(result.ANTHROPIC_API_KEY).toBe('stored-key'); - expect(result.ANTHROPIC_AUTH_TOKEN).toBe('oauth-token'); - }); - - it('prefers stored API key status over environment detection', async () => { + it('prefers stored API key status over environment detection for Anthropic', async () => { getCachedShellEnvMock.mockReturnValue({ ANTHROPIC_API_KEY: 'shell-key', }); @@ -241,31 +159,7 @@ describe('ProviderConnectionService', () => { }); }); - it('does not report ANTHROPIC_AUTH_TOKEN as an API key credential source', async () => { - getCachedShellEnvMock.mockReturnValue({ - ANTHROPIC_AUTH_TOKEN: 'oauth-token', - }); - - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => createConfig('auto'), - } as never - ); - - const info = await service.getConnectionInfo('anthropic'); - - expect(info.apiKeyConfigured).toBe(false); - expect(info.apiKeySource).toBeNull(); - expect(info.apiKeySourceLabel).toBeNull(); - }); - - it('keeps Codex API key beta opt-in disabled by default', async () => { + it('exposes Codex as native-only API-key runtime', async () => { const { ProviderConnectionService } = await import('@main/services/runtime/ProviderConnectionService'); @@ -281,17 +175,19 @@ describe('ProviderConnectionService', () => { const info = await service.getConnectionInfo('codex'); expect(info).toMatchObject({ - supportsOAuth: true, + supportsOAuth: false, supportsApiKey: true, configurableAuthModes: [], configuredAuthMode: null, - apiKeyBetaAvailable: true, - apiKeyBetaEnabled: false, apiKeyConfigured: false, + apiKeySource: null, + apiKeySourceLabel: null, }); + expect(info.apiKeyBetaAvailable).toBeUndefined(); + expect(info.apiKeyBetaEnabled).toBeUndefined(); }); - it('injects OPENAI_API_KEY and selects the API backend when Codex API key mode is enabled', async () => { + it('mirrors a stored OpenAI key into CODEX_API_KEY for native Codex launches', async () => { const lookupPreferred = vi.fn().mockResolvedValue({ envVarName: 'OPENAI_API_KEY', value: 'openai-stored-key', @@ -304,31 +200,18 @@ describe('ProviderConnectionService', () => { lookupPreferred, } as never, { - getConfig: () => ({ - ...createConfig('auto', { - codexApiKeyBetaEnabled: true, - codexAuthMode: 'api_key', - codexRuntimeBackend: 'api', - }), - }), + getConfig: () => createConfig('auto'), } as never ); - const result = await service.applyConfiguredConnectionEnv( - { - OPENAI_API_KEY: undefined, - CLAUDE_CODE_CODEX_BACKEND: 'auto', - }, - 'codex' - ); + const result = await service.applyConfiguredConnectionEnv({}, 'codex'); expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY'); expect(result.OPENAI_API_KEY).toBe('openai-stored-key'); - expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto'); - expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1'); + expect(result.CODEX_API_KEY).toBe('openai-stored-key'); }); - it('keeps the configured Codex backend and strips OPENAI_API_KEY in oauth mode', async () => { + it('keeps ambient OpenAI credentials for native Codex launches', async () => { const { ProviderConnectionService } = await import('@main/services/runtime/ProviderConnectionService'); @@ -337,145 +220,22 @@ describe('ProviderConnectionService', () => { lookupPreferred: vi.fn().mockResolvedValue(null), } as never, { - getConfig: () => createConfig('auto', { - codexApiKeyBetaEnabled: true, - codexAuthMode: 'oauth', - codexRuntimeBackend: 'api', - }), + getConfig: () => createConfig('auto'), } as never ); const result = await service.applyConfiguredConnectionEnv( { OPENAI_API_KEY: 'shell-openai-key', - CLAUDE_CODE_CODEX_BACKEND: 'auto', }, 'codex' ); - expect(result.OPENAI_API_KEY).toBeUndefined(); - expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto'); - expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1'); + expect(result.OPENAI_API_KEY).toBe('shell-openai-key'); + expect(result.CODEX_API_KEY).toBe('shell-openai-key'); }); - it('reports a missing Codex API key when beta api_key mode is enabled', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => - createConfig('auto', { - codexApiKeyBetaEnabled: true, - codexAuthMode: 'api_key', - codexRuntimeBackend: 'api', - }), - } as never - ); - - const issue = await service.getConfiguredConnectionIssue({}, 'codex'); - - expect(issue).toContain('Codex API key mode is enabled'); - expect(issue).toContain('OPENAI_API_KEY'); - }); - - it('augments PTY env for Codex without rewriting the configured backend in oauth mode', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => - createConfig('auto', { - codexApiKeyBetaEnabled: true, - codexAuthMode: 'oauth', - codexRuntimeBackend: 'api', - }), - } as never - ); - - const result = await service.augmentConfiguredConnectionEnv( - { - OPENAI_API_KEY: 'shell-key', - }, - 'codex' - ); - - expect(result.OPENAI_API_KEY).toBe('shell-key'); - expect(result.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined(); - expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1'); - }); - - it('exposes Codex connection modes when codex-native is selected even without the old API beta toggle', async () => { - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred: vi.fn().mockResolvedValue(null), - } as never, - { - getConfig: () => - createConfig('auto', { - codexApiKeyBetaEnabled: false, - codexAuthMode: 'oauth', - codexRuntimeBackend: 'codex-native', - }), - } as never - ); - - const info = await service.getConnectionInfo('codex'); - - expect(info).toMatchObject({ - configurableAuthModes: ['oauth', 'api_key'], - configuredAuthMode: 'oauth', - apiKeyBetaEnabled: false, - }); - }); - - it('mirrors a stored OpenAI key into CODEX_API_KEY for codex-native without changing the selected backend', async () => { - const lookupPreferred = vi.fn().mockResolvedValue({ - envVarName: 'OPENAI_API_KEY', - value: 'openai-stored-key', - }); - const { ProviderConnectionService } = - await import('@main/services/runtime/ProviderConnectionService'); - - const service = new ProviderConnectionService( - { - lookupPreferred, - } as never, - { - getConfig: () => - createConfig('auto', { - codexApiKeyBetaEnabled: false, - codexAuthMode: 'api_key', - codexRuntimeBackend: 'codex-native', - }), - } as never - ); - - const result = await service.applyConfiguredConnectionEnv( - { - CLAUDE_CODE_CODEX_BACKEND: 'codex-native', - }, - 'codex' - ); - - expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY'); - expect(result.OPENAI_API_KEY).toBe('openai-stored-key'); - expect(result.CODEX_API_KEY).toBe('openai-stored-key'); - expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native'); - expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBeUndefined(); - }); - - it('accepts CODEX_API_KEY as the native external credential source for codex-native', async () => { + it('accepts CODEX_API_KEY as the native external credential source for Codex', async () => { getCachedShellEnvMock.mockReturnValue({ CODEX_API_KEY: 'native-key', }); @@ -488,12 +248,7 @@ describe('ProviderConnectionService', () => { lookupPreferred: vi.fn().mockResolvedValue(null), } as never, { - getConfig: () => - createConfig('auto', { - codexApiKeyBetaEnabled: false, - codexAuthMode: 'api_key', - codexRuntimeBackend: 'codex-native', - }), + getConfig: () => createConfig('auto'), } as never ); @@ -510,4 +265,46 @@ describe('ProviderConnectionService', () => { expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY'); expect(issue).toBeNull(); }); + + it('reports a missing native Codex credential when neither OPENAI_API_KEY nor CODEX_API_KEY exist', async () => { + const { ProviderConnectionService } = + await import('@main/services/runtime/ProviderConnectionService'); + + const service = new ProviderConnectionService( + { + lookupPreferred: vi.fn().mockResolvedValue(null), + } as never, + { + getConfig: () => createConfig('auto'), + } as never + ); + + const issue = await service.getConfiguredConnectionIssue({}, 'codex'); + + expect(issue).toContain('Codex native requires OPENAI_API_KEY or CODEX_API_KEY'); + }); + + it('augments PTY env for native Codex without dropping existing OpenAI credentials', async () => { + const { ProviderConnectionService } = + await import('@main/services/runtime/ProviderConnectionService'); + + const service = new ProviderConnectionService( + { + lookupPreferred: vi.fn().mockResolvedValue(null), + } as never, + { + getConfig: () => createConfig('auto'), + } as never + ); + + const result = await service.augmentConfiguredConnectionEnv( + { + OPENAI_API_KEY: 'shell-key', + }, + 'codex' + ); + + expect(result.OPENAI_API_KEY).toBe('shell-key'); + expect(result.CODEX_API_KEY).toBe('shell-key'); + }); }); diff --git a/test/main/services/runtime/providerAwareCliEnv.test.ts b/test/main/services/runtime/providerAwareCliEnv.test.ts index 666f9de5..81567dbe 100644 --- a/test/main/services/runtime/providerAwareCliEnv.test.ts +++ b/test/main/services/runtime/providerAwareCliEnv.test.ts @@ -25,7 +25,7 @@ vi.mock('../../../../src/main/services/infrastructure/ConfigManager', () => ({ runtime: { providerBackends: { gemini: 'cli', - codex: 'adapter', + codex: 'codex-native', }, }, }), @@ -185,17 +185,16 @@ describe('buildProviderAwareCliEnv', () => { expect(augmentAllConfiguredConnectionEnvMock).toHaveBeenCalledWith( expect.objectContaining({ CLAUDE_CODE_GEMINI_BACKEND: 'api', - CLAUDE_CODE_CODEX_BACKEND: 'adapter', + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', }) ); expect(result.env.CLAUDE_CODE_GEMINI_BACKEND).toBe('api'); - expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('adapter'); + expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native'); }); - it('preserves codex-native internal unlock env across provider-aware child env building', async () => { + it('preserves codex-native backend env across provider-aware child env building', async () => { buildEnrichedEnvMock.mockReturnValue({ PATH: '/usr/bin', - CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1', }); const { buildProviderAwareCliEnv } = await import( @@ -207,12 +206,11 @@ describe('buildProviderAwareCliEnv', () => { expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith( expect.objectContaining({ - CLAUDE_CODE_CODEX_BACKEND: 'adapter', - CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK: '1', + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', }), 'codex', undefined ); - expect(result.env.CLAUDE_CODE_CODEX_NATIVE_INTERNAL_UNLOCK).toBe('1'); + expect(result.env.CLAUDE_CODE_CODEX_BACKEND).toBe('codex-native'); }); }); diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 5a09ba9d..66177a74 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -1013,7 +1013,7 @@ describe('TeamProvisioningService', () => { expect(launchArgs).toContain(leadSessionId); }); - it('skips --resume when the persisted runtime backend lane changed', async () => { + it('keeps --resume when a persisted legacy Codex backend normalizes to codex-native', async () => { allowConsoleLogs(); const teamName = 'resume-backend-change-team'; const leadSessionId = 'lead-session-backend-change'; @@ -1066,8 +1066,8 @@ describe('TeamProvisioningService', () => { const launchArgs = vi.mocked(spawnCli).mock.calls.at(-1)?.[1] as string[]; expect(launchArgs).toBeTruthy(); - expect(launchArgs).not.toContain('--resume'); - expect(launchArgs).not.toContain(leadSessionId); + expect(launchArgs).toContain('--resume'); + expect(launchArgs).toContain(leadSessionId); }); it('seeds the current lead session id immediately when launch resumes an existing session', async () => { diff --git a/test/renderer/components/cli/CliStatusVisibility.test.ts b/test/renderer/components/cli/CliStatusVisibility.test.ts index ef8c7b37..4709c890 100644 --- a/test/renderer/components/cli/CliStatusVisibility.test.ts +++ b/test/renderer/components/cli/CliStatusVisibility.test.ts @@ -166,21 +166,21 @@ function createApiKeyMisconfiguredProvider( statusMessage: providerId === 'anthropic' ? 'Anthropic API key mode is enabled, but no ANTHROPIC_API_KEY is configured.' - : 'Codex API key mode is enabled, but no OPENAI_API_KEY is configured.', + : 'Codex native runtime requires OPENAI_API_KEY or CODEX_API_KEY.', models: [], - canLoginFromUi: true, + canLoginFromUi: providerId === 'anthropic', capabilities: { teamLaunch: true, oneShot: true, }, connection: { - supportsOAuth: true, + supportsOAuth: providerId === 'anthropic', supportsApiKey: true, configurableAuthModes: - providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : ['oauth', 'api_key'], - configuredAuthMode: 'api_key', - apiKeyBetaAvailable: providerId === 'codex' ? true : undefined, - apiKeyBetaEnabled: providerId === 'codex' ? true : undefined, + providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : [], + configuredAuthMode: providerId === 'anthropic' ? 'api_key' : null, + apiKeyBetaAvailable: undefined, + apiKeyBetaEnabled: undefined, apiKeyConfigured: false, apiKeySource: null, apiKeySourceLabel: null, @@ -194,22 +194,22 @@ function createApiKeyModeProviderIssue(providerId: 'anthropic' | 'codex'): Recor statusMessage: providerId === 'anthropic' ? 'Anthropic API key was rejected by the runtime.' - : 'OpenAI API key was rejected by the runtime.', + : 'Codex native runtime is unavailable because the configured API key was rejected.', connection: { ...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record }) .connection, apiKeyConfigured: true, apiKeySource: 'stored', apiKeySourceLabel: - providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored OpenAI API key', + providerId === 'anthropic' ? 'Stored Anthropic API key' : 'Stored Codex API key', }, }; } function createCodexNativeRolloutProvider( overrides?: Partial> & { - state?: 'ready' | 'locked' | 'authentication-required' | 'runtime-missing' | 'degraded'; - audience?: 'general' | 'internal'; + state?: 'ready' | 'authentication-required' | 'runtime-missing' | 'degraded'; + audience?: 'general'; selectable?: boolean; available?: boolean; statusMessage?: string | null; @@ -220,23 +220,16 @@ function createCodexNativeRolloutProvider( providerId: 'codex', displayName: 'Codex', supported: true, - authenticated: - overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true, - authMethod: - overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true - ? 'api_key' - : null, + authenticated: overrides?.state === 'ready' || overrides?.available === true, + authMethod: overrides?.state === 'ready' || overrides?.available === true ? 'api_key' : null, verificationState: - overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true - ? 'verified' - : 'unknown', - statusMessage: overrides?.statusMessage ?? 'Ready but locked', - detailMessage: overrides?.detailMessage ?? 'Internal rollout only.', + overrides?.state === 'ready' || overrides?.available === true ? 'verified' : 'unknown', + statusMessage: overrides?.statusMessage ?? 'Ready', + detailMessage: + overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.', selectedBackendId: 'codex-native', resolvedBackendId: - overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true - ? 'codex-native' - : null, + overrides?.state === 'ready' || overrides?.available === true ? 'codex-native' : null, models: ['gpt-5-codex'], canLoginFromUi: false, capabilities: { @@ -248,17 +241,18 @@ function createCodexNativeRolloutProvider( id: 'codex-native', label: 'Codex native', description: 'Use codex exec JSON mode.', - selectable: overrides?.selectable ?? false, - recommended: false, + selectable: overrides?.selectable ?? true, + recommended: true, available: overrides?.available ?? true, - state: overrides?.state ?? 'locked', - audience: overrides?.audience ?? 'internal', - statusMessage: overrides?.statusMessage ?? 'Ready but locked', - detailMessage: overrides?.detailMessage ?? 'Internal rollout only.', + state: overrides?.state ?? 'ready', + audience: overrides?.audience ?? 'general', + statusMessage: overrides?.statusMessage ?? 'Ready', + detailMessage: + overrides?.detailMessage ?? 'Codex native runtime is ready through the local codex exec seam.', }, ], backend: - overrides?.state === 'ready' || overrides?.state === 'locked' || overrides?.available === true + overrides?.state === 'ready' || overrides?.available === true ? { kind: 'codex-native', label: 'Codex native', @@ -403,12 +397,12 @@ describe('CLI status visibility during completed install state', () => { const onSelectBackend = providerRuntimeSettingsDialogProps?.onSelectBackend; expect(onSelectBackend).toBeTypeOf('function'); - await expect(onSelectBackend?.('codex', 'api')).rejects.toThrow( + await expect(onSelectBackend?.('codex', 'codex-native')).rejects.toThrow( 'Runtime updated, but failed to refresh provider status.' ); expect(storeState.updateConfig).toHaveBeenCalledWith('runtime', { providerBackends: { - codex: 'api', + codex: 'codex-native', }, }); expect(storeState.fetchCliProviderStatus).toHaveBeenCalledWith('codex'); @@ -770,7 +764,7 @@ describe('CLI status visibility during completed install state', () => { }); }); - it('shows runtime model availability badges on the dashboard', async () => { + it('shows runtime model availability badges on the dashboard without hiding native Codex models', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'idle'; storeState.cliStatus = createInstalledCliStatus({ @@ -786,7 +780,7 @@ describe('CLI status visibility during completed install state', () => { displayName: 'Codex', supported: true, authenticated: true, - authMethod: 'oauth_token', + authMethod: 'api_key', verificationState: 'verified', modelVerificationState: 'verified', statusMessage: null, @@ -806,15 +800,15 @@ describe('CLI status visibility during completed install state', () => { checkedAt: '2026-04-16T12:00:00.000Z', }, ], - canLoginFromUi: true, + canLoginFromUi: false, capabilities: { teamLaunch: true, oneShot: true, }, backend: { - kind: 'openai', - label: 'OpenAI', - endpointLabel: 'chatgpt.com/backend-api/codex/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, }, ], @@ -830,9 +824,9 @@ describe('CLI status visibility during completed install state', () => { }); expect(host.textContent).toContain('5.4'); - expect(host.textContent).not.toContain('5.1-codex-max'); + expect(host.textContent).toContain('5.1-codex-max'); expect(host.textContent).not.toContain('5.2-codex'); - expect(host.textContent).not.toContain('Unavailable'); + expect(host.textContent).toContain('Unavailable'); await act(async () => { root.unmount(); @@ -840,7 +834,7 @@ describe('CLI status visibility during completed install state', () => { }); }); - it('keeps dashboard codex-native rollout truth explicit for locked internal lanes', async () => { + it('keeps dashboard codex-native truth explicit for ready native lanes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'idle'; storeState.cliStatus = createInstalledCliStatus({ @@ -852,10 +846,12 @@ describe('CLI status visibility during completed install state', () => { authLoggedIn: true, providers: [ createCodexNativeRolloutProvider({ - state: 'locked', + state: 'ready', available: true, - selectable: false, - statusMessage: 'Ready but locked', + selectable: true, + audience: 'general', + statusMessage: 'Ready', + detailMessage: 'Codex native runtime is ready through the local codex exec seam.', }), ], }); @@ -869,8 +865,8 @@ describe('CLI status visibility during completed install state', () => { await Promise.resolve(); }); - expect(host.textContent).toContain('Ready but locked'); - expect(host.textContent).toContain('Runtime: Codex native - internal - locked'); + expect(host.textContent).toContain('Ready'); + expect(host.textContent).toContain('Runtime: Codex native'); expect(host.textContent).not.toContain('Connected via API key'); await act(async () => { @@ -898,7 +894,7 @@ describe('CLI status visibility during completed install state', () => { available: false, selectable: false, statusMessage: 'Codex CLI not found', - detailMessage: 'Install the codex CLI before enabling the lane.', + detailMessage: 'Codex native runtime requires the codex CLI binary to be installed and discoverable.', backend: null, resolvedBackendId: null, }), @@ -915,7 +911,7 @@ describe('CLI status visibility during completed install state', () => { }); expect(host.textContent).toContain('Codex CLI not found'); - expect(host.textContent).toContain('Runtime: Codex native - internal - runtime missing'); + expect(host.textContent).toContain('Runtime: Codex native - runtime missing'); expect(host.textContent).not.toContain('Connected via API key'); await act(async () => { diff --git a/test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts b/test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts index 48fa7810..fe136c57 100644 --- a/test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts +++ b/test/renderer/components/runtime/ProviderRuntimeBackendSelector.test.ts @@ -32,16 +32,6 @@ function createCodexProvider( selectedBackendId: 'codex-native', resolvedBackendId: 'codex-native', availableBackends: [ - { - id: 'auto', - label: 'Auto', - description: 'Automatically choose the best backend.', - selectable: true, - recommended: true, - available: true, - state: 'ready', - audience: 'general', - }, { id: 'codex-native', label: 'Codex native', @@ -109,29 +99,9 @@ describe('ProviderRuntimeBackendSelector helpers', () => { ); }); - it('hides codex legacy fallbacks from normal selectors when native is selected', () => { + it('shows the single native-only codex option after phase 4 migration', () => { const provider = createCodexProvider({ availableBackends: [ - { - id: 'auto', - label: 'Auto', - description: 'Automatically choose the best backend.', - selectable: true, - recommended: false, - available: true, - state: 'ready', - audience: 'general', - }, - { - id: 'api', - label: 'OpenAI API', - description: 'Legacy public Responses API fallback.', - selectable: true, - recommended: false, - available: true, - state: 'ready', - audience: 'internal', - }, { id: 'codex-native', label: 'Codex native', @@ -150,21 +120,11 @@ describe('ProviderRuntimeBackendSelector helpers', () => { ]); }); - it('keeps an explicitly selected legacy codex fallback readable during the soak', () => { + it('normalizes migrated legacy codex fallback rows to Codex native', () => { const provider = createCodexProvider({ - selectedBackendId: 'api', - resolvedBackendId: 'api', + selectedBackendId: 'codex-native', + resolvedBackendId: 'codex-native', availableBackends: [ - { - id: 'api', - label: 'OpenAI API', - description: 'Legacy public Responses API fallback.', - selectable: true, - recommended: false, - available: true, - state: 'ready', - audience: 'internal', - }, { id: 'codex-native', label: 'Codex native', @@ -179,10 +139,8 @@ describe('ProviderRuntimeBackendSelector helpers', () => { }); const visibleOptions = getVisibleProviderRuntimeBackendOptions(provider); - expect(visibleOptions.map((option) => option.id)).toEqual(['api', 'codex-native']); - expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe( - 'Legacy OpenAI fallback' - ); - expect(getProviderRuntimeBackendSummary(provider)).toBe('Legacy OpenAI fallback - internal'); + expect(visibleOptions.map((option) => option.id)).toEqual(['codex-native']); + expect(getOptionDisplayLabel(provider, visibleOptions[0], null)).toBe('Codex native'); + expect(getProviderRuntimeBackendSummary(provider)).toBe('Codex native'); }); }); diff --git a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts index 6e82d4cc..9995f624 100644 --- a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts +++ b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts @@ -138,6 +138,8 @@ vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({ 'Select runtime backend' ), getProviderRuntimeBackendSummary: () => null, + getVisibleProviderRuntimeBackendOptions: (provider: CliProviderStatus) => + provider.availableBackends ?? [], })); vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({ @@ -155,6 +157,10 @@ function createCodexProvider( overrides?: Partial & { authenticated?: boolean; authMethod?: string | null; + selectedBackendId?: string | null; + resolvedBackendId?: string | null; + availableBackends?: CliProviderStatus['availableBackends']; + canLoginFromUi?: boolean; } ): CliProviderStatus { return { @@ -162,30 +168,45 @@ function createCodexProvider( displayName: 'Codex', supported: true, authenticated: overrides?.authenticated ?? true, - authMethod: overrides?.authMethod ?? 'oauth_token', + authMethod: overrides?.authMethod ?? 'api_key', verificationState: 'verified', - statusMessage: 'Connected', + statusMessage: 'Codex native ready', models: ['gpt-5-codex'], - canLoginFromUi: true, + canLoginFromUi: overrides?.canLoginFromUi ?? false, capabilities: { teamLaunch: true, oneShot: true, extensions: createDefaultCliExtensionCapabilities(), }, - selectedBackendId: 'auto', - resolvedBackendId: 'adapter', - availableBackends: [], + selectedBackendId: overrides?.selectedBackendId ?? 'codex-native', + resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native', + availableBackends: + overrides?.availableBackends ?? + [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use the local codex exec JSON seam.', + selectable: true, + recommended: true, + available: true, + state: 'ready', + audience: 'general', + statusMessage: 'Codex native ready', + }, + ], + externalRuntimeDiagnostics: [], backend: { - kind: 'adapter', - label: 'Codex subscription', + kind: 'codex-native', + label: 'Codex native', }, connection: { - supportsOAuth: true, + supportsOAuth: false, supportsApiKey: true, - configurableAuthModes: overrides?.apiKeyBetaEnabled ? ['oauth', 'api_key'] : [], - configuredAuthMode: overrides?.configuredAuthMode ?? null, - apiKeyBetaAvailable: true, - apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? false, + configurableAuthModes: [], + configuredAuthMode: null, + apiKeyBetaAvailable: undefined, + apiKeyBetaEnabled: undefined, apiKeyConfigured: overrides?.apiKeyConfigured ?? false, apiKeySource: overrides?.apiKeySource ?? null, apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null, @@ -217,6 +238,7 @@ function createAnthropicProvider( selectedBackendId: null, resolvedBackendId: null, availableBackends: [], + externalRuntimeDiagnostics: [], backend: null, connection: { supportsOAuth: true, @@ -266,6 +288,7 @@ function createGeminiProvider(): CliProviderStatus { available: true, }, ], + externalRuntimeDiagnostics: [], backend: { kind: 'api', label: 'Gemini API', @@ -292,11 +315,7 @@ function findButtonByText(container: HTMLElement, text: string): HTMLButtonEleme return button; } -function countOccurrences(text: string, fragment: string): number { - return text.split(fragment).length - 1; -} - -describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { +describe('ProviderRuntimeSettingsDialog', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.appConfig = { @@ -346,139 +365,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { vi.unstubAllGlobals(); }); - it('switches Codex into api_key mode when enabling API key mode', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.resolve(undefined)); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createCodexProvider({ - apiKeyBetaEnabled: false, - configuredAuthMode: null, - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from OPENAI_API_KEY', - }), - ], - initialProviderId: 'codex', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Enable API key mode').click(); - await Promise.resolve(); - }); - - expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', { - codex: { - apiKeyBetaEnabled: true, - authMode: 'api_key', - }, - }); - expect(onRefreshProvider).toHaveBeenCalledWith('codex'); - }); - - it('shows a loading message while switching Codex to OpenAI API key', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - let resolveUpdate: (() => void) | null = null; - storeState.appConfig.providerConnections.codex = { - apiKeyBetaEnabled: true, - authMode: 'oauth', - }; - storeState.updateConfig = vi.fn( - () => - new Promise((resolve) => { - resolveUpdate = resolve; - }) - ); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createCodexProvider({ - apiKeyBetaEnabled: true, - configuredAuthMode: 'oauth', - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from OPENAI_API_KEY', - }), - ], - initialProviderId: 'codex', - onSelectBackend: vi.fn(), - onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'OpenAI API key').click(); - await Promise.resolve(); - }); - - expect(host.textContent).toContain('Switching to OpenAI API key...'); - expect(host.textContent).toContain('Switching...'); - - await act(async () => { - resolveUpdate?.(); - await Promise.resolve(); - }); - }); - - it('removes duplicate Codex summary and API key source text when connection cards are visible', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - storeState.appConfig.providerConnections.codex = { - apiKeyBetaEnabled: true, - authMode: 'oauth', - }; - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createCodexProvider({ - apiKeyBetaEnabled: true, - configuredAuthMode: 'oauth', - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from OPENAI_API_KEY', - }), - ], - initialProviderId: 'codex', - onSelectBackend: vi.fn(), - onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), - }) - ); - await Promise.resolve(); - }); - - expect(host.textContent).not.toContain('Current runtime: Codex subscription'); - expect(host.textContent).not.toContain('Mode: Codex subscription'); - expect(host.textContent).not.toContain('Runtime: Default adapter'); - expect(countOccurrences(host.textContent ?? '', 'Using Codex subscription')).toBe(0); - expect(countOccurrences(host.textContent ?? '', 'Detected from OPENAI_API_KEY')).toBe(1); - expect(host.textContent).not.toContain('Connected'); - }); - it('renders provider logos inside the provider tabs', async () => { const host = document.createElement('div'); document.body.appendChild(host); @@ -502,10 +388,11 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { expect(host.querySelector('[data-testid="provider-logo-codex"]')).not.toBeNull(); }); - it('renders Anthropics connection methods as cards and hides the empty runtime section', async () => { + it('renders anthropic connection cards and can switch to API key mode', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); + const onRefreshProvider = vi.fn(() => Promise.resolve(undefined)); await act(async () => { root.render( @@ -516,27 +403,70 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { createAnthropicProvider({ configuredAuthMode: 'auto', apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY', + apiKeySource: 'stored', + apiKeySourceLabel: 'Stored in app', }), ], initialProviderId: 'anthropic', onSelectBackend: vi.fn(), - onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), + onRefreshProvider, }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Connection method'); - expect(host.textContent).toContain('Auto'); expect(host.textContent).toContain('Anthropic subscription'); expect(host.textContent).toContain('API key'); - expect(host.textContent).not.toContain('Authentication method'); - expect(host.textContent).not.toContain('Runtime backend is not configurable'); - expect(host.textContent).not.toContain('Mode: Auto'); - expect(countOccurrences(host.textContent ?? '', 'Using Anthropic subscription')).toBe(1); - expect(countOccurrences(host.textContent ?? '', 'Detected from ANTHROPIC_API_KEY')).toBe(1); + + await act(async () => { + findButtonByText(host, 'API key').click(); + await Promise.resolve(); + }); + + expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', { + anthropic: { + authMode: 'api_key', + }, + }); + expect(onRefreshProvider).toHaveBeenCalledWith('anthropic'); + }); + + it('shows native-only Codex connection copy and API-key management without login actions', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(ProviderRuntimeSettingsDialog, { + open: true, + onOpenChange: vi.fn(), + providers: [ + createCodexProvider({ + authenticated: false, + authMethod: null, + apiKeyConfigured: true, + apiKeySource: 'stored', + apiKeySourceLabel: 'Stored in app', + }), + ], + initialProviderId: 'codex', + onSelectBackend: vi.fn(), + onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), + onRequestLogin: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain( + 'Codex launches always use the native runtime now. Manage API-key credentials here before launching teams or one-shot Codex runs.' + ); + expect(host.textContent).toContain('Set API key'); + expect(host.textContent).not.toContain('Connection method'); + expect(host.textContent).not.toContain('Connect Codex'); + expect(host.textContent).not.toContain('Reconnect Codex'); }); it('keeps the API key icon container square', async () => { @@ -565,79 +495,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { expect(icon?.className).toContain('shrink-0'); }); - it('switches Anthropic to API key mode from the connection cards', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.resolve(undefined)); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - configuredAuthMode: 'auto', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'API key').click(); - await Promise.resolve(); - }); - - expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', { - anthropic: { - authMode: 'api_key', - }, - }); - expect(onRefreshProvider).toHaveBeenCalledWith('anthropic'); - }); - - it('does not show Connect Anthropic when Auto is already authenticated via API key', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - authenticated: true, - authMethod: 'api_key', - configuredAuthMode: 'auto', - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider: vi.fn(() => Promise.resolve(undefined)), - onRequestLogin: vi.fn(), - }) - ); - await Promise.resolve(); - }); - - expect(host.textContent).not.toContain('Connect Anthropic'); - expect(host.textContent).not.toContain('Reconnect Anthropic'); - }); - it('keeps the API key form open and shows an error when delete fails', async () => { const host = document.createElement('div'); document.body.appendChild(host); @@ -646,10 +503,10 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { storeState.apiKeys = [ { id: 'key-1', - envVarName: 'ANTHROPIC_API_KEY', + envVarName: 'OPENAI_API_KEY', scope: 'user', - name: 'Anthropic API Key', - maskedValue: 'sk-ant-...1234', + name: 'OpenAI API Key', + maskedValue: 'sk-proj-...1234', createdAt: Date.now(), }, ]; @@ -661,14 +518,13 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { open: true, onOpenChange: vi.fn(), providers: [ - createAnthropicProvider({ - configuredAuthMode: 'api_key', + createCodexProvider({ apiKeyConfigured: true, apiKeySource: 'stored', apiKeySourceLabel: 'Stored in app', }), ], - initialProviderId: 'anthropic', + initialProviderId: 'codex', onSelectBackend: vi.fn(), onRefreshProvider, }) @@ -691,289 +547,6 @@ describe('ProviderRuntimeSettingsDialog Codex connection flows', () => { expect(onRefreshProvider).not.toHaveBeenCalled(); }); - it('shows a deleted stored key as removed even if provider refresh fails afterwards', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed'))); - storeState.apiKeys = [ - { - id: 'key-1', - envVarName: 'ANTHROPIC_API_KEY', - scope: 'user', - name: 'Anthropic API Key', - maskedValue: 'sk-ant-...1234', - createdAt: Date.now(), - }, - ]; - storeState.deleteApiKey = vi.fn((id: string) => { - storeState.apiKeys = storeState.apiKeys.filter((entry) => entry.id !== id); - return Promise.resolve(undefined); - }); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - configuredAuthMode: 'api_key', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Replace key').click(); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Delete').click(); - await Promise.resolve(); - }); - - expect(host.textContent).toContain('API key deleted, but failed to refresh provider status.'); - expect(host.textContent).toContain('Not configured'); - expect(host.textContent).not.toContain('sk-ant-...1234'); - }); - - it('shows a connection error and skips refresh when auth mode update fails', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.resolve(undefined)); - storeState.updateConfig = vi.fn(() => Promise.reject(new Error('Config update failed'))); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - configuredAuthMode: 'auto', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'API key').click(); - await Promise.resolve(); - }); - - expect(host.textContent).toContain('Config update failed'); - expect(host.textContent).not.toContain('Switching to API key...'); - expect(host.textContent).not.toContain('Switching...'); - expect(onRefreshProvider).not.toHaveBeenCalled(); - }); - - it('clears Codex beta loading state when enabling API key mode fails early', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.resolve(undefined)); - storeState.updateConfig = vi.fn(() => Promise.reject(new Error('Config update failed'))); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createCodexProvider({ - apiKeyBetaEnabled: false, - configuredAuthMode: null, - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'codex', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Enable API key mode').click(); - await Promise.resolve(); - }); - - expect(host.textContent).toContain('Config update failed'); - expect(host.textContent).not.toContain('Enabling API key mode...'); - expect(onRefreshProvider).not.toHaveBeenCalled(); - }); - - it('reports refresh failures separately after a successful auth mode update', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed'))); - storeState.updateConfig = vi.fn((section: string, data: Record) => { - if (section === 'providerConnections') { - const nextProviderConnections = data as Partial; - storeState.appConfig = { - ...storeState.appConfig, - providerConnections: { - anthropic: { - ...storeState.appConfig.providerConnections.anthropic, - ...(nextProviderConnections.anthropic ?? {}), - }, - codex: { - ...storeState.appConfig.providerConnections.codex, - ...(nextProviderConnections.codex ?? {}), - }, - }, - }; - } - - return Promise.resolve(undefined); - }); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - configuredAuthMode: 'auto', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'API key').click(); - await Promise.resolve(); - }); - - expect(storeState.updateConfig).toHaveBeenCalled(); - expect(onRefreshProvider).toHaveBeenCalledWith('anthropic'); - expect(host.textContent).not.toContain('Mode: API key'); - expect(host.textContent).toContain('API keySelected'); - expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.'); - expect(host.textContent).not.toContain('Failed to update connection'); - }); - - it('shows subscription recovery actions when OAuth mode is selected but stale status still says API key', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed'))); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createAnthropicProvider({ - authenticated: true, - authMethod: 'api_key', - configuredAuthMode: 'auto', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'anthropic', - onSelectBackend: vi.fn(), - onRefreshProvider, - onRequestLogin: vi.fn(), - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Anthropic subscription').click(); - await Promise.resolve(); - }); - - expect(host.textContent).not.toContain('Mode: Anthropic subscription'); - expect(host.textContent).toContain('Anthropic subscriptionSelected'); - expect(host.textContent).toContain('Connect Anthropic'); - expect(host.textContent).toContain( - 'Anthropic subscription mode is selected. Sign in with Anthropic to use this provider.' - ); - expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.'); - }); - - it('keeps the Codex API key mode UI in sync with config when refresh fails after enabling beta', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const onRefreshProvider = vi.fn(() => Promise.reject(new Error('refresh failed'))); - - await act(async () => { - root.render( - React.createElement(ProviderRuntimeSettingsDialog, { - open: true, - onOpenChange: vi.fn(), - providers: [ - createCodexProvider({ - apiKeyBetaEnabled: false, - configuredAuthMode: null, - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }), - ], - initialProviderId: 'codex', - onSelectBackend: vi.fn(), - onRefreshProvider, - }) - ); - await Promise.resolve(); - }); - - await act(async () => { - findButtonByText(host, 'Enable API key mode').click(); - await Promise.resolve(); - }); - - expect(storeState.updateConfig).toHaveBeenCalledWith('providerConnections', { - codex: { - apiKeyBetaEnabled: true, - authMode: 'api_key', - }, - }); - expect(host.textContent).not.toContain('Mode: API key'); - expect(host.textContent).toContain('Selected'); - expect(host.textContent).toContain('Disable API key mode'); - expect(host.textContent).toContain('Connection updated, but failed to refresh provider status.'); - }); - it('shows a runtime error when backend selection refresh fails after a successful update', async () => { const host = document.createElement('div'); document.body.appendChild(host); diff --git a/test/renderer/components/runtime/providerConnectionUi.test.ts b/test/renderer/components/runtime/providerConnectionUi.test.ts index ae759a6f..6b2a37eb 100644 --- a/test/renderer/components/runtime/providerConnectionUi.test.ts +++ b/test/renderer/components/runtime/providerConnectionUi.test.ts @@ -6,6 +6,7 @@ import { getProviderCredentialSummary, getProviderCurrentRuntimeSummary, isConnectionManagedRuntimeProvider, + shouldShowProviderConnectAction, } from '@renderer/components/runtime/providerConnectionUi'; import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; @@ -57,6 +58,8 @@ function createCodexProvider( resolvedBackendId?: string | null; availableBackends?: CliProviderStatus['availableBackends']; backend?: CliProviderStatus['backend']; + statusMessage?: string | null; + canLoginFromUi?: boolean; } ): CliProviderStatus { return { @@ -64,33 +67,45 @@ function createCodexProvider( displayName: 'Codex', supported: true, authenticated: overrides?.authenticated ?? true, - authMethod: overrides?.authMethod ?? 'oauth_token', + authMethod: overrides?.authMethod ?? 'api_key', verificationState: 'verified', - statusMessage: 'Connected', + statusMessage: overrides?.statusMessage ?? 'Codex native ready', models: ['gpt-5-codex'], - canLoginFromUi: true, + canLoginFromUi: overrides?.canLoginFromUi ?? false, capabilities: { teamLaunch: true, oneShot: true, extensions: createDefaultCliExtensionCapabilities(), }, - selectedBackendId: overrides?.selectedBackendId ?? 'auto', - resolvedBackendId: overrides?.resolvedBackendId ?? 'adapter', - availableBackends: overrides?.availableBackends ?? [], + selectedBackendId: overrides?.selectedBackendId ?? 'codex-native', + resolvedBackendId: overrides?.resolvedBackendId ?? 'codex-native', + availableBackends: + overrides?.availableBackends ?? + [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: true, + recommended: true, + available: true, + state: 'ready', + audience: 'general', + statusMessage: 'Codex native ready', + }, + ], externalRuntimeDiagnostics: [], backend: overrides?.backend ?? ({ - kind: 'adapter', - label: 'Codex subscription', + kind: 'codex-native', + label: 'Codex native', } satisfies NonNullable), connection: { - supportsOAuth: true, + supportsOAuth: false, supportsApiKey: true, - configurableAuthModes: ['oauth', 'api_key'], - configuredAuthMode: overrides?.configuredAuthMode ?? 'oauth', - apiKeyBetaAvailable: true, - apiKeyBetaEnabled: overrides?.apiKeyBetaEnabled ?? true, + configurableAuthModes: [], + configuredAuthMode: overrides?.configuredAuthMode ?? null, apiKeyConfigured: overrides?.apiKeyConfigured ?? false, apiKeySource: overrides?.apiKeySource ?? null, apiKeySourceLabel: overrides?.apiKeySourceLabel ?? null, @@ -124,202 +139,62 @@ describe('providerConnectionUi', () => { ); }); - it('prefers the actual Codex runtime once the provider is already authenticated', () => { + it('treats Codex as lane-managed and hides the old connection-managed runtime summary', () => { const provider = createCodexProvider({ - authenticated: true, - authMethod: 'oauth_token', - configuredAuthMode: 'api_key', apiKeyConfigured: true, apiKeySource: 'stored', apiKeySourceLabel: 'Stored in app', }); - expect(getProviderCurrentRuntimeSummary(provider)).toBe( - 'Current runtime: Codex subscription' - ); - }); - - it('shows the selected Codex runtime when the provider is not authenticated yet', () => { - const provider = createCodexProvider({ - authenticated: false, - authMethod: null, - configuredAuthMode: 'api_key', - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }); - - expect(getProviderCurrentRuntimeSummary(provider)).toBe('Selected runtime: OpenAI API key'); - }); - - it('reports an environment Anthropic API key without claiming it is stored in Manage', () => { - const provider = createAnthropicProvider({ - authenticated: true, - authMethod: 'oauth_token', - configuredAuthMode: 'oauth', - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from ANTHROPIC_API_KEY', - }); - - expect(getProviderCredentialSummary(provider)).toBe('Detected from ANTHROPIC_API_KEY'); - }); - - it('reports an environment Codex API key without claiming it is stored in Manage', () => { - const provider = createCodexProvider({ - authenticated: true, - authMethod: 'oauth_token', - configuredAuthMode: 'oauth', - apiKeyConfigured: true, - apiKeySource: 'environment', - apiKeySourceLabel: 'Detected from OPENAI_API_KEY', - }); - - expect(getProviderCredentialSummary(provider)).toBe('Detected from OPENAI_API_KEY'); - }); - - it('tells the user when a stored Codex key exists but API key mode is still disabled', () => { - const provider = createCodexProvider({ - authenticated: true, - authMethod: 'oauth_token', - configuredAuthMode: 'oauth', - apiKeyBetaEnabled: false, - apiKeyConfigured: true, - apiKeySource: 'stored', - apiKeySourceLabel: 'Stored in app', - }); - - expect(getProviderCredentialSummary(provider)).toBe( - 'OpenAI API key is saved in Manage. Enable API key mode to use it.' - ); - }); - - it('treats Codex as lane-managed once explicit backend options exist', () => { - const provider = createCodexProvider({ - availableBackends: [ - { - id: 'auto', - label: 'Auto', - description: 'Automatically choose the best backend.', - selectable: true, - recommended: true, - available: true, - }, - { - id: 'codex-native', - label: 'Codex native', - description: 'Use codex exec JSON mode.', - selectable: true, - recommended: false, - available: true, - }, - ], - selectedBackendId: 'codex-native', - resolvedBackendId: 'codex-native', - backend: { - kind: 'codex-native', - label: 'Codex native', - }, - }); - expect(isConnectionManagedRuntimeProvider(provider)).toBe(false); expect(getProviderCurrentRuntimeSummary(provider)).toBeNull(); }); - it('does not tell the user to enable API key mode when codex-native is already selected', () => { + it('shows stored Codex API keys as immediately usable for native runtime', () => { const provider = createCodexProvider({ - apiKeyBetaEnabled: false, - configuredAuthMode: 'api_key', apiKeyConfigured: true, apiKeySource: 'stored', apiKeySourceLabel: 'Stored in app', - selectedBackendId: 'codex-native', - resolvedBackendId: 'codex-native', - availableBackends: [ - { - id: 'codex-native', - label: 'Codex native', - description: 'Use codex exec JSON mode.', - selectable: true, - recommended: false, - available: true, - }, - ], - backend: { - kind: 'codex-native', - label: 'Codex native', - }, }); expect(getProviderCredentialSummary(provider)).toBe('Saved API key available in Manage'); }); - it('keeps locked codex-native lanes visible instead of flattening them to connected-via-api-key', () => { + it('shows environment Codex credentials without claiming they are stored in Manage', () => { const provider = createCodexProvider({ - authenticated: true, - authMethod: 'api_key', - statusMessage: 'Codex native runtime ready', - selectedBackendId: 'codex-native', - resolvedBackendId: 'codex-native', - availableBackends: [ - { - id: 'codex-native', - label: 'Codex native', - description: 'Use codex exec JSON mode.', - selectable: false, - recommended: false, - available: true, - state: 'locked', - audience: 'internal', - statusMessage: 'Ready but locked', - detailMessage: 'Internal rollout only.', - }, - ], - backend: { - kind: 'codex-native', - label: 'Codex native', - }, + apiKeyConfigured: true, + apiKeySource: 'environment', + apiKeySourceLabel: 'Detected from CODEX_API_KEY', }); - expect(formatProviderStatusText(provider)).toBe('Ready but locked'); + expect(getProviderCredentialSummary(provider)).toBe('Detected from CODEX_API_KEY'); }); - it('keeps internal codex-native ready state explicit instead of showing a generic auth label', () => { + it('surfaces native backend status instead of flattening Codex to connected-via-api-key text', () => { const provider = createCodexProvider({ - authenticated: true, - authMethod: 'api_key', - statusMessage: 'Codex native runtime ready', - selectedBackendId: 'codex-native', - resolvedBackendId: 'codex-native', availableBackends: [ { id: 'codex-native', label: 'Codex native', description: 'Use codex exec JSON mode.', selectable: true, - recommended: false, + recommended: true, available: true, state: 'ready', - audience: 'internal', - statusMessage: 'Ready for internal use', - detailMessage: 'Internal rollout only.', + audience: 'general', + statusMessage: 'Codex native ready', }, ], - backend: { - kind: 'codex-native', - label: 'Codex native', - }, }); - expect(formatProviderStatusText(provider)).toBe('Ready for internal use'); + expect(formatProviderStatusText(provider)).toBe('Codex native ready'); }); it('surfaces native auth-required state from the selected backend option', () => { const provider = createCodexProvider({ authenticated: false, authMethod: null, - statusMessage: 'Codex native runtime not ready', - selectedBackendId: 'codex-native', + statusMessage: 'Codex native not ready', resolvedBackendId: null, availableBackends: [ { @@ -327,10 +202,10 @@ describe('providerConnectionUi', () => { label: 'Codex native', description: 'Use codex exec JSON mode.', selectable: false, - recommended: false, + recommended: true, available: false, state: 'authentication-required', - audience: 'internal', + audience: 'general', statusMessage: 'Authentication required', detailMessage: 'Set CODEX_API_KEY.', }, @@ -341,30 +216,13 @@ describe('providerConnectionUi', () => { expect(formatProviderStatusText(provider)).toBe('Authentication required'); }); - it('surfaces native runtime-missing state from the selected backend option', () => { + it('never shows a Connect action for Codex after the native-only cutover', () => { const provider = createCodexProvider({ authenticated: false, authMethod: null, - statusMessage: 'Codex native runtime not ready', - selectedBackendId: 'codex-native', - resolvedBackendId: null, - availableBackends: [ - { - id: 'codex-native', - label: 'Codex native', - description: 'Use codex exec JSON mode.', - selectable: false, - recommended: false, - available: false, - state: 'runtime-missing', - audience: 'internal', - statusMessage: 'Codex CLI not found', - detailMessage: 'Install the codex CLI before enabling the lane.', - }, - ], - backend: null, + canLoginFromUi: false, }); - expect(formatProviderStatusText(provider)).toBe('Codex CLI not found'); + expect(shouldShowProviderConnectAction(provider)).toBe(false); }); }); diff --git a/test/renderer/components/team/TeamModelSelector.test.ts b/test/renderer/components/team/TeamModelSelector.test.ts index d49ac1c9..1e4bd1ea 100644 --- a/test/renderer/components/team/TeamModelSelector.test.ts +++ b/test/renderer/components/team/TeamModelSelector.test.ts @@ -6,7 +6,6 @@ import { } from '@renderer/components/team/dialogs/TeamModelSelector'; import { GPT_5_1_CODEX_MINI_UI_DISABLED_REASON, - GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON, GPT_5_2_CODEX_UI_DISABLED_REASON, GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON, getAvailableTeamProviderModels, @@ -49,15 +48,15 @@ describe('formatTeamModelSummary', () => { expect(getTeamModelUiDisabledReason('anthropic', 'gpt-5.1-codex-mini')).toBeNull(); }); - it('disables 5.1 Codex Max only on the Codex ChatGPT subscription path', () => { - const chatgptCodexProviderStatus = { + it('keeps 5.1 Codex Max available on the native Codex path', () => { + const nativeCodexProviderStatus = { providerId: 'codex' as const, models: ['gpt-5.4', 'gpt-5.1-codex-max'], - authMethod: 'oauth_token' as const, + authMethod: 'api_key' as const, backend: { - kind: 'adapter', - label: 'Default adapter', - endpointLabel: 'chatgpt.com/backend-api/codex/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, modelVerificationState: 'verified' as const, modelAvailability: [], @@ -66,14 +65,14 @@ describe('formatTeamModelSummary', () => { }; expect( - getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus) - ).toBe(GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON); - expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus)).toBe( - '' + getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus) + ).toBeNull(); + expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus)).toBe( + 'gpt-5.1-codex-max' ); expect( - getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', chatgptCodexProviderStatus) - ).toContain('Temporarily disabled for team agents when using Codex ChatGPT subscription'); + getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', nativeCodexProviderStatus) + ).toBeNull(); expect(getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-max')).toBeNull(); }); @@ -88,11 +87,11 @@ describe('formatTeamModelSummary', () => { const codexProviderStatus = { providerId: 'codex' as const, models: ['gpt-5.4', 'gpt-5.3-codex'], - authMethod: 'oauth_token' as const, + authMethod: 'api_key' as const, backend: { - kind: 'adapter', - label: 'Default adapter', - endpointLabel: 'chatgpt.com/backend-api/codex/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, modelVerificationState: 'verified' as const, modelAvailability: [ diff --git a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts index 640cb4a6..449ad5c0 100644 --- a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts +++ b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts @@ -259,17 +259,17 @@ describe('TeamModelSelector disabled Codex models', () => { }); }); - it('shows 5.1 Codex Max as a disabled tile on the ChatGPT subscription path', async () => { + it('keeps 5.1 Codex Max selectable on the native Codex path', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliStatus = { providers: [ { providerId: 'codex', - authMethod: 'oauth_token', + authMethod: 'api_key', backend: { - kind: 'adapter', - label: 'Default adapter', - endpointLabel: 'chatgpt.com/backend-api/codex/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, models: ['gpt-5.4', 'gpt-5.1-codex-max'], modelVerificationState: 'idle', @@ -295,23 +295,20 @@ describe('TeamModelSelector disabled Codex models', () => { await Promise.resolve(); }); - const disabledButton = Array.from(host.querySelectorAll('button')).find((button) => + const button = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('5.1 Codex Max') ); - expect(disabledButton).not.toBeNull(); - expect(disabledButton?.getAttribute('aria-disabled')).toBe('true'); - expect(disabledButton?.textContent).toContain('Disabled'); - expect(disabledButton?.getAttribute('title')).toContain( - 'Not available with Codex ChatGPT subscription' - ); + expect(button).not.toBeNull(); + expect(button?.getAttribute('aria-disabled')).toBe('false'); + expect(button?.textContent).not.toContain('Disabled'); await act(async () => { - disabledButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + button?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await Promise.resolve(); }); - expect(onValueChange).not.toHaveBeenCalled(); + expect(onValueChange).toHaveBeenCalledWith('gpt-5.1-codex-max'); await act(async () => { root.unmount(); diff --git a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts index 1bb0927d..a4a8065e 100644 --- a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts +++ b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts @@ -51,7 +51,7 @@ describe('ProvisioningProviderStatusList', () => { { providerId: 'codex', status: 'failed', - backendSummary: 'Default adapter', + backendSummary: 'Codex native', details: [ '5.4 Mini - verified', '5.1 Codex Max - unavailable - Not available with Codex ChatGPT subscription', @@ -64,7 +64,7 @@ describe('ProvisioningProviderStatusList', () => { }); expect(host.textContent).toContain( - 'Codex (Default adapter): Selected model checks - 1 model unavailable, 1 verified' + 'Codex (Codex native): Selected model checks - 1 model unavailable, 1 verified' ); expect(host.textContent).toContain('5.4 Mini - verified'); expect(host.textContent).toContain( @@ -110,7 +110,7 @@ describe('ProvisioningProviderStatusList', () => { { providerId: 'codex', status: 'notes', - backendSummary: 'Default adapter', + backendSummary: 'Codex native', details: ['5.3 Codex - check failed - Model verification timed out'], }, ], @@ -120,7 +120,7 @@ describe('ProvisioningProviderStatusList', () => { }); expect(host.textContent).toContain( - 'Codex (Default adapter): Selected model checks - 1 model timed out' + 'Codex (Codex native): Selected model checks - 1 model timed out' ); expect(host.textContent).toContain('5.3 Codex - check failed - Model verification timed out'); @@ -143,7 +143,7 @@ describe('ProvisioningProviderStatusList', () => { { providerId: 'codex', status: 'notes', - backendSummary: 'Default adapter', + backendSummary: 'Codex native', details: [ 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence', ], @@ -154,7 +154,7 @@ describe('ProvisioningProviderStatusList', () => { await Promise.resolve(); }); - expect(host.textContent).toContain('Codex (Default adapter): CLI preflight did not complete'); + expect(host.textContent).toContain('Codex (Codex native): CLI preflight did not complete'); await act(async () => { root.unmount(); @@ -179,36 +179,26 @@ describe('ProvisioningProviderStatusList', () => { selectable: false, recommended: false, available: true, - state: 'locked', - audience: 'internal', - statusMessage: 'Ready but locked', + state: 'ready', + audience: 'general', + statusMessage: 'Ready', }, ], }) - ).toBe('Codex native - internal, locked'); + ).toBe('Codex native'); }); - it('marks explicit legacy codex fallback summaries as legacy during the soak', () => { + it('normalizes persisted legacy codex fallback summaries to Codex native', () => { expect( getProvisioningProviderBackendSummary({ providerId: 'codex', selectedBackendId: 'api', resolvedBackendId: 'api', backend: { - kind: 'api', - label: 'OpenAI API', + kind: 'codex-native', + label: 'Codex native', }, availableBackends: [ - { - id: 'api', - label: 'OpenAI API', - description: 'Legacy public Responses API fallback.', - selectable: true, - recommended: false, - available: true, - state: 'ready', - audience: 'internal', - }, { id: 'codex-native', label: 'Codex native', @@ -221,6 +211,6 @@ describe('ProvisioningProviderStatusList', () => { }, ], }) - ).toBe('Legacy OpenAI fallback - internal'); + ).toBe('Codex native'); }); }); diff --git a/test/renderer/components/team/dialogs/providerPrepareCacheKey.test.ts b/test/renderer/components/team/dialogs/providerPrepareCacheKey.test.ts index 46a7c78d..ef5aff67 100644 --- a/test/renderer/components/team/dialogs/providerPrepareCacheKey.test.ts +++ b/test/renderer/components/team/dialogs/providerPrepareCacheKey.test.ts @@ -27,7 +27,7 @@ describe('buildProviderPrepareModelCacheKey', () => { const input = { cwd: '/tmp/project', providerId: 'codex' as const, - backendSummary: 'Default adapter', + backendSummary: 'Codex native', limitContext: false, }; diff --git a/test/renderer/utils/memberRuntimeSummary.test.ts b/test/renderer/utils/memberRuntimeSummary.test.ts index 633c5fbf..baa7acd9 100644 --- a/test/renderer/utils/memberRuntimeSummary.test.ts +++ b/test/renderer/utils/memberRuntimeSummary.test.ts @@ -41,7 +41,7 @@ describe('resolveMemberRuntimeSummary', () => { const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true }); expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe( - 'Anthropic ยท Opus 4.7 ยท Medium' + 'Anthropic ยท Opus 4.7 ยท Medium ยท Codex native' ); }); @@ -61,7 +61,9 @@ describe('resolveMemberRuntimeSummary', () => { runtimeModel: 'gpt-5.4-mini', }); - expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe('5.4 Mini ยท Medium'); + expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe( + '5.4 Mini ยท Medium ยท Codex native' + ); }); it('appends runtime memory when a live process snapshot is available', () => { @@ -77,7 +79,7 @@ describe('resolveMemberRuntimeSummary', () => { }; expect(resolveMemberRuntimeSummary(member, undefined, undefined, runtimeEntry)).toBe( - '5.4 Mini ยท Medium ยท 256.0 MB' + '5.4 Mini ยท Medium ยท Codex native ยท 256.0 MB' ); }); @@ -99,7 +101,7 @@ describe('resolveMemberRuntimeSummary', () => { ).toBe('5.4 Mini ยท Medium ยท Codex native'); }); - it('marks persisted legacy Codex lanes as legacy fallbacks in the runtime summary', () => { + it('normalizes persisted legacy Codex lanes to the native runtime summary', () => { const member = createMember({ model: 'gpt-5.4-mini' }); expect( @@ -114,6 +116,6 @@ describe('resolveMemberRuntimeSummary', () => { }, undefined ) - ).toBe('5.4 Mini ยท Medium ยท Legacy OpenAI fallback'); + ).toBe('5.4 Mini ยท Medium ยท Codex native'); }); }); diff --git a/test/renderer/utils/teamModelAvailability.test.ts b/test/renderer/utils/teamModelAvailability.test.ts index 448710fa..a5cbb329 100644 --- a/test/renderer/utils/teamModelAvailability.test.ts +++ b/test/renderer/utils/teamModelAvailability.test.ts @@ -15,11 +15,11 @@ function createCodexProviderStatus( return { providerId: 'codex', models, - authMethod: 'oauth_token', + authMethod: 'api_key', backend: { - kind: 'adapter', - label: 'Default adapter', - endpointLabel: 'chatgpt.com/backend-api/codex/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, authenticated: true, supported: true, @@ -39,7 +39,7 @@ describe('teamModelAvailability', () => { ]); }); - it('filters Codex models that are UI-disabled even if runtime reports them', () => { + it('filters only the Codex models that remain UI-disabled on the native runtime path', () => { const providerStatus = createCodexProviderStatus([ 'gpt-5.4', 'gpt-5.3-codex-spark', @@ -48,16 +48,19 @@ describe('teamModelAvailability', () => { 'gpt-5.1-codex-max', ]); - expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']); + expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([ + 'gpt-5.4', + 'gpt-5.1-codex-max', + ]); }); - it('keeps 5.1 Codex Max available outside the ChatGPT subscription path', () => { + it('keeps 5.1 Codex Max available on the native runtime path', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.1-codex-max'], { authMethod: 'api_key', backend: { - kind: 'openai', - label: 'OpenAI', - endpointLabel: 'api.openai.com/v1/responses', + kind: 'codex-native', + label: 'Codex native', + endpointLabel: 'codex exec --json', }, });