feat(runtime): add codex-native phase 0 app integration
This commit is contained in:
parent
fbf299f276
commit
ba37c1caf5
23 changed files with 7908 additions and 103 deletions
5148
docs/research/codex-native-runtime-integration-decision.md
Normal file
5148
docs/research/codex-native-runtime-integration-decision.md
Normal file
File diff suppressed because it is too large
Load diff
1216
docs/research/codex-native-runtime-phase-0-implementation-spec.md
Normal file
1216
docs/research/codex-native-runtime-phase-0-implementation-spec.md
Normal file
File diff suppressed because it is too large
Load diff
226
docs/research/codex-native-runtime-phase-0-signoff-evidence.md
Normal file
226
docs/research/codex-native-runtime-phase-0-signoff-evidence.md
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
# Codex Native Runtime - Phase 0 Sign-off Evidence
|
||||||
|
|
||||||
|
Captured on 2026-04-19.
|
||||||
|
|
||||||
|
This file is the repo-visible evidence package referenced by:
|
||||||
|
|
||||||
|
- [codex-native-runtime-phase-0-implementation-spec.md](./codex-native-runtime-phase-0-implementation-spec.md)
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
Phase 0 sign-off evidence is now captured.
|
||||||
|
|
||||||
|
What this proves:
|
||||||
|
|
||||||
|
- the `codex-native` lane executes through the raw `codex exec --json` seam
|
||||||
|
- persisted transcript projection remains parseable by current `claude_team` readers
|
||||||
|
- `ephemeral` and `persistent` runs keep different history-completeness truth
|
||||||
|
- thread status, warning attribution, executable identity, and usage authority survive end-to-end
|
||||||
|
- old Codex lane fallback truth remains covered by targeted regression tests
|
||||||
|
|
||||||
|
What this does **not** mean:
|
||||||
|
|
||||||
|
- `codex-native` should be unlocked for general runtime selection
|
||||||
|
- `auto` should start resolving to `codex-native`
|
||||||
|
- broader plugin or interactive capability claims are now safe
|
||||||
|
|
||||||
|
## Command Package
|
||||||
|
|
||||||
|
### `agent_teams_orchestrator`
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test src/services/codexNative/signOffHarness.test.ts \
|
||||||
|
src/services/codexNative/statusAuthority.test.ts \
|
||||||
|
src/services/codexNative/transcriptProjector.test.ts \
|
||||||
|
src/services/codexNative/turnExecutor.test.ts \
|
||||||
|
src/services/codexNative/execRunner.test.ts \
|
||||||
|
src/services/codexNative/jsonlMapper.test.ts \
|
||||||
|
src/services/runtimeBackends/codexBackendResolver.test.ts \
|
||||||
|
src/services/runtimeBackends/registry.agentTeams.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- `27 pass`
|
||||||
|
- `0 fail`
|
||||||
|
|
||||||
|
### `claude_team`
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec vitest run \
|
||||||
|
test/main/utils/jsonl.test.ts \
|
||||||
|
test/main/services/parsing/SessionParser.test.ts \
|
||||||
|
test/main/services/team/BoardTaskExactLogStrictParser.test.ts \
|
||||||
|
test/main/ipc/configValidation.test.ts \
|
||||||
|
test/main/services/runtime/ProviderConnectionService.test.ts \
|
||||||
|
test/main/services/runtime/providerAwareCliEnv.test.ts \
|
||||||
|
test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts \
|
||||||
|
test/renderer/components/runtime/providerConnectionUi.test.ts \
|
||||||
|
test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts \
|
||||||
|
test/renderer/components/cli/CliStatusVisibility.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- `134 pass`
|
||||||
|
- `0 fail`
|
||||||
|
|
||||||
|
### Diff cleanliness
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- clean in both worktrees
|
||||||
|
|
||||||
|
## Live Native Run Evidence
|
||||||
|
|
||||||
|
### Common live-run facts
|
||||||
|
|
||||||
|
Observed from both runs:
|
||||||
|
|
||||||
|
- native binary path: `/usr/local/bin/codex`
|
||||||
|
- native binary source: `system-path`
|
||||||
|
- native binary version: `codex-cli 0.117.0`
|
||||||
|
- credential input source for the sign-off harness: `OPENAI_API_KEY`
|
||||||
|
- credential source observed by the runner: `explicit-api-key`
|
||||||
|
- capability profile: `headless-limited`
|
||||||
|
- final assistant text: `OK`
|
||||||
|
|
||||||
|
### Ephemeral run
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run ./scripts/codex-native-phase0-signoff.ts \
|
||||||
|
--cwd /tmp \
|
||||||
|
--prompt 'Reply only with OK' \
|
||||||
|
--ephemeral
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- thread id: `019da680-6f43-7e10-824c-4d985bcdca12`
|
||||||
|
- completion policy: `ephemeral`
|
||||||
|
- final history completeness: `live-only`
|
||||||
|
- final usage authority: `live-turn-completed`
|
||||||
|
- assistant usage:
|
||||||
|
- input tokens: `23616`
|
||||||
|
- cached input tokens: `0`
|
||||||
|
- output tokens: `42`
|
||||||
|
|
||||||
|
History authority proof:
|
||||||
|
|
||||||
|
- projected warning subtype: `codex_native_warning`
|
||||||
|
- projected warning source: `history`
|
||||||
|
- observed warning text contained:
|
||||||
|
- `thread/read failed while backfilling turn items for turn completion`
|
||||||
|
- `ephemeral threads do not support includeTurns`
|
||||||
|
|
||||||
|
This is the explicit proof that `ephemeral` live stream does **not** equal canonical hydrated history.
|
||||||
|
|
||||||
|
### Persistent run
|
||||||
|
|
||||||
|
Executed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run ./scripts/codex-native-phase0-signoff.ts \
|
||||||
|
--cwd /tmp \
|
||||||
|
--prompt 'Reply only with OK' \
|
||||||
|
--persistent
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed result:
|
||||||
|
|
||||||
|
- thread id: `019da680-6f42-77c0-94f1-4e450a69d1f1`
|
||||||
|
- completion policy: `persistent`
|
||||||
|
- final history completeness: `explicit-hydration-required`
|
||||||
|
- final usage authority: `live-turn-completed`
|
||||||
|
- assistant usage:
|
||||||
|
- input tokens: `23616`
|
||||||
|
- cached input tokens: `0`
|
||||||
|
- output tokens: `33`
|
||||||
|
|
||||||
|
This is the explicit proof that persistent native runs keep a different history-completeness contract from `ephemeral` runs.
|
||||||
|
|
||||||
|
## Warning Attribution Proof
|
||||||
|
|
||||||
|
The live runs produced both:
|
||||||
|
|
||||||
|
- process/runtime warnings
|
||||||
|
- history-completeness warnings
|
||||||
|
|
||||||
|
Observed process-attributed warnings included:
|
||||||
|
|
||||||
|
- plugin cache / featured plugins unauthorized warnings
|
||||||
|
- state DB migration mismatch warnings
|
||||||
|
- shell snapshot timeout warnings
|
||||||
|
- MCP process-group termination warnings
|
||||||
|
|
||||||
|
Observed history-attributed warning included:
|
||||||
|
|
||||||
|
- `thread/read failed while backfilling turn items for turn completion: ... ephemeral threads do not support includeTurns`
|
||||||
|
|
||||||
|
This proves the lane now keeps `process` and `history` warning truth distinct in projected transcript rows.
|
||||||
|
|
||||||
|
## Thread-status Proof
|
||||||
|
|
||||||
|
Observed projected system rows included:
|
||||||
|
|
||||||
|
- `codex_native_thread_status`
|
||||||
|
- `running`
|
||||||
|
- `completed`
|
||||||
|
|
||||||
|
This proves the lane now writes native thread-status authority into persisted transcript-compatible rows instead of forcing UI and replay consumers to infer health from provider-global process truth.
|
||||||
|
|
||||||
|
## Parser And Exact-log Proof
|
||||||
|
|
||||||
|
Covered by green targeted tests:
|
||||||
|
|
||||||
|
- `test/main/utils/jsonl.test.ts`
|
||||||
|
- `test/main/services/parsing/SessionParser.test.ts`
|
||||||
|
- `test/main/services/team/BoardTaskExactLogStrictParser.test.ts`
|
||||||
|
|
||||||
|
These tests prove:
|
||||||
|
|
||||||
|
- projected assistant usage remains parseable
|
||||||
|
- projected warning/source metadata remains parseable
|
||||||
|
- projected execution-summary/history metadata remains parseable
|
||||||
|
- exact-log readers do not drop the native authority rows
|
||||||
|
|
||||||
|
## Degraded Old-lane Fallback Proof
|
||||||
|
|
||||||
|
Covered by green targeted tests:
|
||||||
|
|
||||||
|
- `src/services/runtimeBackends/codexBackendResolver.test.ts`
|
||||||
|
- `src/services/runtimeBackends/registry.agentTeams.test.ts`
|
||||||
|
|
||||||
|
Those tests prove:
|
||||||
|
|
||||||
|
- `auto` still does not silently resolve to `codex-native`
|
||||||
|
- native lane remains unavailable without:
|
||||||
|
- feature flag
|
||||||
|
- binary
|
||||||
|
- `CODEX_API_KEY`
|
||||||
|
- old Codex lane remains the truthful fallback when native is absent or degraded
|
||||||
|
|
||||||
|
## Sign-off Conclusion
|
||||||
|
|
||||||
|
✅ The Phase 0 code path is implementation-complete and evidence-backed.
|
||||||
|
|
||||||
|
⚠️ The lane should still remain:
|
||||||
|
|
||||||
|
- feature-flagged
|
||||||
|
- non-default
|
||||||
|
- non-auto-resolved
|
||||||
|
- non-selectable for normal runtime switching
|
||||||
|
|
||||||
|
That remaining lock is now a rollout-policy choice, not a missing-code problem.
|
||||||
|
|
@ -442,10 +442,16 @@ function validateRuntimeSection(data: unknown): ValidationSuccess<'runtime'> | V
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
if (backendId !== 'auto' && backendId !== 'adapter') {
|
if (
|
||||||
|
backendId !== 'auto' &&
|
||||||
|
backendId !== 'adapter' &&
|
||||||
|
backendId !== 'api' &&
|
||||||
|
backendId !== 'codex-native'
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'runtime.providerBackends.codex must be one of: auto, adapter',
|
error:
|
||||||
|
'runtime.providerBackends.codex must be one of: auto, adapter, api, codex-native',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
providerBackends.codex = backendId;
|
providerBackends.codex = backendId;
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export interface GeneralConfig {
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'auto' | 'api' | 'cli-sdk';
|
gemini: 'auto' | 'api' | 'cli-sdk';
|
||||||
codex: 'auto' | 'adapter';
|
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const CODEX_API_KEY_BETA_ENV_VAR = 'CLAUDE_CODE_CODEX_API_KEY_BETA';
|
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';
|
||||||
|
|
||||||
export class ProviderConnectionService {
|
export class ProviderConnectionService {
|
||||||
private static instance: ProviderConnectionService | null = null;
|
private static instance: ProviderConnectionService | null = null;
|
||||||
|
|
@ -64,7 +66,7 @@ export class ProviderConnectionService {
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||||
return codexConnection.apiKeyBetaEnabled ? codexConnection.authMode : null;
|
return this.shouldExposeCodexConnectionModes() ? codexConnection.authMode : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -107,31 +109,55 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||||
if (!codexConnection.apiKeyBetaEnabled) {
|
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||||
|
if (!this.shouldExposeCodexConnectionModes()) {
|
||||||
delete env[CODEX_API_KEY_BETA_ENV_VAR];
|
delete env[CODEX_API_KEY_BETA_ENV_VAR];
|
||||||
delete env.OPENAI_API_KEY;
|
delete env.OPENAI_API_KEY;
|
||||||
|
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
|
if (codexConnection.apiKeyBetaEnabled) {
|
||||||
|
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
|
||||||
|
} else {
|
||||||
|
delete env[CODEX_API_KEY_BETA_ENV_VAR];
|
||||||
|
}
|
||||||
|
|
||||||
if (codexConnection.authMode === 'oauth') {
|
if (codexConnection.authMode === 'oauth') {
|
||||||
env.CLAUDE_CODE_CODEX_BACKEND = 'adapter';
|
|
||||||
delete env.OPENAI_API_KEY;
|
delete env.OPENAI_API_KEY;
|
||||||
|
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
env.CLAUDE_CODE_CODEX_BACKEND = 'api';
|
|
||||||
|
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||||
if (storedKey?.value.trim()) {
|
const existingOpenAiKey =
|
||||||
env.OPENAI_API_KEY = storedKey.value;
|
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||||
|
? env.OPENAI_API_KEY
|
||||||
|
: null;
|
||||||
|
const existingNativeKey =
|
||||||
|
typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' &&
|
||||||
|
env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim()
|
||||||
|
? env[CODEX_NATIVE_API_KEY_ENV_VAR]
|
||||||
|
: null;
|
||||||
|
const resolvedApiKey =
|
||||||
|
storedKey?.value.trim() ||
|
||||||
|
existingOpenAiKey ||
|
||||||
|
(codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID ? existingNativeKey : null);
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof env.OPENAI_API_KEY !== 'string' || !env.OPENAI_API_KEY.trim()) {
|
if (typeof env.OPENAI_API_KEY !== 'string' || !env.OPENAI_API_KEY.trim()) {
|
||||||
delete env.OPENAI_API_KEY;
|
delete env.OPENAI_API_KEY;
|
||||||
}
|
}
|
||||||
|
delete env[CODEX_NATIVE_API_KEY_ENV_VAR];
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
@ -165,20 +191,48 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||||
if (!codexConnection.apiKeyBetaEnabled) {
|
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||||
|
if (!this.shouldExposeCodexConnectionModes()) {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
|
if (codexConnection.apiKeyBetaEnabled) {
|
||||||
env.CLAUDE_CODE_CODEX_BACKEND = codexConnection.authMode === 'oauth' ? 'adapter' : 'api';
|
env[CODEX_API_KEY_BETA_ENV_VAR] = '1';
|
||||||
|
} else {
|
||||||
|
delete env[CODEX_API_KEY_BETA_ENV_VAR];
|
||||||
|
}
|
||||||
|
|
||||||
if (codexConnection.authMode !== 'api_key') {
|
if (codexConnection.authMode !== 'api_key') {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||||
|
const existingOpenAiKey =
|
||||||
|
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||||
|
? env.OPENAI_API_KEY
|
||||||
|
: null;
|
||||||
|
const existingNativeKey =
|
||||||
|
typeof env[CODEX_NATIVE_API_KEY_ENV_VAR] === 'string' &&
|
||||||
|
env[CODEX_NATIVE_API_KEY_ENV_VAR]?.trim()
|
||||||
|
? env[CODEX_NATIVE_API_KEY_ENV_VAR]
|
||||||
|
: null;
|
||||||
|
const resolvedApiKey =
|
||||||
|
storedKey?.value.trim() ||
|
||||||
|
existingOpenAiKey ||
|
||||||
|
(codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID ? existingNativeKey : null);
|
||||||
|
|
||||||
if (storedKey?.value.trim()) {
|
if (storedKey?.value.trim()) {
|
||||||
env.OPENAI_API_KEY = storedKey.value;
|
env.OPENAI_API_KEY = storedKey.value;
|
||||||
|
} else if (
|
||||||
|
!existingOpenAiKey &&
|
||||||
|
existingNativeKey &&
|
||||||
|
codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID
|
||||||
|
) {
|
||||||
|
env.OPENAI_API_KEY = existingNativeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID && resolvedApiKey) {
|
||||||
|
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
|
|
@ -216,7 +270,8 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
const codexConnection = this.configManager.getConfig().providerConnections.codex;
|
||||||
if (!codexConnection.apiKeyBetaEnabled || codexConnection.authMode !== 'api_key') {
|
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend();
|
||||||
|
if (!this.shouldExposeCodexConnectionModes() || codexConnection.authMode !== 'api_key') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,10 +279,17 @@ export class ProviderConnectionService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (
|
||||||
'Codex API key mode is enabled, but no OPENAI_API_KEY is configured. ' +
|
codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID &&
|
||||||
'Add a stored/environment API key or switch Codex auth mode back to OAuth.'
|
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.';
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfiguredConnectionIssues(
|
async getConfiguredConnectionIssues(
|
||||||
|
|
@ -260,17 +322,23 @@ export class ProviderConnectionService {
|
||||||
async getConnectionInfo(providerId: CliProviderId): Promise<CliProviderConnectionInfo> {
|
async getConnectionInfo(providerId: CliProviderId): Promise<CliProviderConnectionInfo> {
|
||||||
const capabilities = PROVIDER_CAPABILITIES[providerId];
|
const capabilities = PROVIDER_CAPABILITIES[providerId];
|
||||||
const storedApiKey = await this.getStoredApiKey(providerId);
|
const storedApiKey = await this.getStoredApiKey(providerId);
|
||||||
const externalCredential = this.getExternalCredential(providerId);
|
const codexRuntimeBackend =
|
||||||
|
providerId === 'codex' ? this.getConfiguredCodexRuntimeBackend() : null;
|
||||||
|
const externalCredential = this.getExternalCredential(providerId, codexRuntimeBackend);
|
||||||
const codexBetaEnabled =
|
const codexBetaEnabled =
|
||||||
providerId === 'codex'
|
providerId === 'codex'
|
||||||
? this.configManager.getConfig().providerConnections.codex.apiKeyBetaEnabled
|
? this.configManager.getConfig().providerConnections.codex.apiKeyBetaEnabled
|
||||||
: undefined;
|
: undefined;
|
||||||
const configurableAuthModes =
|
const configurableAuthModes =
|
||||||
providerId === 'codex' && codexBetaEnabled
|
providerId === 'codex' &&
|
||||||
|
(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
|
||||||
? (['oauth', 'api_key'] as CliProviderAuthMode[])
|
? (['oauth', 'api_key'] as CliProviderAuthMode[])
|
||||||
: capabilities.configurableAuthModes;
|
: capabilities.configurableAuthModes;
|
||||||
const configuredAuthMode =
|
const configuredAuthMode =
|
||||||
providerId === 'codex' && !codexBetaEnabled ? null : this.getConfiguredAuthMode(providerId);
|
providerId === 'codex' &&
|
||||||
|
!(codexBetaEnabled || codexRuntimeBackend === CODEX_NATIVE_BACKEND_ID)
|
||||||
|
? null
|
||||||
|
: this.getConfiguredAuthMode(providerId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...capabilities,
|
...capabilities,
|
||||||
|
|
@ -301,7 +369,22 @@ export class ProviderConnectionService {
|
||||||
return this.apiKeyService.lookupPreferred(envVarName);
|
return this.apiKeyService.lookupPreferred(envVarName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExternalCredential(providerId: CliProviderId): ExternalCredential {
|
private getConfiguredCodexRuntimeBackend(): 'auto' | 'adapter' | 'api' | 'codex-native' {
|
||||||
|
return this.configManager.getConfig().runtime.providerBackends.codex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldExposeCodexConnectionModes(): boolean {
|
||||||
|
const config = this.configManager.getConfig();
|
||||||
|
return (
|
||||||
|
config.providerConnections.codex.apiKeyBetaEnabled ||
|
||||||
|
config.runtime.providerBackends.codex === CODEX_NATIVE_BACKEND_ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExternalCredential(
|
||||||
|
providerId: CliProviderId,
|
||||||
|
codexRuntimeBackend: 'auto' | 'adapter' | 'api' | 'codex-native' | null = null
|
||||||
|
): ExternalCredential {
|
||||||
const shellEnv = getCachedShellEnv() ?? {};
|
const shellEnv = getCachedShellEnv() ?? {};
|
||||||
const sources = [shellEnv, process.env];
|
const sources = [shellEnv, process.env];
|
||||||
|
|
||||||
|
|
@ -336,6 +419,16 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
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 apiKey = findEnvValue('OPENAI_API_KEY');
|
const apiKey = findEnvValue('OPENAI_API_KEY');
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -198,9 +198,22 @@ export interface AssistantEntry extends ConversationalEntry {
|
||||||
|
|
||||||
export interface SystemEntry extends ConversationalEntry {
|
export interface SystemEntry extends ConversationalEntry {
|
||||||
type: 'system';
|
type: 'system';
|
||||||
subtype: 'turn_duration' | 'init';
|
subtype?: 'turn_duration' | 'init' | 'informational' | 'permission_retry' | 'api_retry' | string;
|
||||||
durationMs: number;
|
durationMs?: number;
|
||||||
isMeta: boolean;
|
isMeta: boolean;
|
||||||
|
content?: string;
|
||||||
|
level?: 'info' | 'warning' | 'error' | 'suggestion' | string;
|
||||||
|
toolUseID?: string;
|
||||||
|
preventContinuation?: boolean;
|
||||||
|
codexNativeWarningSource?: string;
|
||||||
|
codexNativeThreadStatus?: string;
|
||||||
|
codexNativeThreadId?: string;
|
||||||
|
codexNativeCompletionPolicy?: 'ephemeral' | 'persistent' | string;
|
||||||
|
codexNativeHistoryCompleteness?: string;
|
||||||
|
codexNativeFinalUsageAuthority?: string;
|
||||||
|
codexNativeExecutablePath?: string;
|
||||||
|
codexNativeExecutableSource?: string;
|
||||||
|
codexNativeExecutableVersion?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SummaryEntry extends BaseEntry {
|
export interface SummaryEntry extends BaseEntry {
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,19 @@ export interface ParsedMessage {
|
||||||
isCompactSummary?: boolean;
|
isCompactSummary?: boolean;
|
||||||
/** API request ID for deduplicating streaming entries */
|
/** API request ID for deduplicating streaming entries */
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
/** System-message severity when available in the raw transcript */
|
||||||
|
level?: string;
|
||||||
|
/** Raw system subtype when available in the transcript */
|
||||||
|
subtype?: string;
|
||||||
|
codexNativeWarningSource?: string;
|
||||||
|
codexNativeThreadStatus?: string;
|
||||||
|
codexNativeThreadId?: string;
|
||||||
|
codexNativeCompletionPolicy?: string;
|
||||||
|
codexNativeHistoryCompleteness?: string;
|
||||||
|
codexNativeFinalUsageAuthority?: string;
|
||||||
|
codexNativeExecutablePath?: string;
|
||||||
|
codexNativeExecutableSource?: string;
|
||||||
|
codexNativeExecutableVersion?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,17 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null {
|
||||||
let gitBranch: string | undefined;
|
let gitBranch: string | undefined;
|
||||||
let agentId: string | undefined;
|
let agentId: string | undefined;
|
||||||
let agentName: string | undefined;
|
let agentName: string | undefined;
|
||||||
|
let level: string | undefined;
|
||||||
|
let subtype: string | undefined;
|
||||||
|
let codexNativeWarningSource: string | undefined;
|
||||||
|
let codexNativeThreadStatus: string | undefined;
|
||||||
|
let codexNativeThreadId: string | undefined;
|
||||||
|
let codexNativeCompletionPolicy: string | undefined;
|
||||||
|
let codexNativeHistoryCompleteness: string | undefined;
|
||||||
|
let codexNativeFinalUsageAuthority: string | undefined;
|
||||||
|
let codexNativeExecutablePath: string | undefined;
|
||||||
|
let codexNativeExecutableSource: string | undefined;
|
||||||
|
let codexNativeExecutableVersion: string | null | undefined;
|
||||||
let isSidechain = false;
|
let isSidechain = false;
|
||||||
let isMeta = false;
|
let isMeta = false;
|
||||||
let userType: string | undefined;
|
let userType: string | undefined;
|
||||||
|
|
@ -283,7 +294,19 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null {
|
||||||
agentId = entry.agentId;
|
agentId = entry.agentId;
|
||||||
requestId = entry.requestId;
|
requestId = entry.requestId;
|
||||||
} else if (entry.type === 'system') {
|
} else if (entry.type === 'system') {
|
||||||
|
content = entry.content ?? '';
|
||||||
isMeta = entry.isMeta ?? false;
|
isMeta = entry.isMeta ?? false;
|
||||||
|
level = entry.level;
|
||||||
|
subtype = entry.subtype;
|
||||||
|
codexNativeWarningSource = entry.codexNativeWarningSource;
|
||||||
|
codexNativeThreadStatus = entry.codexNativeThreadStatus;
|
||||||
|
codexNativeThreadId = entry.codexNativeThreadId;
|
||||||
|
codexNativeCompletionPolicy = entry.codexNativeCompletionPolicy;
|
||||||
|
codexNativeHistoryCompleteness = entry.codexNativeHistoryCompleteness;
|
||||||
|
codexNativeFinalUsageAuthority = entry.codexNativeFinalUsageAuthority;
|
||||||
|
codexNativeExecutablePath = entry.codexNativeExecutablePath;
|
||||||
|
codexNativeExecutableSource = entry.codexNativeExecutableSource;
|
||||||
|
codexNativeExecutableVersion = entry.codexNativeExecutableVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,6 +333,17 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null {
|
||||||
isMeta,
|
isMeta,
|
||||||
userType,
|
userType,
|
||||||
isCompactSummary,
|
isCompactSummary,
|
||||||
|
level,
|
||||||
|
subtype,
|
||||||
|
codexNativeWarningSource,
|
||||||
|
codexNativeThreadStatus,
|
||||||
|
codexNativeThreadId,
|
||||||
|
codexNativeCompletionPolicy,
|
||||||
|
codexNativeHistoryCompleteness,
|
||||||
|
codexNativeFinalUsageAuthority,
|
||||||
|
codexNativeExecutablePath,
|
||||||
|
codexNativeExecutableSource,
|
||||||
|
codexNativeExecutableVersion,
|
||||||
// Tool info
|
// Tool info
|
||||||
toolCalls,
|
toolCalls,
|
||||||
toolResults: toolResultsList,
|
toolResults: toolResultsList,
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,15 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.providerId === 'codex') {
|
||||||
|
return {
|
||||||
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'login', '--provider', provider.providerId],
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
};
|
};
|
||||||
|
|
@ -259,6 +268,15 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.providerId === 'codex') {
|
||||||
|
return {
|
||||||
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'logout', '--provider', provider.providerId],
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,10 @@ export const ProviderRuntimeBackendSelector = ({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={option.id}
|
key={option.id}
|
||||||
value={option.id}
|
value={option.id}
|
||||||
disabled={!option.available && option.id !== selectedBackendId}
|
disabled={
|
||||||
|
(!option.available || option.selectable === false) &&
|
||||||
|
option.id !== selectedBackendId
|
||||||
|
}
|
||||||
className="py-2.5"
|
className="py-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
|
|
@ -127,6 +130,16 @@ export const ProviderRuntimeBackendSelector = ({
|
||||||
>
|
>
|
||||||
Unavailable
|
Unavailable
|
||||||
</span>
|
</span>
|
||||||
|
) : option.selectable === false ? (
|
||||||
|
<span
|
||||||
|
className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px]"
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Locked
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
<span className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
|
@ -179,6 +192,27 @@ export const ProviderRuntimeBackendSelector = ({
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
) : selectedOption.selectable === false ? (
|
||||||
|
<TooltipProvider delayDuration={150}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="cursor-help rounded-full px-1.5 py-0.5 text-[10px]"
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Locked
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{selectedOption.detailMessage ??
|
||||||
|
selectedOption.statusMessage ??
|
||||||
|
'This backend cannot be selected yet.'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-1 text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
<div className="mt-2 space-y-1 text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,17 @@ function isApiKeyProviderId(providerId: CliProviderId): providerId is ApiKeyProv
|
||||||
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'gemini';
|
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' &&
|
||||||
|
(provider.selectedBackendId === 'codex-native' || provider.resolvedBackendId === 'codex-native')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function findPreferredApiKeyEntry(apiKeys: ApiKeyEntry[], envVarName: string): ApiKeyEntry | null {
|
function findPreferredApiKeyEntry(apiKeys: ApiKeyEntry[], envVarName: string): ApiKeyEntry | null {
|
||||||
const matches = apiKeys.filter((entry) => entry.envVarName === envVarName);
|
const matches = apiKeys.filter((entry) => entry.envVarName === envVarName);
|
||||||
return matches.find((entry) => entry.scope === 'user') ?? null;
|
return matches.find((entry) => entry.scope === 'user') ?? null;
|
||||||
|
|
@ -113,9 +124,11 @@ function getConnectionDescription(provider: CliProviderStatus): string {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return 'Choose how app-launched Anthropic sessions authenticate.';
|
return 'Choose how app-launched Anthropic sessions authenticate.';
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return provider.connection?.apiKeyBetaEnabled
|
return hasExplicitRuntimeBackends(provider)
|
||||||
? 'Choose whether app-launched Codex sessions use your Codex subscription or an OpenAI API key. Runtime follows this automatically.'
|
? 'Choose which credentials app-launched Codex sessions should use. Runtime backend is configured separately below.'
|
||||||
: 'Codex uses your subscription session by default. Enable API key mode if you want to switch Codex to OPENAI_API_KEY billing.';
|
: provider.connection?.apiKeyBetaEnabled
|
||||||
|
? 'Choose whether app-launched Codex sessions use your Codex subscription or an OpenAI API key.'
|
||||||
|
: 'Codex uses your subscription session by default. Enable API key mode if you want to switch Codex credential routing to API-key billing.';
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
|
return 'Configure optional API access. CLI SDK and ADC are still discovered automatically.';
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +139,9 @@ function getRuntimeDescription(provider: CliProviderStatus): string {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return 'Anthropic currently has no separate runtime backend selector.';
|
return 'Anthropic currently has no separate runtime backend selector.';
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return 'Codex runtime selection follows the active connection method automatically.';
|
return hasExplicitRuntimeBackends(provider)
|
||||||
|
? 'Choose which Codex runtime backend multimodel should use. Connection method only controls credentials.'
|
||||||
|
: 'Codex runtime selection follows the active connection method automatically.';
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return 'Choose which Gemini runtime backend multimodel should use.';
|
return 'Choose which Gemini runtime backend multimodel should use.';
|
||||||
}
|
}
|
||||||
|
|
@ -146,8 +161,8 @@ function getAuthModeDescription(providerId: CliProviderId, authMode: CliProvider
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
return authMode === 'api_key'
|
return authMode === 'api_key'
|
||||||
? 'Use OPENAI_API_KEY and the public OpenAI Responses API backend.'
|
? 'Use API-key credentials for app-launched Codex sessions. The selected runtime backend decides how those credentials are consumed.'
|
||||||
: 'Use your Codex subscription session and the built-in Codex runtime.';
|
: 'Use your Codex subscription session. API-key-only backends remain unavailable until you switch this credential mode.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -185,25 +200,20 @@ function getConnectionAlert(provider: CliProviderStatus): string | null {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
provider.providerId === 'codex' &&
|
provider.providerId === 'codex' &&
|
||||||
provider.connection?.apiKeyBetaEnabled &&
|
|
||||||
authMode === 'api_key' &&
|
authMode === 'api_key' &&
|
||||||
!provider.connection?.apiKeyConfigured
|
!provider.connection?.apiKeyConfigured
|
||||||
) {
|
) {
|
||||||
return 'API key mode is selected, but no OPENAI_API_KEY credential is available yet.';
|
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 (
|
if (provider.providerId === 'codex' && authMode === 'oauth' && !hasCodexSubscriptionSession) {
|
||||||
provider.providerId === 'codex' &&
|
|
||||||
provider.connection?.apiKeyBetaEnabled &&
|
|
||||||
authMode === 'oauth' &&
|
|
||||||
!hasCodexSubscriptionSession
|
|
||||||
) {
|
|
||||||
return 'Codex subscription mode is selected. Sign in with Codex to use this provider.';
|
return 'Codex subscription mode is selected. Sign in with Codex to use this provider.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
provider.providerId === 'codex' &&
|
provider.providerId === 'codex' &&
|
||||||
provider.connection?.apiKeyBetaEnabled &&
|
|
||||||
authMode === 'oauth' &&
|
authMode === 'oauth' &&
|
||||||
provider.connection?.apiKeySource === 'stored'
|
provider.connection?.apiKeySource === 'stored'
|
||||||
) {
|
) {
|
||||||
|
|
@ -266,7 +276,9 @@ function getConnectionMethodCardOptions(
|
||||||
|
|
||||||
function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null {
|
function getConnectionMethodCardsHint(provider: CliProviderStatus): string | null {
|
||||||
if (provider.providerId === 'codex') {
|
if (provider.providerId === 'codex') {
|
||||||
return 'Runtime follows your connection method automatically.';
|
return hasExplicitRuntimeBackends(provider)
|
||||||
|
? 'Connection method controls credentials only. Runtime backend selection is independent.'
|
||||||
|
: 'Runtime follows your connection method automatically.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'anthropic') {
|
if (provider.providerId === 'anthropic') {
|
||||||
|
|
@ -899,7 +911,8 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
|
|
||||||
{selectedProvider.providerId === 'codex' &&
|
{selectedProvider.providerId === 'codex' &&
|
||||||
selectedProvider.connection?.apiKeyBetaAvailable &&
|
selectedProvider.connection?.apiKeyBetaAvailable &&
|
||||||
!selectedProvider.connection.apiKeyBetaEnabled ? (
|
!selectedProvider.connection.apiKeyBetaEnabled &&
|
||||||
|
!showConnectionMethodCards ? (
|
||||||
<div
|
<div
|
||||||
className="space-y-3 rounded-md border p-3"
|
className="space-y-3 rounded-md border p-3"
|
||||||
style={{ borderColor: 'var(--color-border-subtle)' }}
|
style={{ borderColor: 'var(--color-border-subtle)' }}
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,22 @@ export function formatProviderAuthMethodLabelForProvider(
|
||||||
return formatProviderAuthMethodLabel(authMethod);
|
return formatProviderAuthMethodLabel(authMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCodexNativeLane(provider: CliProviderStatus): boolean {
|
||||||
|
return (
|
||||||
|
provider.providerId === 'codex' &&
|
||||||
|
(provider.resolvedBackendId === 'codex-native' || provider.selectedBackendId === 'codex-native')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
|
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
|
||||||
return provider.providerId === 'codex';
|
return provider.providerId === 'codex' && (provider.availableBackends?.length ?? 0) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
|
function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
|
||||||
|
if (isCodexNativeLane(provider) && provider.backend?.label) {
|
||||||
|
return provider.backend.label;
|
||||||
|
}
|
||||||
|
|
||||||
if (provider.authenticated) {
|
if (provider.authenticated) {
|
||||||
return provider.authMethod === 'api_key' ? CODEX_API_KEY_LABEL : CODEX_SUBSCRIPTION_LABEL;
|
return provider.authMethod === 'api_key' ? CODEX_API_KEY_LABEL : CODEX_SUBSCRIPTION_LABEL;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +97,7 @@ function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null {
|
export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): string | null {
|
||||||
if (provider.providerId !== 'codex') {
|
if (provider.providerId !== 'codex' || !isConnectionManagedRuntimeProvider(provider)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +174,12 @@ export function getProviderCredentialSummary(provider: CliProviderStatus): strin
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.providerId === 'codex' && provider.connection?.apiKeyBetaEnabled !== true) {
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
return provider.connection.apiKeySource === 'stored'
|
return provider.connection.apiKeySource === 'stored'
|
||||||
? 'OpenAI API key is saved in Manage. Enable API key mode to use it.'
|
? '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.';
|
: 'OpenAI API key detected. Enable API key mode in Manage to use it.';
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,15 @@ function getProviderTerminalCommand(provider: CliProviderStatus): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.providerId === 'codex') {
|
||||||
|
return {
|
||||||
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'login', '--provider', provider.providerId],
|
args: ['auth', 'login', '--provider', provider.providerId],
|
||||||
};
|
};
|
||||||
|
|
@ -124,6 +133,15 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.providerId === 'codex') {
|
||||||
|
return {
|
||||||
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: provider.selectedBackendId ?? 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
args: ['auth', 'logout', '--provider', provider.providerId],
|
args: ['auth', 'logout', '--provider', provider.providerId],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ export interface AppConfig {
|
||||||
runtime: {
|
runtime: {
|
||||||
providerBackends: {
|
providerBackends: {
|
||||||
gemini: 'auto' | 'api' | 'cli-sdk';
|
gemini: 'auto' | 'api' | 'cli-sdk';
|
||||||
codex: 'auto' | 'adapter';
|
codex: 'auto' | 'adapter' | 'api' | 'codex-native';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Display and UI settings */
|
/** Display and UI settings */
|
||||||
|
|
|
||||||
|
|
@ -239,4 +239,49 @@ describe('configValidation', () => {
|
||||||
expect(result.error).toContain('providerConnections.codex.authMode');
|
expect(result.error).toContain('providerConnections.codex.authMode');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts Codex runtime backend updates for api and codex-native', () => {
|
||||||
|
const apiResult = validateConfigUpdatePayload('runtime', {
|
||||||
|
providerBackends: {
|
||||||
|
codex: 'api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiResult.valid).toBe(true);
|
||||||
|
if (apiResult.valid) {
|
||||||
|
expect(apiResult.data).toEqual({
|
||||||
|
providerBackends: {
|
||||||
|
codex: 'api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeResult = validateConfigUpdatePayload('runtime', {
|
||||||
|
providerBackends: {
|
||||||
|
codex: 'codex-native',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nativeResult.valid).toBe(true);
|
||||||
|
if (nativeResult.valid) {
|
||||||
|
expect(nativeResult.data).toEqual({
|
||||||
|
providerBackends: {
|
||||||
|
codex: 'codex-native',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unknown Codex runtime backends', () => {
|
||||||
|
const result = validateConfigUpdatePayload('runtime', {
|
||||||
|
providerBackends: {
|
||||||
|
codex: 'native',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
if (!result.valid) {
|
||||||
|
expect(result.error).toContain('auto, adapter, api, codex-native');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
218
test/main/services/parsing/CodexNativePhase0Smoke.test.ts
Normal file
218
test/main/services/parsing/CodexNativePhase0Smoke.test.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
// @vitest-environment node
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { LocalFileSystemProvider } from '../../../../src/main/services/infrastructure/LocalFileSystemProvider';
|
||||||
|
import { SessionParser } from '../../../../src/main/services/parsing/SessionParser';
|
||||||
|
import { BoardTaskExactLogStrictParser } from '../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogStrictParser';
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
const mockProjectScanner = {
|
||||||
|
scan: vi.fn(),
|
||||||
|
getSessionPath: vi.fn(),
|
||||||
|
listSessionsPaginated: vi.fn(),
|
||||||
|
listSessions: vi.fn(),
|
||||||
|
listSubagentFiles: vi.fn(),
|
||||||
|
getSession: vi.fn(),
|
||||||
|
listWorktreeSessions: vi.fn(),
|
||||||
|
scanWithWorktreeGrouping: vi.fn(),
|
||||||
|
getFileSystemProvider: vi.fn().mockReturnValue(new LocalFileSystemProvider()),
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
tempDirs.splice(0, tempDirs.length).map(async (dirPath) => {
|
||||||
|
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('codex-native phase 0 smoke', () => {
|
||||||
|
let parser: SessionParser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// @ts-expect-error test partial
|
||||||
|
parser = new SessionParser(mockProjectScanner);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps native projected runtime truth parseable through session and exact-log readers', async () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-native-phase0-smoke-'));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, 'native-phase0-session.jsonl');
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-smoke',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-running',
|
||||||
|
timestamp: '2026-04-19T10:00:00.000Z',
|
||||||
|
subtype: 'codex_native_thread_status',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'Codex native thread started: thread-smoke',
|
||||||
|
codexNativeThreadStatus: 'running',
|
||||||
|
codexNativeThreadId: 'thread-smoke',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-smoke',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-warning',
|
||||||
|
timestamp: '2026-04-19T10:00:01.000Z',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
level: 'warning',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'thread/read failed while backfilling turn items for turn completion',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
codexNativeThreadId: 'thread-smoke',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: 'user-smoke-1',
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-smoke',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'assistant',
|
||||||
|
uuid: 'assistant-smoke',
|
||||||
|
requestId: 'request-smoke',
|
||||||
|
timestamp: '2026-04-19T10:00:02.000Z',
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'gpt-5.4-mini',
|
||||||
|
id: 'msg-native-smoke',
|
||||||
|
type: 'message',
|
||||||
|
stop_reason: 'end_turn',
|
||||||
|
stop_sequence: null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
content: [{ type: 'text', text: 'OK' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-smoke',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-summary',
|
||||||
|
timestamp: '2026-04-19T10:00:03.000Z',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content:
|
||||||
|
'Codex native execution summary: thread=thread-smoke, completion=ephemeral, history=live-only, usageAuthority=live-turn-completed, binary=codex-cli 0.117.0',
|
||||||
|
codexNativeThreadId: 'thread-smoke',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-smoke',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-completed',
|
||||||
|
timestamp: '2026-04-19T10:00:04.000Z',
|
||||||
|
subtype: 'codex_native_thread_status',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'Codex native thread completed: thread-smoke',
|
||||||
|
codexNativeThreadStatus: 'completed',
|
||||||
|
codexNativeThreadId: 'thread-smoke',
|
||||||
|
}),
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = await parser.parseSessionFile(filePath);
|
||||||
|
const exact = await new BoardTaskExactLogStrictParser().parseFiles([filePath]);
|
||||||
|
const exactRows = exact.get(filePath) ?? [];
|
||||||
|
|
||||||
|
expect(parsed.byType.assistant).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'assistant-smoke',
|
||||||
|
requestId: 'request-smoke',
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(parsed.byType.system).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'native-running',
|
||||||
|
subtype: 'codex_native_thread_status',
|
||||||
|
codexNativeThreadStatus: 'running',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: 'native-warning',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: 'native-summary',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: 'native-completed',
|
||||||
|
subtype: 'codex_native_thread_status',
|
||||||
|
codexNativeThreadStatus: 'completed',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(exactRows.map((row) => row.uuid)).toEqual([
|
||||||
|
'native-running',
|
||||||
|
'native-warning',
|
||||||
|
'assistant-smoke',
|
||||||
|
'native-summary',
|
||||||
|
'native-completed',
|
||||||
|
]);
|
||||||
|
expect(exactRows).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
uuid: 'native-warning',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
uuid: 'native-summary',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SessionParser,
|
SessionParser,
|
||||||
|
|
@ -173,6 +176,145 @@ describe('SessionParser', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseSessionFile', () => {
|
||||||
|
it('keeps codex-native projected assistant usage and modern system warnings parseable', async () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-parser-native-'));
|
||||||
|
const filePath = path.join(tempDir, 'native-session.jsonl');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-parse',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'system-native-warning-1',
|
||||||
|
timestamp: '2026-04-19T10:00:00.000Z',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
level: 'warning',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'native stderr warning',
|
||||||
|
codexNativeWarningSource: 'process',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: 'user-native-1',
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-parse',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'assistant',
|
||||||
|
uuid: 'assistant-native-1',
|
||||||
|
requestId: 'native-request-1',
|
||||||
|
timestamp: '2026-04-19T10:00:01.000Z',
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'gpt-5.4-mini',
|
||||||
|
id: 'msg-native-1',
|
||||||
|
type: 'message',
|
||||||
|
stop_reason: 'end_turn',
|
||||||
|
stop_sequence: null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
content: [{ type: 'text', text: 'OK' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = await parser.parseSessionFile(filePath);
|
||||||
|
|
||||||
|
expect(parsed.byType.system).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'system-native-warning-1',
|
||||||
|
content: 'native stderr warning',
|
||||||
|
level: 'warning',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
codexNativeWarningSource: 'process',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(parsed.byType.assistant).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'assistant-native-1',
|
||||||
|
requestId: 'native-request-1',
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps codex-native execution summary metadata parseable for replay and history truth', async () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-parser-native-summary-'));
|
||||||
|
const filePath = path.join(tempDir, 'native-summary-session.jsonl');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-summary',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'system-native-summary-1',
|
||||||
|
timestamp: '2026-04-19T10:00:02.000Z',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content:
|
||||||
|
'Codex native execution summary: thread=thread-persistent, completion=persistent, history=explicit-hydration-required, usageAuthority=live-turn-completed, binary=codex-cli 0.117.0',
|
||||||
|
codexNativeThreadId: 'thread-persistent',
|
||||||
|
codexNativeCompletionPolicy: 'persistent',
|
||||||
|
codexNativeHistoryCompleteness: 'explicit-hydration-required',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutablePath: '/usr/local/bin/codex',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = await parser.parseSessionFile(filePath);
|
||||||
|
|
||||||
|
expect(parsed.byType.system).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'system-native-summary-1',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeThreadId: 'thread-persistent',
|
||||||
|
codexNativeCompletionPolicy: 'persistent',
|
||||||
|
codexNativeHistoryCompleteness: 'explicit-hydration-required',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getResponses', () => {
|
describe('getResponses', () => {
|
||||||
it('should get assistant responses after user message', () => {
|
it('should get assistant responses after user message', () => {
|
||||||
const userMsgUuid = 'user-1';
|
const userMsgUuid = 'user-1';
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
import type { PathLike } from 'fs';
|
import type { PathLike } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
getProviderConnectionModeSummary,
|
||||||
|
getProviderCurrentRuntimeSummary,
|
||||||
|
isConnectionManagedRuntimeProvider,
|
||||||
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
|
|
||||||
const execCliMock = vi.fn();
|
const execCliMock = vi.fn();
|
||||||
const buildProviderAwareCliEnvMock = vi.fn();
|
const buildProviderAwareCliEnvMock = vi.fn();
|
||||||
|
|
@ -293,4 +298,145 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps codex-native lane truth honest from unified runtime status through renderer summaries', async () => {
|
||||||
|
execCliMock.mockResolvedValue({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
providers: {
|
||||||
|
anthropic: {
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'oauth_token',
|
||||||
|
verificationState: 'verified',
|
||||||
|
canLoginFromUi: true,
|
||||||
|
models: ['claude-sonnet-4-5'],
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: true,
|
||||||
|
oneShot: true,
|
||||||
|
extensions: {
|
||||||
|
plugins: { status: 'supported', ownership: 'shared', reason: null },
|
||||||
|
mcp: { status: 'supported', ownership: 'shared', reason: null },
|
||||||
|
skills: { status: 'supported', ownership: 'shared', reason: null },
|
||||||
|
apiKeys: { status: 'supported', ownership: 'shared', reason: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: { kind: 'anthropic', label: 'Anthropic' },
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'oauth_token',
|
||||||
|
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.',
|
||||||
|
selectedBackendId: 'codex-native',
|
||||||
|
resolvedBackendId: 'codex-native',
|
||||||
|
availableBackends: [
|
||||||
|
{
|
||||||
|
id: 'auto',
|
||||||
|
label: 'Auto',
|
||||||
|
selectable: true,
|
||||||
|
recommended: true,
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'codex-native',
|
||||||
|
label: 'Codex native',
|
||||||
|
selectable: false,
|
||||||
|
recommended: false,
|
||||||
|
available: true,
|
||||||
|
statusMessage: 'Experimental native lane',
|
||||||
|
detailMessage: 'Phase 0 keeps the lane locked behind rollout policy.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
externalRuntimeDiagnostics: [
|
||||||
|
{
|
||||||
|
id: 'codex-cli',
|
||||||
|
label: 'Codex CLI',
|
||||||
|
detected: true,
|
||||||
|
statusMessage: 'Detected',
|
||||||
|
detailMessage: 'System codex binary available.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: true,
|
||||||
|
oneShot: true,
|
||||||
|
extensions: {
|
||||||
|
plugins: {
|
||||||
|
status: 'unsupported',
|
||||||
|
ownership: 'shared',
|
||||||
|
reason: 'codex-native phase 0 keeps plugin execution disabled.',
|
||||||
|
},
|
||||||
|
mcp: {
|
||||||
|
status: 'unsupported',
|
||||||
|
ownership: 'shared',
|
||||||
|
reason: 'Headless-limited lane',
|
||||||
|
},
|
||||||
|
skills: {
|
||||||
|
status: 'unsupported',
|
||||||
|
ownership: 'shared',
|
||||||
|
reason: 'Headless-limited lane',
|
||||||
|
},
|
||||||
|
apiKeys: { status: 'supported', ownership: 'shared', reason: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
kind: 'codex-native',
|
||||||
|
label: 'Codex native',
|
||||||
|
authMethodDetail: 'API key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
supported: false,
|
||||||
|
authenticated: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ClaudeMultimodelBridgeService } =
|
||||||
|
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
|
||||||
|
const service = new ClaudeMultimodelBridgeService();
|
||||||
|
|
||||||
|
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator');
|
||||||
|
const codex = providers.find((provider) => provider.providerId === 'codex');
|
||||||
|
|
||||||
|
expect(codex).toMatchObject({
|
||||||
|
providerId: 'codex',
|
||||||
|
authenticated: true,
|
||||||
|
selectedBackendId: 'codex-native',
|
||||||
|
resolvedBackendId: 'codex-native',
|
||||||
|
backend: {
|
||||||
|
kind: 'codex-native',
|
||||||
|
label: 'Codex native',
|
||||||
|
},
|
||||||
|
availableBackends: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'auto',
|
||||||
|
selectable: true,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'codex-native',
|
||||||
|
selectable: false,
|
||||||
|
available: true,
|
||||||
|
statusMessage: 'Experimental native lane',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
externalRuntimeDiagnostics: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'codex-cli',
|
||||||
|
detected: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(codex?.capabilities.extensions.plugins).toMatchObject({
|
||||||
|
status: 'unsupported',
|
||||||
|
});
|
||||||
|
expect(isConnectionManagedRuntimeProvider(codex!)).toBe(false);
|
||||||
|
expect(getProviderConnectionModeSummary(codex!)).toBeNull();
|
||||||
|
expect(getProviderCurrentRuntimeSummary(codex!)).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,30 @@ vi.mock('@main/utils/shellEnv', () => ({
|
||||||
|
|
||||||
describe('ProviderConnectionService', () => {
|
describe('ProviderConnectionService', () => {
|
||||||
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
||||||
|
const originalCodexApiKey = process.env.CODEX_API_KEY;
|
||||||
|
|
||||||
function createConfig(authMode: 'auto' | 'oauth' | 'api_key' = 'auto') {
|
function createConfig(
|
||||||
|
authMode: 'auto' | 'oauth' | 'api_key' = 'auto',
|
||||||
|
overrides?: {
|
||||||
|
codexAuthMode?: 'oauth' | 'api_key';
|
||||||
|
codexApiKeyBetaEnabled?: boolean;
|
||||||
|
codexRuntimeBackend?: 'auto' | 'adapter' | 'api' | 'codex-native';
|
||||||
|
}
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode,
|
authMode,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
apiKeyBetaEnabled: false,
|
apiKeyBetaEnabled: overrides?.codexApiKeyBetaEnabled ?? false,
|
||||||
authMode: 'oauth' as const,
|
authMode: overrides?.codexAuthMode ?? ('oauth' as const),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
providerBackends: {
|
||||||
|
gemini: 'auto' as const,
|
||||||
|
codex: overrides?.codexRuntimeBackend ?? ('auto' as const),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -29,15 +43,22 @@ describe('ProviderConnectionService', () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
getCachedShellEnvMock.mockReturnValue({});
|
getCachedShellEnvMock.mockReturnValue({});
|
||||||
delete process.env.OPENAI_API_KEY;
|
delete process.env.OPENAI_API_KEY;
|
||||||
|
delete process.env.CODEX_API_KEY;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (originalOpenAiApiKey === undefined) {
|
if (originalOpenAiApiKey === undefined) {
|
||||||
delete process.env.OPENAI_API_KEY;
|
delete process.env.OPENAI_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.OPENAI_API_KEY = originalOpenAiApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalCodexApiKey === undefined) {
|
||||||
|
delete process.env.CODEX_API_KEY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.env.OPENAI_API_KEY = originalOpenAiApiKey;
|
process.env.CODEX_API_KEY = originalCodexApiKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
|
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
|
||||||
|
|
@ -284,15 +305,11 @@ describe('ProviderConnectionService', () => {
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => ({
|
getConfig: () => ({
|
||||||
providerConnections: {
|
...createConfig('auto', {
|
||||||
anthropic: {
|
codexApiKeyBetaEnabled: true,
|
||||||
authMode: 'auto',
|
codexAuthMode: 'api_key',
|
||||||
},
|
codexRuntimeBackend: 'api',
|
||||||
codex: {
|
}),
|
||||||
apiKeyBetaEnabled: true,
|
|
||||||
authMode: 'api_key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
@ -307,11 +324,11 @@ describe('ProviderConnectionService', () => {
|
||||||
|
|
||||||
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
|
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
|
||||||
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
|
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('api');
|
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
|
||||||
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('forces the Codex adapter and strips OPENAI_API_KEY in OAuth mode', async () => {
|
it('keeps the configured Codex backend and strips OPENAI_API_KEY in oauth mode', async () => {
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
|
@ -320,16 +337,10 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred: vi.fn().mockResolvedValue(null),
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => ({
|
getConfig: () => createConfig('auto', {
|
||||||
providerConnections: {
|
codexApiKeyBetaEnabled: true,
|
||||||
anthropic: {
|
codexAuthMode: 'oauth',
|
||||||
authMode: 'auto',
|
codexRuntimeBackend: 'api',
|
||||||
},
|
|
||||||
codex: {
|
|
||||||
apiKeyBetaEnabled: true,
|
|
||||||
authMode: 'oauth',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
@ -343,7 +354,7 @@ describe('ProviderConnectionService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.OPENAI_API_KEY).toBeUndefined();
|
expect(result.OPENAI_API_KEY).toBeUndefined();
|
||||||
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('adapter');
|
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('auto');
|
||||||
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -356,17 +367,12 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred: vi.fn().mockResolvedValue(null),
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => ({
|
getConfig: () =>
|
||||||
providerConnections: {
|
createConfig('auto', {
|
||||||
anthropic: {
|
codexApiKeyBetaEnabled: true,
|
||||||
authMode: 'auto',
|
codexAuthMode: 'api_key',
|
||||||
},
|
codexRuntimeBackend: 'api',
|
||||||
codex: {
|
}),
|
||||||
apiKeyBetaEnabled: true,
|
|
||||||
authMode: 'api_key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -376,7 +382,7 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(issue).toContain('OPENAI_API_KEY');
|
expect(issue).toContain('OPENAI_API_KEY');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('augments PTY env for Codex without deleting an existing OPENAI_API_KEY in oauth mode', async () => {
|
it('augments PTY env for Codex without rewriting the configured backend in oauth mode', async () => {
|
||||||
const { ProviderConnectionService } =
|
const { ProviderConnectionService } =
|
||||||
await import('@main/services/runtime/ProviderConnectionService');
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
|
@ -385,17 +391,12 @@ describe('ProviderConnectionService', () => {
|
||||||
lookupPreferred: vi.fn().mockResolvedValue(null),
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
||||||
} as never,
|
} as never,
|
||||||
{
|
{
|
||||||
getConfig: () => ({
|
getConfig: () =>
|
||||||
providerConnections: {
|
createConfig('auto', {
|
||||||
anthropic: {
|
codexApiKeyBetaEnabled: true,
|
||||||
authMode: 'auto',
|
codexAuthMode: 'oauth',
|
||||||
},
|
codexRuntimeBackend: 'api',
|
||||||
codex: {
|
}),
|
||||||
apiKeyBetaEnabled: true,
|
|
||||||
authMode: 'oauth',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
} as never
|
} as never
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -407,7 +408,106 @@ describe('ProviderConnectionService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.OPENAI_API_KEY).toBe('shell-key');
|
expect(result.OPENAI_API_KEY).toBe('shell-key');
|
||||||
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBe('adapter');
|
expect(result.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined();
|
||||||
expect(result.CLAUDE_CODE_CODEX_API_KEY_BETA).toBe('1');
|
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 () => {
|
||||||
|
getCachedShellEnvMock.mockReturnValue({
|
||||||
|
CODEX_API_KEY: 'native-key',
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'api_key',
|
||||||
|
codexRuntimeBackend: 'codex-native',
|
||||||
|
}),
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
const info = await service.getConnectionInfo('codex');
|
||||||
|
const issue = await service.getConfiguredConnectionIssue(
|
||||||
|
{
|
||||||
|
CODEX_API_KEY: 'native-key',
|
||||||
|
},
|
||||||
|
'codex'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(info.apiKeyConfigured).toBe(true);
|
||||||
|
expect(info.apiKeySource).toBe('environment');
|
||||||
|
expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY');
|
||||||
|
expect(issue).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,102 @@ describe('BoardTaskExactLogStrictParser', () => {
|
||||||
|
|
||||||
expect(parsed.get(filePath)?.map((message) => message.uuid)).toEqual(['good-ts']);
|
expect(parsed.get(filePath)?.map((message) => message.uuid)).toEqual(['good-ts']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves codex-native replay and history authority rows for exact-log readers', async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'exact-log-parser-native-'));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, 'native-session.jsonl');
|
||||||
|
await fs.writeFile(
|
||||||
|
filePath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-ephemeral',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-warning-1',
|
||||||
|
timestamp: '2026-04-19T10:00:00.000Z',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
level: 'warning',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'thread/read failed while backfilling turn items for turn completion',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-ephemeral',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-summary-ephemeral',
|
||||||
|
timestamp: '2026-04-19T10:00:01.000Z',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content:
|
||||||
|
'Codex native execution summary: thread=thread-ephemeral, completion=ephemeral, history=live-only, usageAuthority=live-turn-completed, binary=codex-cli 0.117.0',
|
||||||
|
codexNativeThreadId: 'thread-ephemeral',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-persistent',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'native-summary-persistent',
|
||||||
|
timestamp: '2026-04-19T10:00:02.000Z',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content:
|
||||||
|
'Codex native execution summary: thread=thread-persistent, completion=persistent, history=explicit-hydration-required, usageAuthority=live-turn-completed, binary=codex-cli 0.117.0',
|
||||||
|
codexNativeThreadId: 'thread-persistent',
|
||||||
|
codexNativeCompletionPolicy: 'persistent',
|
||||||
|
codexNativeHistoryCompleteness: 'explicit-hydration-required',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = await new BoardTaskExactLogStrictParser().parseFiles([filePath]);
|
||||||
|
|
||||||
|
expect(parsed.get(filePath)).toMatchObject([
|
||||||
|
{
|
||||||
|
uuid: 'native-warning-1',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: 'native-summary-ephemeral',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: 'native-summary-persistent',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeCompletionPolicy: 'persistent',
|
||||||
|
codexNativeHistoryCompleteness: 'explicit-hydration-required',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -276,5 +276,145 @@ describe('jsonl', () => {
|
||||||
expect(parsed?.sourceToolUseID).toBe('call-bash-real');
|
expect(parsed?.sourceToolUseID).toBe('call-bash-real');
|
||||||
expect(parsed?.toolResults[0]?.toolUseId).toBe('call-bash-real');
|
expect(parsed?.toolResults[0]?.toolUseId).toBe('call-bash-real');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses codex-native projected assistant rows with usage intact', () => {
|
||||||
|
const parsed = parseJsonlLine(
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: 'user-1',
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-1',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'assistant',
|
||||||
|
uuid: 'assistant-native-1',
|
||||||
|
requestId: 'native-request-1',
|
||||||
|
timestamp: '2026-04-19T10:00:00.000Z',
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'gpt-5-codex',
|
||||||
|
id: 'msg-native-1',
|
||||||
|
type: 'message',
|
||||||
|
stop_reason: 'end_turn',
|
||||||
|
stop_sequence: null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
content: [{ type: 'text', text: 'OK' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsed).toMatchObject({
|
||||||
|
uuid: 'assistant-native-1',
|
||||||
|
type: 'assistant',
|
||||||
|
content: [{ type: 'text', text: 'OK' }],
|
||||||
|
requestId: 'native-request-1',
|
||||||
|
usage: {
|
||||||
|
input_tokens: 12,
|
||||||
|
cache_read_input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses modern system warning rows without dropping content or severity', () => {
|
||||||
|
const parsed = parseJsonlLine(
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-1',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'system-native-warning-1',
|
||||||
|
timestamp: '2026-04-19T10:00:01.000Z',
|
||||||
|
subtype: 'informational',
|
||||||
|
level: 'warning',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'native stderr warning',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsed).toMatchObject({
|
||||||
|
uuid: 'system-native-warning-1',
|
||||||
|
type: 'system',
|
||||||
|
content: 'native stderr warning',
|
||||||
|
level: 'warning',
|
||||||
|
subtype: 'informational',
|
||||||
|
isMeta: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses codex-native execution-summary and warning metadata from projected system rows', () => {
|
||||||
|
const warningParsed = parseJsonlLine(
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-warning',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'system-native-warning-2',
|
||||||
|
timestamp: '2026-04-19T10:00:02.000Z',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
level: 'warning',
|
||||||
|
isMeta: false,
|
||||||
|
content: 'thread/read failed while backfilling turn items for turn completion',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warningParsed).toMatchObject({
|
||||||
|
uuid: 'system-native-warning-2',
|
||||||
|
subtype: 'codex_native_warning',
|
||||||
|
codexNativeWarningSource: 'history',
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryParsed = parseJsonlLine(
|
||||||
|
JSON.stringify({
|
||||||
|
parentUuid: null,
|
||||||
|
isSidechain: false,
|
||||||
|
userType: 'external',
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
sessionId: 'session-native-summary',
|
||||||
|
version: '1.0.0',
|
||||||
|
gitBranch: 'main',
|
||||||
|
type: 'system',
|
||||||
|
uuid: 'system-native-summary-1',
|
||||||
|
timestamp: '2026-04-19T10:00:03.000Z',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
level: 'info',
|
||||||
|
isMeta: false,
|
||||||
|
content:
|
||||||
|
'Codex native execution summary: thread=thread-ephemeral, completion=ephemeral, history=live-only, usageAuthority=live-turn-completed, binary=codex-cli 0.117.0',
|
||||||
|
codexNativeThreadId: 'thread-ephemeral',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutablePath: '/usr/local/bin/codex',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(summaryParsed).toMatchObject({
|
||||||
|
uuid: 'system-native-summary-1',
|
||||||
|
subtype: 'codex_native_execution_summary',
|
||||||
|
codexNativeThreadId: 'thread-ephemeral',
|
||||||
|
codexNativeCompletionPolicy: 'ephemeral',
|
||||||
|
codexNativeHistoryCompleteness: 'live-only',
|
||||||
|
codexNativeFinalUsageAuthority: 'live-turn-completed',
|
||||||
|
codexNativeExecutableSource: 'system-path',
|
||||||
|
codexNativeExecutableVersion: 'codex-cli 0.117.0',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
getProviderConnectionModeSummary,
|
getProviderConnectionModeSummary,
|
||||||
getProviderCredentialSummary,
|
getProviderCredentialSummary,
|
||||||
getProviderCurrentRuntimeSummary,
|
getProviderCurrentRuntimeSummary,
|
||||||
|
isConnectionManagedRuntimeProvider,
|
||||||
} from '@renderer/components/runtime/providerConnectionUi';
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||||
|
|
||||||
|
|
@ -51,6 +52,10 @@ function createCodexProvider(
|
||||||
overrides?: Partial<CliProviderStatus['connection']> & {
|
overrides?: Partial<CliProviderStatus['connection']> & {
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
authMethod?: string | null;
|
authMethod?: string | null;
|
||||||
|
selectedBackendId?: string | null;
|
||||||
|
resolvedBackendId?: string | null;
|
||||||
|
availableBackends?: CliProviderStatus['availableBackends'];
|
||||||
|
backend?: CliProviderStatus['backend'];
|
||||||
}
|
}
|
||||||
): CliProviderStatus {
|
): CliProviderStatus {
|
||||||
return {
|
return {
|
||||||
|
|
@ -68,14 +73,16 @@ function createCodexProvider(
|
||||||
oneShot: true,
|
oneShot: true,
|
||||||
extensions: createDefaultCliExtensionCapabilities(),
|
extensions: createDefaultCliExtensionCapabilities(),
|
||||||
},
|
},
|
||||||
selectedBackendId: 'auto',
|
selectedBackendId: overrides?.selectedBackendId ?? 'auto',
|
||||||
resolvedBackendId: 'adapter',
|
resolvedBackendId: overrides?.resolvedBackendId ?? 'adapter',
|
||||||
availableBackends: [],
|
availableBackends: overrides?.availableBackends ?? [],
|
||||||
externalRuntimeDiagnostics: [],
|
externalRuntimeDiagnostics: [],
|
||||||
backend: {
|
backend:
|
||||||
kind: 'adapter',
|
overrides?.backend ??
|
||||||
label: 'Codex subscription',
|
({
|
||||||
},
|
kind: 'adapter',
|
||||||
|
label: 'Codex subscription',
|
||||||
|
} satisfies NonNullable<CliProviderStatus['backend']>),
|
||||||
connection: {
|
connection: {
|
||||||
supportsOAuth: true,
|
supportsOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
|
|
@ -185,4 +192,64 @@ describe('providerConnectionUi', () => {
|
||||||
'OpenAI API key is saved in Manage. Enable API key mode to use it.'
|
'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', () => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue