diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 501229444dc..3f3b10970f8 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -991,6 +991,49 @@ describe('CodexRuntimeHomeService', () => { expect(existsSync(getRuntimeCodexHomePath())).toBe(true) }) + it('routes host system default to the real home (null) when the flag is ON', async () => { + const store = createStore(createSettings({ codexSystemDefaultRealHomeEnabled: true })) + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const service = new CodexRuntimeHomeService(store as never) + + expect(service.isHostSystemDefaultRealHome()).toBe(true) + expect(service.prepareForCodexLaunch()).toBeNull() + expect(service.prepareForRateLimitFetch()).toBeNull() + }) + + it('keeps the managed home for a host MANAGED account even when the flag is ON', async () => { + const managedHomePath = createManagedAuth( + testState.userDataDir, + 'account-1', + '{"account":"managed"}\n' + ) + const store = createStore( + createSettings({ + codexSystemDefaultRealHomeEnabled: true, + codexManagedAccounts: [ + { + id: 'account-1', + email: 'user@example.com', + managedHomePath, + providerAccountId: null, + workspaceLabel: null, + workspaceAccountId: null, + 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) + + expect(service.isHostSystemDefaultRealHome()).toBe(false) + expect(service.prepareForCodexLaunch()).toBe(getRuntimeCodexHomePath()) + }) + 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 29e181bd775..52853883379 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -60,6 +60,7 @@ import { type CodexAccountSelectionTarget } from './runtime-selection' import { getDefaultWslDistro, getWslHome } from '../wsl' +import { isCodexSystemDefaultRealHomeEnabled } from '../codex/codex-real-home-flag' type CodexAuthIdentity = { email: string | null @@ -147,6 +148,13 @@ export class CodexRuntimeHomeService { this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath) return runtimeHomePath } + if (this.isHostSystemDefaultRealHome()) { + // Why (flag ON, system default): run Codex on the user's own ~/.codex. + // Returning null tells the PTY/env layer to inject no managed CODEX_HOME; + // sessions, auth, and config all live in the native home. No system-> + // managed session bridge runs, so the real home stays the single source. + return null + } this.syncForCurrentSelection() syncSystemCodexResourcesIntoManagedHome() syncSystemConfigIntoManagedCodexHome() @@ -192,6 +200,17 @@ export class CodexRuntimeHomeService { return this.getRuntimeHomePath() } + // Why: real-home routing applies only to the host system-default selection + // (no managed account chosen for host) with the staged flag ON. Managed host + // accounts keep the isolated runtime home for hot-swap and token persistence. + isHostSystemDefaultRealHome(): boolean { + const settings = this.store.getSettings() + return ( + isCodexSystemDefaultRealHomeEnabled(settings) && + normalizeCodexRuntimeSelection(settings).host === null + ) + } + syncActiveWslSelectionsBeforeRestart(): void { if (process.platform !== 'win32') { return @@ -255,6 +274,13 @@ export class CodexRuntimeHomeService { const syncedRuntimeHomePath = this.getPreparedWslRateLimitHomePath(wslTarget) return syncedRuntimeHomePath ?? this.getWslSystemCodexHomePath(wslTarget) } + if (this.isHostSystemDefaultRealHome()) { + // Why (flag ON, system default): read usage/auth from the user's own + // ~/.codex. Returning null makes the fetcher fall back to ~/.codex and + // its auth-presence gate check the real auth.json, so the background + // poller never spawns Codex against the managed home (the #5370 auth war). + return null + } this.syncForCurrentSelection() syncSystemCodexResourcesIntoManagedHome() syncSystemConfigIntoManagedCodexHome() diff --git a/src/main/codex/codex-real-home-flag.test.ts b/src/main/codex/codex-real-home-flag.test.ts new file mode 100644 index 00000000000..d5e9ab717a6 --- /dev/null +++ b/src/main/codex/codex-real-home-flag.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { isCodexSystemDefaultRealHomeEnabled } from './codex-real-home-flag' + +const ENV_FLAG = 'ORCA_CODEX_SYSTEM_DEFAULT_REAL_HOME' + +afterEach(() => { + delete process.env[ENV_FLAG] +}) + +describe('isCodexSystemDefaultRealHomeEnabled', () => { + it('is OFF by default (undefined settings)', () => { + expect(isCodexSystemDefaultRealHomeEnabled(undefined)).toBe(false) + expect(isCodexSystemDefaultRealHomeEnabled(null)).toBe(false) + expect(isCodexSystemDefaultRealHomeEnabled({})).toBe(false) + }) + + it('honors the settings flag when set to true', () => { + expect(isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: true })).toBe( + true + ) + expect(isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: false })).toBe( + false + ) + }) + + it('lets the env override force ON regardless of settings', () => { + for (const raw of ['1', 'true', 'on', 'TRUE', ' On ']) { + process.env[ENV_FLAG] = raw + expect( + isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: false }) + ).toBe(true) + } + }) + + it('lets the env override force OFF regardless of settings', () => { + for (const raw of ['0', 'false', 'off']) { + process.env[ENV_FLAG] = raw + expect(isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: true })).toBe( + false + ) + } + }) + + it('ignores an unrecognized env value and falls back to settings', () => { + process.env[ENV_FLAG] = 'maybe' + expect(isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: true })).toBe( + true + ) + expect(isCodexSystemDefaultRealHomeEnabled({ codexSystemDefaultRealHomeEnabled: false })).toBe( + false + ) + }) +}) diff --git a/src/main/codex/codex-real-home-flag.ts b/src/main/codex/codex-real-home-flag.ts new file mode 100644 index 00000000000..c41f1b22dfa --- /dev/null +++ b/src/main/codex/codex-real-home-flag.ts @@ -0,0 +1,40 @@ +import type { GlobalSettings } from '../../shared/types' + +/** + * Staged internal flag: route the SYSTEM-DEFAULT Codex account at the user's + * real ~/.codex instead of Orca's managed runtime home. + * + * Why a flag: this moves where user Codex state lives (auth, config, sessions, + * hooks). It ships dark, default OFF, with NO settings UI, so flag-OFF stays + * byte-identical to today's managed-home behavior. Managed (multi-account) + * selections are unaffected either way. + * + * The env override exists only so isolated dev/CDP verification can exercise + * the ON path without a settings write; it never appears in the UI. + */ +const CODEX_REAL_HOME_ENV_FLAG = 'ORCA_CODEX_SYSTEM_DEFAULT_REAL_HOME' + +export function isCodexSystemDefaultRealHomeEnabled( + settings: Pick | null | undefined +): boolean { + const envOverride = readCodexRealHomeEnvOverride() + if (envOverride !== null) { + return envOverride + } + return settings?.codexSystemDefaultRealHomeEnabled === true +} + +function readCodexRealHomeEnvOverride(): boolean | null { + const raw = process.env[CODEX_REAL_HOME_ENV_FLAG] + if (raw === undefined) { + return null + } + const normalized = raw.trim().toLowerCase() + if (normalized === '1' || normalized === 'true' || normalized === 'on') { + return true + } + if (normalized === '0' || normalized === 'false' || normalized === 'off') { + return false + } + return null +} diff --git a/src/main/index.ts b/src/main/index.ts index 8bda7328eda..525abb8c247 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -734,6 +734,13 @@ function startTerminalRuntimeStartupServices(): Promise { function prepareCodexRuntimeHomeForLaunch(target?: CodexAccountSelectionTarget): string | null { const runtimeHomePath = codexRuntimeHome!.prepareForCodexLaunch(target) + if (runtimeHomePath === null && codexRuntimeHome!.isHostSystemDefaultRealHome()) { + // Why (flag ON, system default): Codex runs on the user's real ~/.codex, so + // the managed-home hook install below would target a home Codex never reads. + // The real-home hook installer (trust granted via the app-server client) + // owns hook install for this lane and lands with the trust plumbing. + return null + } const hookTarget = target?.runtime === 'wsl' ? { diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 6adb7e5cf69..8f69a265023 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -1364,6 +1364,40 @@ describe('registerPtyHandlers', () => { expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME) }) + it('leaves an inherited CODEX_HOME untouched for system default when the flag is OFF', async () => { + // Why: flag OFF must stay byte-identical to today. With no managed home + // selected (resolver null) and the real-home flag off, no CODEX_HOME + // injection or strip happens; an inherited value survives as before. + const env = await spawnAndGetEnv( + undefined, + { CODEX_HOME: '/tmp/system-codex-home' }, + () => null + ) + expect(env.CODEX_HOME).toBe('/tmp/system-codex-home') + }) + + it('strips a nested-Orca override for system default when the real-home flag is ON', async () => { + const env = await spawnAndGetEnv( + { CODEX_HOME: '/managed/home', ORCA_CODEX_HOME: '/managed/home' }, + undefined, + () => null, + () => ({ codexSystemDefaultRealHomeEnabled: true }) as never + ) + expect(env.CODEX_HOME).toBeUndefined() + expect(env.ORCA_CODEX_HOME).toBeUndefined() + }) + + it('preserves a user-owned CODEX_HOME for system default when the real-home flag is ON', async () => { + const env = await spawnAndGetEnv( + { CODEX_HOME: '/home/me/.config/codex' }, + { ORCA_CODEX_HOME: undefined }, + () => null, + () => ({ codexSystemDefaultRealHomeEnabled: true }) as never + ) + expect(env.CODEX_HOME).toBe('/home/me/.config/codex') + expect(env.ORCA_CODEX_HOME).toBeUndefined() + }) + it('injects explicit proxy settings into local PTY env', async () => { const env = await spawnAndGetEnv(undefined, undefined, undefined, () => ({ httpProxyUrl: 'http://proxy.example:8080', diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index ffaa3a3990d..a2cff121876 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -140,6 +140,7 @@ import { setTerminalViewAttributes } from '../runtime/terminal-view-attribute-st import { validateTerminalViewAttributes } from '../../shared/terminal-view-attributes' import type { PtyModelRestoreReason } from '../../shared/pty-model-restore-marker' import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' +import { isCodexSystemDefaultRealHomeEnabled } from '../codex/codex-real-home-flag' import { isHostCodexHomeForWsl, isWslCodexHomeForHost } from '../pty/codex-home-wsl-env' import { buildConfiguredProxyEnv, type NetworkProxySettings } from '../../shared/network-proxy' import { resolveSetupAgentSequenceLaunchCommand } from '../../shared/setup-agent-sequencing' @@ -546,6 +547,10 @@ export type BuildPtyHostEnvOptions = { userDataPath: string selectedCodexHomePath: string | null skipCodexHomeEnv?: boolean + /** System-default real-home routing (flag ON): inject no managed CODEX_HOME, + * and strip only an inherited Orca-owned override so nested Orca panes do not + * leak the parent's managed home. A user-set CODEX_HOME is preserved. */ + stripInheritedOrcaCodexHome?: boolean githubAttributionEnabled: boolean /** The launch command the renderer chose for this PTY (e.g. 'pi', 'omp', * 'claude'). Used to resolve the per-agent managed extension target for @@ -617,7 +622,39 @@ function shouldSkipCodexHomeEnvForWindowsShell( return isWslShellName(shellPath) || (typeof cwd === 'string' && parseWslPath(cwd) !== null) } +// Why: with the real-home flag ON, a host system-default launch resolves to a +// null managed home. Signal the env builder to strip a nested-Orca-inherited +// override instead of injecting one, so Codex runs on the user's own ~/.codex. +function shouldStripInheritedOrcaCodexHome(args: { + target: CodexAccountSelectionTarget + selectedCodexHomePath: string | null + skipCodexHomeEnv: boolean + settings: GlobalSettings | undefined +}): boolean { + return ( + args.target.runtime === 'host' && + args.selectedCodexHomePath === null && + !args.skipCodexHomeEnv && + isCodexSystemDefaultRealHomeEnabled(args.settings) + ) +} + const CODEX_HOME_ENV_KEYS = ['CODEX_HOME', 'ORCA_CODEX_HOME'] as const + +// Why: system-default real-home routing runs Codex on the user's own ~/.codex. +// Nested Orca panes inherit the parent's Orca-owned override; strip only that +// (CODEX_HOME matching Orca's private ORCA_CODEX_HOME marker), and always drop +// the marker so a shell-ready wrapper cannot restore the managed home. A +// user-set CODEX_HOME with no Orca marker is preserved untouched (see #8606). +function stripInheritedOrcaCodexHomeOverride(baseEnv: Record): void { + const inheritedOrcaOverride = baseEnv.ORCA_CODEX_HOME ?? process.env.ORCA_CODEX_HOME + const inheritedCodexHome = baseEnv.CODEX_HOME ?? process.env.CODEX_HOME + if (inheritedOrcaOverride && inheritedCodexHome === inheritedOrcaOverride) { + delete baseEnv.CODEX_HOME + } + delete baseEnv.ORCA_CODEX_HOME +} + type GetSelectedCodexHomePath = (target?: CodexAccountSelectionTarget) => string | null type PrepareClaudeAuth = ( target?: ClaudeAccountSelectionTarget @@ -990,6 +1027,8 @@ export function buildPtyHostEnv( // Why: user startup files may re-export CODEX_HOME; shell-ready wrappers // restore this runtime home before Codex can be launched from the prompt. baseEnv.ORCA_CODEX_HOME = opts.selectedCodexHomePath + } else if (opts.stripInheritedOrcaCodexHome) { + stripInheritedOrcaCodexHomeOverride(baseEnv) } // Why: WSL shells need the managed userData root for shell-ready wrappers; dev-mode terminals need the same export so `orca` targets the live dev instance. @@ -1556,11 +1595,18 @@ export function registerPtyHandlers( codexSelectionTarget, getSelectedCodexHomePath?.(codexSelectionTarget) ?? null ) + const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath const env = buildPtyHostEnv(id, baseEnv, { isPackaged: app.isPackaged, userDataPath: app.getPath('userData'), selectedCodexHomePath, - skipCodexHomeEnv: ctx?.isWsl === true && !selectedCodexHomePath, + skipCodexHomeEnv, + stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({ + target: codexSelectionTarget, + selectedCodexHomePath, + skipCodexHomeEnv, + settings: getSettings?.() + }), githubAttributionEnabled: getSettings?.()?.enableGitHubAttribution ?? false, launchCommand: ctx?.command, launchAgent: ctx?.launchAgent, @@ -3045,6 +3091,12 @@ export function registerPtyHandlers( userDataPath: app.getPath('userData'), selectedCodexHomePath, skipCodexHomeEnv, + stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({ + target: codexSelectionTarget, + selectedCodexHomePath, + skipCodexHomeEnv, + settings: getSettings?.() + }), githubAttributionEnabled: getSettings?.()?.enableGitHubAttribution ?? false, launchCommand: args.command, launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, @@ -3927,6 +3979,12 @@ export function registerPtyHandlers( userDataPath: app.getPath('userData'), selectedCodexHomePath, skipCodexHomeEnv, + stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({ + target: codexSelectionTarget, + selectedCodexHomePath, + skipCodexHomeEnv, + settings: getSettings?.() + }), githubAttributionEnabled: getSettings?.()?.enableGitHubAttribution ?? false, launchCommand: args.command, launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, diff --git a/src/main/text-generation/commit-message-agent-environment.ts b/src/main/text-generation/commit-message-agent-environment.ts index 30516945fd1..587007c9aed 100644 --- a/src/main/text-generation/commit-message-agent-environment.ts +++ b/src/main/text-generation/commit-message-agent-environment.ts @@ -25,6 +25,20 @@ function cloneProcessEnv(): Record { return env } +// Why: with system-default real-home routing, the headless Codex commit run +// must use the user's own ~/.codex. If Orca itself was launched from a nested +// Orca terminal it can inherit an Orca-owned CODEX_HOME override; strip only +// that (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a +// user-set CODEX_HOME. +function cloneProcessEnvWithoutOrcaCodexHomeOverride(): Record { + const env = cloneProcessEnv() + if (env.ORCA_CODEX_HOME && env.CODEX_HOME === env.ORCA_CODEX_HOME) { + delete env.CODEX_HOME + } + delete env.ORCA_CODEX_HOME + return env +} + function readInheritedOrShellEnvVar(name: string, sourceName?: string): string | undefined { return ( (sourceName ? process.env[sourceName] : undefined) ?? @@ -100,7 +114,9 @@ export async function prepareLocalCommitMessageAgentEnv( } return { ok: true, - env: codexHomePath ? { ...cloneProcessEnv(), CODEX_HOME: codexHomePath } : undefined + env: codexHomePath + ? { ...cloneProcessEnv(), CODEX_HOME: codexHomePath } + : cloneProcessEnvWithoutOrcaCodexHomeOverride() } } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 950cc999477..dd9339bb6e8 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -338,6 +338,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings { minimaxGroupId: '', minimaxUsageModels: 'general', geminiCliOAuthEnabled: false, + codexSystemDefaultRealHomeEnabled: false, agentCmdOverrides: {}, agentDefaultArgs: { ...DEFAULT_TUI_AGENT_ARGS }, agentDefaultEnv: { ...DEFAULT_TUI_AGENT_ENV }, diff --git a/src/shared/types.ts b/src/shared/types.ts index 6c1ebaf8cc8..6c5d15ab8ac 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2841,6 +2841,11 @@ export type GlobalSettings = { /** Whether to extract OAuth credentials from the local Gemini CLI installation * for rate-limit fetching. Disabled by default for explicit opt-in. */ geminiCliOAuthEnabled: boolean + /** Staged internal flag (default OFF, no settings UI): route the system-default + * Codex account at the user's real ~/.codex instead of Orca's managed runtime + * home. OFF is byte-identical to today; managed accounts are unaffected. + * See src/main/codex/codex-real-home-flag.ts. */ + codexSystemDefaultRealHomeEnabled?: boolean /** Per-agent CLI command overrides. A missing key means use the catalog default binary name. */ agentCmdOverrides: Partial> /** Why: Orca bridges Codex session history from the user's real Codex home into