diff --git a/src/main/agent-hooks/managed-agent-hook-controls.ts b/src/main/agent-hooks/managed-agent-hook-controls.ts index e3070afed3e..1611aed7f00 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.ts @@ -77,6 +77,17 @@ export function isAgentStatusHooksEnabled( return settings?.agentStatusHooksEnabled !== false } +/** + * Why: opt-in setting that makes Codex use its default config home (~/.codex) + * by skipping the managed CODEX_HOME injection. Default off (undefined/false) + * keeps Orca's managed runtime home and its account hot-swap behavior. + */ +export function isCodexDefaultHomeEnabled( + settings: Pick | null | undefined +): boolean { + return settings?.codexUseDefaultConfigDir === true +} + export function installManagedAgentHooks(): void { for (const [agent, install] of MANAGED_AGENT_HOOK_INSTALLERS) { try { diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index cfee7b8ad10..cdbd53a9016 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -736,6 +736,47 @@ describe('CodexRuntimeHomeService', () => { expect(existsSync(getRuntimeCodexHomePath())).toBe(true) }) + it('targets the default ~/.codex home for launch and quota fetch when the opt-in is on', async () => { + // Why: closes the background revocation gap — when codexUseDefaultConfigDir + // is on, the quota poller and any host launch must resolve null (the system + // ~/.codex home) instead of spawning Codex against the managed runtime home, + // which would refresh and revoke the user's single-OAuth-session token. + const managedHomePath = createManagedAuth( + testState.userDataDir, + 'account-1', + createCodexAuthJson('one@example.com', 'acct-1', 'one') + ) + const store = createStore( + createSettings({ + codexUseDefaultConfigDir: true, + codexManagedAccounts: [ + { + id: 'account-1', + email: 'one@example.com', + managedHomePath, + providerAccountId: 'acct-1', + workspaceLabel: null, + workspaceAccountId: 'acct-1', + createdAt: 1, + updatedAt: 1, + lastAuthenticatedAt: 1 + } + ], + activeCodexManagedAccountId: 'account-1', + activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} } + }) + ) + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const service = new CodexRuntimeHomeService(store as never) + + // Why: null is how callers (pty env build, rate-limit fetcher, commit-msg + // agent) resolve the system ~/.codex home instead of an Orca-managed + // CODEX_HOME, so the background poller never spawns Codex against the + // managed home and never refreshes/revokes the ~/.codex OAuth session. + expect(service.prepareForRateLimitFetch()).toBeNull() + expect(service.prepareForCodexLaunch()).toBeNull() + }) + it('uses the same host CODEX_HOME after switching managed Codex accounts', async () => { const runtimeAuthPath = getRuntimeCodexAuthPath() const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one') diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 12d119a8510..9a1429fa2d9 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -136,6 +136,14 @@ export class CodexRuntimeHomeService { this.getWslSystemCodexHomePath(wslTarget) ) } + // Why: with the default-home opt-in, every host Codex launch (terminals, + // commit-message agent) must resolve ~/.codex. Returning null tells callers + // to inject no CODEX_HOME, and skipping the managed-home sync means Orca + // never spawns Codex against the managed home to drive a token refresh that + // would revoke the user's single-OAuth-session ~/.codex login. + if (this.isDefaultConfigDirEnabled()) { + return null + } this.syncForCurrentSelection() syncSystemCodexResourcesIntoManagedHome() syncSystemConfigIntoManagedCodexHome() @@ -189,12 +197,24 @@ export class CodexRuntimeHomeService { this.getWslSystemCodexHomePath(wslTarget) ) } + // Why: when the user opts into the default ~/.codex home, the background + // quota poller must NOT spawn Codex against the managed home — doing so + // refreshes the managed home's single-use OAuth token and revokes the + // ~/.codex session. Return null so the fetcher targets the system home and + // skip the managed-home sync entirely (no auth.json read-back to mutate). + if (this.isDefaultConfigDirEnabled()) { + return null + } this.syncForCurrentSelection() syncSystemCodexResourcesIntoManagedHome() syncSystemConfigIntoManagedCodexHome() return this.getRuntimeHomePath() } + private isDefaultConfigDirEnabled(): boolean { + return this.store.getSettings().codexUseDefaultConfigDir === true + } + syncForCurrentSelection(target?: CodexAccountSelectionTarget): void { if (target?.runtime === 'wsl') { this.syncWslRuntimeForCurrentSelection(target) diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 4cb6aa044c2..b15dafbcea9 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -547,6 +547,7 @@ describe('registerPtyHandlers', () => { getSettings?: () => { enableGitHubAttribution?: boolean agentStatusHooksEnabled?: boolean + codexUseDefaultConfigDir?: boolean httpProxyUrl?: string httpProxyBypassRules?: string }, @@ -697,6 +698,34 @@ describe('registerPtyHandlers', () => { expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME) }) + it('skips the managed Codex home when codexUseDefaultConfigDir is enabled', async () => { + // Why: the opt-in setting must force Codex back to its default ~/.codex + // home by injecting NO CODEX_HOME, even though a managed home is selected + // (getSelectedCodexHomePath returns TEST_CODEX_HOME here). Any ambient + // CODEX_HOME/ORCA_CODEX_HOME must also be stripped, not left to leak in. + const env = await spawnAndGetEnv( + { CODEX_HOME: '/tmp/ambient-codex-home', ORCA_CODEX_HOME: '/tmp/ambient-orca-codex-home' }, + undefined, + () => TEST_CODEX_HOME, + () => ({ codexUseDefaultConfigDir: true }) + ) + expect(env.CODEX_HOME).toBeUndefined() + expect(env.ORCA_CODEX_HOME).toBeUndefined() + }) + + it('still injects the managed Codex home when codexUseDefaultConfigDir is off', async () => { + // Why: guards the OR condition wired into skipCodexHomeEnv — a false (or + // absent) setting must preserve the existing managed-home injection. + const env = await spawnAndGetEnv( + undefined, + undefined, + () => TEST_CODEX_HOME, + () => ({ codexUseDefaultConfigDir: false }) + ) + expect(env.CODEX_HOME).toBe(TEST_CODEX_HOME) + expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME) + }) + it('injects the OpenCode hook env into Orca terminal PTYs', async () => { // Why: clear any ambient OPENCODE_CONFIG_DIR so the mock's value is used const env = await spawnAndGetEnv(undefined, { OPENCODE_CONFIG_DIR: undefined }) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index b771e82acaf..82913272519 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -30,7 +30,10 @@ import { getFirstCommandToken } from '../../shared/command-token-scanner' import { agentHookServer } from '../agent-hooks/server' -import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' +import { + isAgentStatusHooksEnabled, + isCodexDefaultHomeEnabled +} from '../agent-hooks/managed-agent-hook-controls' import { piTitlebarExtensionService } from '../pi/titlebar-extension-service' import { detectPiAgentKindFromCommand, type PiAgentKind } from '../../shared/pi-agent-kind' import { isPwshAvailable } from '../pwsh' @@ -1222,7 +1225,9 @@ export function registerPtyHandlers( isPackaged: app.isPackaged, userDataPath: app.getPath('userData'), selectedCodexHomePath, - skipCodexHomeEnv: ctx?.isWsl === true && !selectedCodexHomePath, + skipCodexHomeEnv: + isCodexDefaultHomeEnabled(getSettings?.()) || + (ctx?.isWsl === true && !selectedCodexHomePath), githubAttributionEnabled: getSettings?.()?.enableGitHubAttribution ?? false, launchCommand: ctx?.command, shellPath: ctx?.shellPath, @@ -1833,9 +1838,10 @@ export function registerPtyHandlers( ) : null const skipCodexHomeEnv = - isDaemonHostSpawn && - shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, args.cwd) && - !selectedCodexHomePath + isCodexDefaultHomeEnabled(getSettings?.()) || + (isDaemonHostSpawn && + shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, args.cwd) && + !selectedCodexHomePath) if (isDaemonHostSpawn && sessionId) { if (!isSafePtySessionId(sessionId, app.getPath('userData'))) { throw new Error('Invalid PTY session id') @@ -2475,9 +2481,10 @@ export function registerPtyHandlers( ) : null const skipCodexHomeEnv = - isDaemonHostSpawn && - shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, args.cwd) && - !selectedCodexHomePath + isCodexDefaultHomeEnabled(getSettings?.()) || + (isDaemonHostSpawn && + shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, args.cwd) && + !selectedCodexHomePath) if (isDaemonHostSpawn) { if (effectiveSessionId === undefined) { // Should be unreachable: the expression above returns a string when diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx index 35540a2b880..1ed9e27d4f0 100644 --- a/src/renderer/src/components/settings/AccountsPane.tsx +++ b/src/renderer/src/components/settings/AccountsPane.tsx @@ -28,7 +28,7 @@ import { getAccountsPaneSearchEntries } from './accounts-search' import { SearchableSetting } from './SearchableSetting' -import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls' +import { SettingsRow, SettingsSegmentedControl, SettingsSwitchRow } from './SettingsFormControls' import { matchesSettingsSearch } from './settings-search' import { markLiveCodexSessionsForRestart } from '@/lib/codex-session-restart' import { @@ -1109,6 +1109,40 @@ export function AccountsPane({ )} + + + + updateSettings({ + codexUseDefaultConfigDir: settings.codexUseDefaultConfigDir !== true + }) + } + ariaLabel={translate( + 'auto.components.settings.AccountsPane.2eb471df34', + 'Use default Codex config directory' + )} + /> + ) : null, matchesSettingsSearch(searchQuery, getAccountsGeminiSearchEntries()) ? ( diff --git a/src/renderer/src/components/settings/accounts-search.ts b/src/renderer/src/components/settings/accounts-search.ts index 1d4e1564553..01a03c50a1b 100644 --- a/src/renderer/src/components/settings/accounts-search.ts +++ b/src/renderer/src/components/settings/accounts-search.ts @@ -93,6 +93,24 @@ export const getAccountsCodexSearchEntries = createLocalizedCatalog(() => [ ...translateSearchKeyword('auto.components.settings.accounts.search.f2d666a886', 'optional'), ...translateSearchKeyword('auto.components.settings.accounts.search.35b461d817', 'sign in') ] + }, + { + title: translate( + 'auto.components.settings.accounts.search.d6347a2658', + 'Default Codex config directory' + ), + description: translate( + 'auto.components.settings.accounts.search.edd9a67f8d', + "Run Codex with your system ~/.codex config instead of Orca's managed home." + ), + keywords: [ + ...translateSearchKeyword('auto.components.settings.accounts.search.70d1b8def5', 'codex'), + ...translateSearchKeyword('auto.components.settings.accounts.search.e85a7f3385', 'default'), + ...translateSearchKeyword('auto.components.settings.accounts.search.397ba215c5', 'config'), + ...translateSearchKeyword('auto.components.settings.accounts.search.67a07ebd35', 'directory'), + ...translateSearchKeyword('auto.components.settings.accounts.search.5d23eca2a7', 'home'), + ...translateSearchKeyword('auto.components.settings.accounts.search.c919fa7390', 'managed') + ] } ]) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index f082ae69916..4e8235660cd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -4459,7 +4459,9 @@ "75ca9b718e": "Codex reported that the active account needs a fresh sign-in. Re-authenticate it before starting new Codex sessions.", "b11078a9c2": "wsl", "350b2a1aa7": "Use your current", - "e05d0ff737": "Use your current {{value0}} Claude login." + "e05d0ff737": "Use your current {{value0}} Claude login.", + "2eb471df34": "Use default Codex config directory", + "e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync." }, "AdvancedPane": { "40b29e0bf3": "Restart", @@ -6573,7 +6575,9 @@ "bdbd1e668e": "windows", "593720c17f": "location", "b84a5b0c8a": "Choose whether provider accounts are inspected and added on this device or in WSL.", - "d09fb5ca92": "Account Location" + "d09fb5ca92": "Account Location", + "d6347a2658": "Default Codex config directory", + "edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home." } }, "advanced": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index d9da0fd6124..547bddcf9a1 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -4459,7 +4459,9 @@ "75ca9b718e": "Codex informó que la cuenta activa necesita un nuevo inicio de sesión. Vuelva a autenticarlo antes de iniciar nuevas sesiones del Codex.", "b11078a9c2": "wsl", "350b2a1aa7": "Usa tu actual", - "e05d0ff737": "Utilice su inicio de sesión actual de {{value0}} Claude." + "e05d0ff737": "Utilice su inicio de sesión actual de {{value0}} Claude.", + "2eb471df34": "Use default Codex config directory", + "e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync." }, "AdvancedPane": { "40b29e0bf3": "Reanudar", @@ -6536,7 +6538,9 @@ "bdbd1e668e": "ventanas", "593720c17f": "ubicación", "b84a5b0c8a": "Elija si las cuentas de proveedor se inspeccionan y agregan en este dispositivo o en WSL.", - "d09fb5ca92": "Ubicación de la cuenta" + "d09fb5ca92": "Ubicación de la cuenta", + "d6347a2658": "Default Codex config directory", + "edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home." } }, "advanced": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 6f87487774c..d272cd6d011 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4444,7 +4444,9 @@ "75ca9b718e": "Codex は、アクティブなアカウントには新たにサインインする必要があると報告しました。新規 Codex セッションを開始する前に、再認証してください。", "b11078a9c2": "wsl", "350b2a1aa7": "Use your current", - "e05d0ff737": "現在の {{value0}} Claude ログインを使用します。" + "e05d0ff737": "現在の {{value0}} Claude ログインを使用します。", + "2eb471df34": "Use default Codex config directory", + "e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync." }, "AdvancedPane": { "40b29e0bf3": "再起動", @@ -6558,7 +6560,9 @@ "bdbd1e668e": "窓", "593720c17f": "場所", "b84a5b0c8a": "プロバイダー アカウントをこのデバイスまたは WSL で検査および追加するかどうかを選択します。", - "d09fb5ca92": "アカウントの場所" + "d09fb5ca92": "アカウントの場所", + "d6347a2658": "Default Codex config directory", + "edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home." } }, "advanced": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 4f7a5f168bf..7bfc4913e59 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4444,7 +4444,9 @@ "75ca9b718e": "Codex는 활성 계정에 새로 로그인해야 한다고 보고했습니다. 새 Codex 세션을 시작하기 전에 다시 인증하세요.", "b11078a9c2": "wsl", "350b2a1aa7": "현재 사용 중인", - "e05d0ff737": "현재 {{value0}} Claude 로그인을 사용하세요." + "e05d0ff737": "현재 {{value0}} Claude 로그인을 사용하세요.", + "2eb471df34": "Use default Codex config directory", + "e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync." }, "AdvancedPane": { "40b29e0bf3": "다시 시작", @@ -6521,7 +6523,9 @@ "bdbd1e668e": "Windows", "593720c17f": "위치", "b84a5b0c8a": "이 장치 또는 WSL에서 공급자 계정을 검사하고 추가할지 선택합니다.", - "d09fb5ca92": "계정 위치" + "d09fb5ca92": "계정 위치", + "d6347a2658": "Default Codex config directory", + "edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home." } }, "advanced": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 41fc54e3f27..f28c85f7233 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4444,7 +4444,9 @@ "75ca9b718e": "Codex 报告活动账户需要重新登录。在开始新的 Codex 会话之前重新对其进行身份验证。", "b11078a9c2": "wsl", "350b2a1aa7": "用你已有的", - "e05d0ff737": "使用您当前 {{value0}} Claude 登录名。" + "e05d0ff737": "使用您当前 {{value0}} Claude 登录名。", + "2eb471df34": "Use default Codex config directory", + "e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync." }, "AdvancedPane": { "40b29e0bf3": "重新启动", @@ -6521,7 +6523,9 @@ "bdbd1e668e": "视窗", "593720c17f": "位置", "b84a5b0c8a": "选择是否在此设备上或 WSL 中检查和添加提供商账户。", - "d09fb5ca92": "账户位置" + "d09fb5ca92": "账户位置", + "d6347a2658": "Default Codex config directory", + "edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home." } }, "advanced": { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index ca2e5deb364..b0dd2446255 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -317,6 +317,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings { agentDefaultEnv: { ...DEFAULT_TUI_AGENT_ENV }, agentYoloDefaultsMigrated: true, agentStatusHooksEnabled: true, + codexUseDefaultConfigDir: false, tabAutoGenerateTitle: false, confirmClosePinnedTab: true, keepComputerAwakeWhileAgentsRun: false, diff --git a/src/shared/types.ts b/src/shared/types.ts index 1c6e17c92be..50609cd6950 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2692,6 +2692,13 @@ export type GlobalSettings = { /** Why: disabling must persist so startup does not reinstall global agent * hook entries right after the user removes them from Settings or CLI. */ agentStatusHooksEnabled: boolean + /** Why: opt-in escape hatch so Codex launched inside Orca terminals reads the + * user's real ~/.codex config instead of Orca's managed runtime home. When + * true, Orca injects no CODEX_HOME and the background quota poller refreshes + * ~/.codex instead of the managed home — stopping the single-OAuth-session + * refresh-token revocation war with non-Orca Codex. Disables Codex account + * hot-swap and auth sync. Default false preserves the managed-home behavior. */ + codexUseDefaultConfigDir?: boolean /** Why: generated tab titles are semantic but subjective, so they stay opt-in * and manual renames remain the stronger user intent. */ tabAutoGenerateTitle: boolean