fix(codex): propagate native runtime context
This commit is contained in:
parent
bef34983f5
commit
e87ef2dd85
5 changed files with 192 additions and 0 deletions
|
|
@ -64,6 +64,11 @@ export interface CodexLoginStateDto {
|
||||||
startedAt: string | null;
|
startedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodexRuntimeContextDto {
|
||||||
|
binaryPath: string | null;
|
||||||
|
codexHome: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CodexAccountSnapshotDto {
|
export interface CodexAccountSnapshotDto {
|
||||||
preferredAuthMode: CodexAccountAuthMode;
|
preferredAuthMode: CodexAccountAuthMode;
|
||||||
effectiveAuthMode: CodexAccountEffectiveAuthMode;
|
effectiveAuthMode: CodexAccountEffectiveAuthMode;
|
||||||
|
|
@ -77,6 +82,7 @@ export interface CodexAccountSnapshotDto {
|
||||||
requiresOpenaiAuth: boolean | null;
|
requiresOpenaiAuth: boolean | null;
|
||||||
localAccountArtifactsPresent?: boolean;
|
localAccountArtifactsPresent?: boolean;
|
||||||
localActiveChatgptAccountPresent?: boolean;
|
localActiveChatgptAccountPresent?: boolean;
|
||||||
|
runtimeContext?: CodexRuntimeContextDto;
|
||||||
login: CodexLoginStateDto;
|
login: CodexLoginStateDto;
|
||||||
rateLimits: CodexRateLimitSnapshotDto | null;
|
rateLimits: CodexRateLimitSnapshotDto | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ interface CodexLastKnownRateLimits {
|
||||||
observedAt: number;
|
observedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CodexRuntimeContext {
|
||||||
|
binaryPath: string | null;
|
||||||
|
codexHome: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodexLastKnownRuntimeContext {
|
||||||
|
payload: CodexRuntimeContext;
|
||||||
|
observedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface CodexSnapshotRefreshOptions {
|
interface CodexSnapshotRefreshOptions {
|
||||||
includeRateLimits: boolean;
|
includeRateLimits: boolean;
|
||||||
forceRefreshToken: boolean;
|
forceRefreshToken: boolean;
|
||||||
|
|
@ -130,6 +140,16 @@ function asRateLimits(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRuntimeContext(
|
||||||
|
binaryPath: string | null | undefined,
|
||||||
|
codexHome: string | null | undefined
|
||||||
|
): CodexRuntimeContext {
|
||||||
|
return {
|
||||||
|
binaryPath: binaryPath?.trim() || null,
|
||||||
|
codexHome: codexHome?.trim() || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getPreferredAuthMode(configManager: {
|
function getPreferredAuthMode(configManager: {
|
||||||
getConfig: () => {
|
getConfig: () => {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
|
|
@ -236,6 +256,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
private pendingRefreshOptions: CodexSnapshotRefreshOptions | null = null;
|
private pendingRefreshOptions: CodexSnapshotRefreshOptions | null = null;
|
||||||
private lastKnownAccount: CodexLastKnownAccount | null = null;
|
private lastKnownAccount: CodexLastKnownAccount | null = null;
|
||||||
private lastKnownRateLimits: CodexLastKnownRateLimits | null = null;
|
private lastKnownRateLimits: CodexLastKnownRateLimits | null = null;
|
||||||
|
private lastKnownRuntimeContext: CodexLastKnownRuntimeContext | null = null;
|
||||||
private mutationQueue: Promise<void> = Promise.resolve();
|
private mutationQueue: Promise<void> = Promise.resolve();
|
||||||
private mutationQueueRelease: (() => void) | null = null;
|
private mutationQueueRelease: (() => void) | null = null;
|
||||||
private activeMutationCount = 0;
|
private activeMutationCount = 0;
|
||||||
|
|
@ -372,6 +393,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
this.pendingRefreshOptions = null;
|
this.pendingRefreshOptions = null;
|
||||||
this.lastKnownAccount = null;
|
this.lastKnownAccount = null;
|
||||||
this.lastKnownRateLimits = null;
|
this.lastKnownRateLimits = null;
|
||||||
|
this.lastKnownRuntimeContext = null;
|
||||||
this.activeMutationCount = 0;
|
this.activeMutationCount = 0;
|
||||||
if (this.mutationQueueRelease) {
|
if (this.mutationQueueRelease) {
|
||||||
this.mutationQueueRelease();
|
this.mutationQueueRelease();
|
||||||
|
|
@ -441,6 +463,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
let appServerStatusMessage: string | null = null;
|
let appServerStatusMessage: string | null = null;
|
||||||
let accountPayload = this.lastKnownAccount?.payload ?? null;
|
let accountPayload = this.lastKnownAccount?.payload ?? null;
|
||||||
let requiresOpenaiAuth: boolean | null = accountPayload?.requiresOpenaiAuth ?? null;
|
let requiresOpenaiAuth: boolean | null = accountPayload?.requiresOpenaiAuth ?? null;
|
||||||
|
let runtimeContext = createRuntimeContext(binaryPath, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accountResult = await this.appServerClient.readAccount({
|
const accountResult = await this.appServerClient.readAccount({
|
||||||
|
|
@ -448,6 +471,13 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
env,
|
env,
|
||||||
refreshToken: options?.forceRefreshToken ?? false,
|
refreshToken: options?.forceRefreshToken ?? false,
|
||||||
});
|
});
|
||||||
|
runtimeContext = createRuntimeContext(binaryPath, accountResult.initialize.codexHome);
|
||||||
|
if (runtimeContext.codexHome) {
|
||||||
|
this.lastKnownRuntimeContext = {
|
||||||
|
payload: runtimeContext,
|
||||||
|
observedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
const canReuseLastKnownManagedAccount =
|
const canReuseLastKnownManagedAccount =
|
||||||
options?.forceRefreshToken !== true &&
|
options?.forceRefreshToken !== true &&
|
||||||
localActiveChatgptAccountPresent &&
|
localActiveChatgptAccountPresent &&
|
||||||
|
|
@ -483,6 +513,14 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
accountPayload = this.lastKnownAccount.payload;
|
accountPayload = this.lastKnownAccount.payload;
|
||||||
requiresOpenaiAuth = this.lastKnownAccount.payload.requiresOpenaiAuth;
|
requiresOpenaiAuth = this.lastKnownAccount.payload.requiresOpenaiAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastKnownRuntimeContext &&
|
||||||
|
now - this.lastKnownRuntimeContext.observedAt <= LAST_KNOWN_GOOD_MANAGED_ACCOUNT_TTL_MS &&
|
||||||
|
this.lastKnownRuntimeContext.payload.binaryPath === binaryPath
|
||||||
|
) {
|
||||||
|
runtimeContext = this.lastKnownRuntimeContext.payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rateLimits: CodexRateLimitSnapshotDto | null = null;
|
let rateLimits: CodexRateLimitSnapshotDto | null = null;
|
||||||
|
|
@ -542,6 +580,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
requiresOpenaiAuth,
|
requiresOpenaiAuth,
|
||||||
localAccountArtifactsPresent,
|
localAccountArtifactsPresent,
|
||||||
localActiveChatgptAccountPresent,
|
localActiveChatgptAccountPresent,
|
||||||
|
runtimeContext,
|
||||||
login,
|
login,
|
||||||
rateLimits,
|
rateLimits,
|
||||||
updatedAt: new Date(now).toISOString(),
|
updatedAt: new Date(now).toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ const PROVIDER_API_KEY_ENV_VARS: Partial<Record<CliProviderId, string>> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const CODEX_NATIVE_API_KEY_ENV_VAR = 'CODEX_API_KEY';
|
const CODEX_NATIVE_API_KEY_ENV_VAR = 'CODEX_API_KEY';
|
||||||
|
const CODEX_CLI_PATH_ENV_VAR = 'CODEX_CLI_PATH';
|
||||||
|
const CODEX_HOME_ENV_VAR = 'CODEX_HOME';
|
||||||
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
|
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
|
||||||
|
|
||||||
function isCodexExecBinary(binaryPath?: string | null): boolean {
|
function isCodexExecBinary(binaryPath?: string | null): boolean {
|
||||||
|
|
@ -85,6 +87,21 @@ function buildCodexForcedLoginLaunchArgs(
|
||||||
return ['--settings', JSON.stringify({ codex: { forced_login_method: loginMethod } })];
|
return ['--settings', JSON.stringify({ codex: { forced_login_method: loginMethod } })];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyCodexRuntimeContextEnv(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
snapshot: CodexAccountSnapshotDto
|
||||||
|
): void {
|
||||||
|
const binaryPath = snapshot.runtimeContext?.binaryPath?.trim();
|
||||||
|
if (binaryPath) {
|
||||||
|
env[CODEX_CLI_PATH_ENV_VAR] = binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codexHome = snapshot.runtimeContext?.codexHome?.trim();
|
||||||
|
if (codexHome) {
|
||||||
|
env[CODEX_HOME_ENV_VAR] = codexHome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ProviderConnectionService {
|
export class ProviderConnectionService {
|
||||||
private static instance: ProviderConnectionService | null = null;
|
private static instance: ProviderConnectionService | null = null;
|
||||||
private codexAccountFeature: Pick<CodexAccountFeatureFacade, 'getSnapshot'> | null = null;
|
private codexAccountFeature: Pick<CodexAccountFeatureFacade, 'getSnapshot'> | null = null;
|
||||||
|
|
@ -179,6 +196,7 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = this.mergeCodexApiKeyAvailability(await this.getCodexAccountSnapshot(), env);
|
const snapshot = this.mergeCodexApiKeyAvailability(await this.getCodexAccountSnapshot(), env);
|
||||||
|
applyCodexRuntimeContextEnv(env, snapshot);
|
||||||
const readiness = evaluateCodexLaunchReadiness({
|
const readiness = evaluateCodexLaunchReadiness({
|
||||||
preferredAuthMode: snapshot.preferredAuthMode,
|
preferredAuthMode: snapshot.preferredAuthMode,
|
||||||
managedAccount: snapshot.managedAccount,
|
managedAccount: snapshot.managedAccount,
|
||||||
|
|
@ -239,6 +257,7 @@ export class ProviderConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = this.mergeCodexApiKeyAvailability(await this.getCodexAccountSnapshot(), env);
|
const snapshot = this.mergeCodexApiKeyAvailability(await this.getCodexAccountSnapshot(), env);
|
||||||
|
applyCodexRuntimeContextEnv(env, snapshot);
|
||||||
const readiness = evaluateCodexLaunchReadiness({
|
const readiness = evaluateCodexLaunchReadiness({
|
||||||
preferredAuthMode: snapshot.preferredAuthMode,
|
preferredAuthMode: snapshot.preferredAuthMode,
|
||||||
managedAccount: snapshot.managedAccount,
|
managedAccount: snapshot.managedAccount,
|
||||||
|
|
@ -566,6 +585,10 @@ export class ProviderConnectionService {
|
||||||
requiresOpenaiAuth: null,
|
requiresOpenaiAuth: null,
|
||||||
localAccountArtifactsPresent: false,
|
localAccountArtifactsPresent: false,
|
||||||
localActiveChatgptAccountPresent: false,
|
localActiveChatgptAccountPresent: false,
|
||||||
|
runtimeContext: {
|
||||||
|
binaryPath: null,
|
||||||
|
codexHome: null,
|
||||||
|
},
|
||||||
login: {
|
login: {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,10 @@ describe('createCodexAccountFeature', () => {
|
||||||
source: 'environment',
|
source: 'environment',
|
||||||
sourceLabel: 'Detected from OPENAI_API_KEY',
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
||||||
},
|
},
|
||||||
|
runtimeContext: {
|
||||||
|
binaryPath: '/usr/local/bin/codex',
|
||||||
|
codexHome: '/Users/test/.codex',
|
||||||
|
},
|
||||||
launchAllowed: true,
|
launchAllowed: true,
|
||||||
launchReadinessState: 'ready_both',
|
launchReadinessState: 'ready_both',
|
||||||
});
|
});
|
||||||
|
|
@ -315,6 +319,10 @@ describe('createCodexAccountFeature', () => {
|
||||||
type: 'chatgpt',
|
type: 'chatgpt',
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
});
|
});
|
||||||
|
expect(degradedSnapshot.runtimeContext).toEqual({
|
||||||
|
binaryPath: '/usr/local/bin/codex',
|
||||||
|
codexHome: '/Users/test/.codex',
|
||||||
|
});
|
||||||
expect(degradedSnapshot.launchAllowed).toBe(true);
|
expect(degradedSnapshot.launchAllowed).toBe(true);
|
||||||
expect(logger.warn).not.toHaveBeenCalledWith(
|
expect(logger.warn).not.toHaveBeenCalledWith(
|
||||||
expect.stringContaining('false logout'),
|
expect.stringContaining('false logout'),
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,122 @@ describe('ProviderConnectionService', () => {
|
||||||
expect(result.CODEX_API_KEY).toBe('shell-openai-key');
|
expect(result.CODEX_API_KEY).toBe('shell-openai-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes Codex runtime context while clearing API keys for ChatGPT launches', 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
|
||||||
|
);
|
||||||
|
|
||||||
|
service.setCodexAccountFeature({
|
||||||
|
getSnapshot: vi.fn().mockResolvedValue({
|
||||||
|
preferredAuthMode: 'chatgpt',
|
||||||
|
effectiveAuthMode: 'chatgpt',
|
||||||
|
launchAllowed: true,
|
||||||
|
launchIssueMessage: null,
|
||||||
|
launchReadinessState: 'ready_chatgpt',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
appServerStatusMessage: null,
|
||||||
|
managedAccount: {
|
||||||
|
type: 'chatgpt',
|
||||||
|
email: 'user@example.com',
|
||||||
|
planType: 'pro',
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
available: false,
|
||||||
|
source: null,
|
||||||
|
sourceLabel: null,
|
||||||
|
},
|
||||||
|
requiresOpenaiAuth: false,
|
||||||
|
runtimeContext: {
|
||||||
|
binaryPath: '/opt/codex/bin/codex',
|
||||||
|
codexHome: '/Users/tester/.codex-custom',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
startedAt: null,
|
||||||
|
},
|
||||||
|
rateLimits: null,
|
||||||
|
updatedAt: '2026-04-20T00:00:00.000Z',
|
||||||
|
}),
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const result = await service.applyConfiguredConnectionEnv(
|
||||||
|
{
|
||||||
|
OPENAI_API_KEY: 'ambient-openai-key',
|
||||||
|
CODEX_API_KEY: 'ambient-codex-key',
|
||||||
|
},
|
||||||
|
'codex'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.OPENAI_API_KEY).toBeUndefined();
|
||||||
|
expect(result.CODEX_API_KEY).toBeUndefined();
|
||||||
|
expect(result.CODEX_CLI_PATH).toBe('/opt/codex/bin/codex');
|
||||||
|
expect(result.CODEX_HOME).toBe('/Users/tester/.codex-custom');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Codex runtime context when API-key mode mirrors credentials', async () => {
|
||||||
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||||
|
envVarName: 'OPENAI_API_KEY',
|
||||||
|
value: 'stored-openai-key',
|
||||||
|
});
|
||||||
|
const { ProviderConnectionService } =
|
||||||
|
await import('@main/services/runtime/ProviderConnectionService');
|
||||||
|
|
||||||
|
const service = new ProviderConnectionService(
|
||||||
|
{
|
||||||
|
lookupPreferred,
|
||||||
|
} as never,
|
||||||
|
{
|
||||||
|
getConfig: () => createConfig('auto'),
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
service.setCodexAccountFeature({
|
||||||
|
getSnapshot: vi.fn().mockResolvedValue({
|
||||||
|
preferredAuthMode: 'api_key',
|
||||||
|
effectiveAuthMode: 'api_key',
|
||||||
|
launchAllowed: true,
|
||||||
|
launchIssueMessage: null,
|
||||||
|
launchReadinessState: 'ready_api_key',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
appServerStatusMessage: null,
|
||||||
|
managedAccount: null,
|
||||||
|
apiKey: {
|
||||||
|
available: true,
|
||||||
|
source: 'stored',
|
||||||
|
sourceLabel: 'Stored in app',
|
||||||
|
},
|
||||||
|
requiresOpenaiAuth: false,
|
||||||
|
runtimeContext: {
|
||||||
|
binaryPath: '/opt/codex/bin/codex.cmd',
|
||||||
|
codexHome: 'C:\\Users\\tester\\.codex',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
startedAt: null,
|
||||||
|
},
|
||||||
|
rateLimits: null,
|
||||||
|
updatedAt: '2026-04-20T00:00:00.000Z',
|
||||||
|
}),
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const result = await service.applyConfiguredConnectionEnv({}, 'codex');
|
||||||
|
|
||||||
|
expect(result.OPENAI_API_KEY).toBe('stored-openai-key');
|
||||||
|
expect(result.CODEX_API_KEY).toBe('stored-openai-key');
|
||||||
|
expect(result.CODEX_CLI_PATH).toBe('/opt/codex/bin/codex.cmd');
|
||||||
|
expect(result.CODEX_HOME).toBe('C:\\Users\\tester\\.codex');
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts CODEX_API_KEY as the native external credential source for Codex', async () => {
|
it('accepts CODEX_API_KEY as the native external credential source for Codex', async () => {
|
||||||
getCachedShellEnvMock.mockReturnValue({
|
getCachedShellEnvMock.mockReturnValue({
|
||||||
CODEX_API_KEY: 'native-key',
|
CODEX_API_KEY: 'native-key',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue