From 2fa848395b159592873c62e892dbe1c8c46a11ea Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:32:03 -0700 Subject: [PATCH] refactor(pty): one host-env step for both terminal spawn lanes (#29297) * test(pty): pin where the two spawn lanes build a daemon spawn's host env differently Characterization on main's code before the host-env step merges: which failed env builds sweep per-PTY state (minted ids only, both lanes), the empty-id split, the host-only WSL Pi/OMP guest readiness wait, and that a host create operation still waits on a managed Codex login. * refactor(pty): one host-env step for both terminal spawn lanes The window (pty:spawn) and host (runtime controller) lanes each carried a copy of Codex resume, Codex home selection, the managed-auth wait, backfill recovery and the daemon host-env build. Both now call resolvePtySpawnHostEnv. Lane differences are explicit inputs: two TEMPORARY ones (the host-only WSL Pi/OMP guest readiness wait; the host skipping host env for an empty id) and three that mean the same in both lanes. The resume step's launchEnv was never read and is gone. * test(pty): pin that both lanes hand Codex resume prep the request's CODEX_HOME orca serve (#29209) skips resume prep for a launch whose env names a client-sent CODEX_HOME. * test(pty): type the new lane-pin requests instead of asserting them * test: pin selected Codex home on terminal restart --- .../pty-daemon-spawn-session-identity.test.ts | 82 ++++++ .../pty-spawn-codex-home-unavailable.test.ts | 96 ++++++- src/main/ipc/pty/ipc/spawn-env-codex.ts | 192 +++---------- src/main/ipc/pty/ipc/spawn-types.ts | 27 +- src/main/ipc/pty/runtime/controller-deps.ts | 28 +- src/main/ipc/pty/runtime/spawn-preflight.ts | 190 +++---------- src/main/ipc/pty/spawn-host-env.ts | 256 ++++++++++++++++++ 7 files changed, 498 insertions(+), 373 deletions(-) create mode 100644 src/main/ipc/pty/spawn-host-env.ts diff --git a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts index dc288610945..7cae556681a 100644 --- a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts +++ b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { openCodeClearPtyMock, piBuildPtyEnvMock, piClearPtyMock } from './pty-ipc-mock-registry' +import { awaitExplicitPiOmpGuestReadiness } from '../agent-hooks/wsl-pi-omp-guest-readiness' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { createDaemonActiveProviderFixtures } from './pty-ipc-daemon-provider-fixtures' import { registerPtyHandlers } from './pty/register-handlers' @@ -48,6 +49,9 @@ vi.mock('../codex/codex-pane-account-registry', () => vi.mock('../codex/codex-state-db-backfill-recovery', () => import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock()) ) +vi.mock('../agent-hooks/wsl-pi-omp-guest-readiness', () => ({ + awaitExplicitPiOmpGuestReadiness: vi.fn(async () => true) +})) describe('registerPtyHandlers', () => { const { handlers, mainWindow, recoveredAgentClaim, recoveredAgentSurface } = setupPtyIpcSuite() @@ -322,4 +326,82 @@ describe('registerPtyHandlers', () => { }) }) }) + + // Pins main's host-env step (row 4, C5c-1): where the window (pty:spawn) and host (runtime + // controller) lanes build a daemon spawn's env differently today. + describe('daemon host env: where the lanes differ today', () => { + const { setupDaemonAdapter } = createDaemonActiveProviderFixtures({ handlers, mainWindow }) + + function startLanes() { + const daemonSpawn = setupDaemonAdapter() + let controller: { spawn: (args: Record) => Promise } | undefined + const runtime = { + setPtyController: vi.fn((next) => { + controller = next + }), + createPreAllocatedTerminalHandle: vi.fn(() => null), + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn() + } + handlers.clear() + registerPtyHandlers(mainWindow as never, runtime as never) + return { + daemonSpawn, + spawn: (lane: 'window' | 'host', request: Record) => + lane === 'window' + ? handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, ...request }) + : controller!.spawn({ cols: 80, rows: 24, ...request }) + } + } + + // Only the id this request minted is swept; an operation id or caller id may name a live PTY. + it.each([ + { lane: 'window', name: 'a minted id', request: {}, swept: true }, + { lane: 'window', name: 'a caller id', request: { sessionId: 'caller-1' }, swept: false }, + { lane: 'host', name: 'a minted id', request: {}, swept: true }, + { + lane: 'host', + name: 'an operation id', + request: { worktreeId: 'wt-alpha', agentSessionCreateOperationId: 'a'.repeat(43) }, + swept: false + }, + { + lane: 'host', + name: 'a new caller id', + request: { sessionId: 'caller-1', isNewSession: true }, + swept: false + } + ] as const)('$lane lane, $name: a failed env build sweeps per-PTY state: $swept', async (c) => { + const lanes = startLanes() + piBuildPtyEnvMock.mockImplementationOnce(() => { + throw new Error('env boom') + }) + + await expect(lanes.spawn(c.lane, { env: {}, ...c.request })).rejects.toThrow('env boom') + expect(lanes.daemonSpawn).not.toHaveBeenCalled() + expect(piClearPtyMock).toHaveBeenCalledTimes(c.swept ? 1 : 0) + }) + + // TEMPORARY (row 7): an empty id (the host trims a blank one to it) skips the host's host env; the + // window refuses it. + it('an empty session id: the window refuses it; the host spawns without host env', async () => { + const lanes = startLanes() + await expect(lanes.spawn('window', { env: {}, sessionId: '' })).rejects.toThrow( + /Invalid PTY session id/ + ) + await lanes.spawn('host', { env: {}, sessionId: '' }) + expect(lanes.daemonSpawn.mock.calls.at(-1)![0].env.ORCA_AGENT_HOOK_PORT).toBeUndefined() + expect(piBuildPtyEnvMock).not.toHaveBeenCalled() + }) + + // TEMPORARY: only host starts wait for a WSL guest's Pi/OMP relay. + it('only the host lane runs the WSL Pi/OMP guest readiness wait', async () => { + const lanes = startLanes() + vi.mocked(awaitExplicitPiOmpGuestReadiness).mockClear() + await lanes.spawn('window', { env: {}, command: 'pi' }) + expect(vi.mocked(awaitExplicitPiOmpGuestReadiness)).not.toHaveBeenCalled() + await lanes.spawn('host', { env: {}, command: 'pi' }) + expect(vi.mocked(awaitExplicitPiOmpGuestReadiness)).toHaveBeenCalledOnce() + }) + }) }) diff --git a/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts b/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts index 72c3a2cd368..a76b37305b0 100644 --- a/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts +++ b/src/main/ipc/pty-spawn-codex-home-unavailable.test.ts @@ -5,6 +5,7 @@ import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { createDaemonActiveProviderFixtures } from './pty-ipc-daemon-provider-fixtures' import { ManagedCodexHomeTemporarilyUnavailableError } from '../codex-accounts/host-codex-managed-home-ownership' import { registerPtyHandlers } from './pty/register-handlers' +import type { PrepareCodexSessionResume } from './pty/host-env/types' vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) @@ -63,8 +64,12 @@ const MANAGED_ACCOUNT_SETTINGS = (() => ] }) as never) as () => never +type RuntimeSpawnRequest = { cols: number; rows: number; launchAgent?: 'codex' } & Record< + string, + unknown +> type RuntimeSpawnController = { - spawn(args: { cols: number; rows: number; launchAgent?: 'codex' }): Promise<{ id: string }> + spawn(args: RuntimeSpawnRequest): Promise<{ id: string }> } /** @@ -82,6 +87,7 @@ describe('registerPtyHandlers Codex launch refusal on an unreadable managed home function makeRuntime() { return { setPtyController: vi.fn(), + createPreAllocatedTerminalHandle: vi.fn(() => null), registerPty: vi.fn(), noteTerminalSpawnCommand: vi.fn(), onPtySpawned: vi.fn(), @@ -97,7 +103,7 @@ describe('registerPtyHandlers Codex launch refusal on an unreadable managed home context?: { unavailableManagedHomePath?: string; launchesCodex?: boolean } ) => string | null, runtime?: ReturnType, - prepareCodexSessionResume?: () => Promise + prepareCodexSessionResume?: PrepareCodexSessionResume ): void { handlers.clear() registerPtyHandlers( @@ -357,6 +363,92 @@ describe('registerPtyHandlers Codex launch refusal on an unreadable managed home }) }) + // Both lanes: only a launch whose caller named no session waits on a managed login. A host + // create operation's derived id is not caller-named, so it waits too. + it.each([ + { + lane: 'host', + name: 'a create operation', + request: { worktreeId: 'wt-alpha', agentSessionCreateOperationId: 'a'.repeat(43) }, + waits: true + }, + { + lane: 'host', + name: 'a caller-named session', + request: { sessionId: 'caller-1' }, + waits: false + }, + { + lane: 'window', + name: 'a caller-named session', + request: { sessionId: 'caller-1' }, + waits: false + } + ] as const)('$lane lane, $name: waits on the managed login: $waits', async (c) => { + vi.useFakeTimers() + makeAuthUnreadable() + const daemonSpawn = setupDaemonAdapter() + const runtime = makeRuntime() + register((_target, _env, context) => { + if (context?.unavailableManagedHomePath) { + throw new ManagedCodexHomeTemporarilyUnavailableError() + } + return TEST_CODEX_HOME + }, runtime) + const controller: RuntimeSpawnController = runtime.setPtyController.mock.calls[0]?.[0] + const request: RuntimeSpawnRequest = { cols: 80, rows: 24, launchAgent: 'codex', ...c.request } + + const spawn = Promise.resolve( + c.lane === 'host' ? controller.spawn(request) : handlers.get('pty:spawn')!(null, request) + ).then( + () => 'spawned', + (error: Error) => error.message + ) + await vi.advanceTimersByTimeAsync(2_000) + + await expect(spawn).resolves.toBe(c.waits ? UNAVAILABLE_MESSAGE : 'spawned') + expect(daemonSpawn).toHaveBeenCalledTimes(c.waits ? 0 : 1) + }) + + // #29209: a host runtime skips resume prep when the launch env names a client-sent CODEX_HOME. + it.each(['window', 'host', 'window restart'] as const)( + '%s: resume preserves the request home and selected-account restart', + async (entry) => { + const restart = entry === 'window restart' + readFileSyncMock.mockReturnValue(TEST_CODEX_AUTH_JSON) + const daemonSpawn = setupDaemonAdapter() + const runtime = makeRuntime() + const sourceHome = `${TEST_CODEX_HOME}-source` + const prepareResume = vi.fn(async (args) => ({ + outcome: 'resume', + codexHomePath: args.useSelectedAccount ? TEST_CODEX_HOME : sourceHome + })) + register(() => TEST_CODEX_HOME, runtime, prepareResume) + const controller: RuntimeSpawnController = runtime.setPtyController.mock.calls[0]?.[0] + const request: RuntimeSpawnRequest = { + cols: 80, + rows: 24, + launchAgent: 'codex', + command: `codex resume ${RESUME_SESSION.id}`, + resumeProviderSession: RESUME_SESSION, + ...(restart ? { replacesPtyId: 'old-pty' } : {}), + env: { CODEX_HOME: '/client/codex-home' } + } + + await (entry === 'host' + ? controller.spawn(request) + : handlers.get('pty:spawn')!(null, request)) + + expect(prepareResume.mock.calls[0]?.[0].launchEnv).toMatchObject({ + CODEX_HOME: '/client/codex-home' + }) + expect(prepareResume.mock.calls[0]?.[0].useSelectedAccount).toBe(restart ? true : undefined) + expect(daemonSpawn.mock.calls[0]?.[0].env.CODEX_HOME).toBe( + restart ? TEST_CODEX_HOME : sourceHome + ) + } + ) + // Why: only an Orca-launched Codex may wait for Codex's hook hashes; a plain terminal never does. describe('which spawns launch prep may make wait', () => { it('marks a Codex launch, and not a plain terminal, on the local spawn path', async () => { diff --git a/src/main/ipc/pty/ipc/spawn-env-codex.ts b/src/main/ipc/pty/ipc/spawn-env-codex.ts index 1344bd9982b..25adfbf5353 100644 --- a/src/main/ipc/pty/ipc/spawn-env-codex.ts +++ b/src/main/ipc/pty/ipc/spawn-env-codex.ts @@ -1,23 +1,5 @@ -import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' -import { getAppEnvironment } from '../../../../shared/app-environment' -import { isTuiAgent } from '../../../../shared/tui-agent-config' -import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' -import { isSafePtySessionId } from '../../../daemon/pty-session-id' import { isNativeWindowsLocalPtySpawn } from '../../../runtime/terminal-model-query-authority' -import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env' -import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env' -import { getRuntimeSourceStamp } from '../../../runtime/host-descriptor' -import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery' -import { buildPtyHostEnv } from '../host-env/assembly' -import { - getCompatibleSelectedCodexHomePath, - resolveCodexHomeAfterManagedAuthReadiness, - shouldSkipCodexHomeEnvForWindowsShell, - shouldStripInheritedOrcaCodexHome, - codexHomePathsEqual -} from '../host-env/codex-home' -import { promoteAgentTeamsShimPath } from '../host-env/path' -import { clearProviderPtyState } from '../provider/state-cleanup' +import { resolvePtySpawnHostEnv } from '../spawn-host-env' import type { PtyIpcSpawnState } from './spawn-state' export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promise { @@ -28,147 +10,35 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis cwd: args.cwd, shellOverride: ctx.effectiveShellOverride }) - const codexResumePreparation = ctx.preAdoptedStablePane - ? null - : ctx.deps.prepareCodexResumeHome({ - connectionId: args.connectionId, - launchAgent: args.launchAgent, - providerSession: args.resumeProviderSession, - target: ctx.codexSelectionTarget, - launchEnv: ctx.baseEnv, - ...(args.replacesPtyId ? { useSelectedAccount: true } : {}) - }) - ctx.codexResumeLaunch = codexResumePreparation - ? await ctx.deps.resolveCodexResumeLaunch(args.command, codexResumePreparation) - : ctx.deps.noCodexResumeLaunch(ctx.preAdoptedStablePane ? undefined : args.command) - // Why: these three phases have unrelated costs (session provenance + hook - // repair, account/auth resolution, then the synchronous env build). One - // `host_env` label hid all of them behind the name of the cheapest. - ctx.spawnTiming.mark('codex_resume') - const codexResumeHome = ctx.codexResumeLaunch.codexResumeHome - ctx.launchCommand = ctx.codexResumeLaunch.command - ctx.baseEnv = ctx.deps.stripSequencedStartupResumeArgv(ctx.baseEnv, ctx.codexResumeLaunch) - // Why: declared after the strip so a local-provider spawn cannot capture the - // pre-strip env — only the daemon branch below re-derives this from baseEnv. - ctx.env = ctx.baseEnv - const launchesCodex = args.launchAgent === 'codex' - const selectLaunchCodexHome = async (): Promise => - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { - launchesCodex - })) ?? null - ctx.selectedCodexHomePath = - !ctx.preAdoptedStablePane && !args.connectionId - ? getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - codexResumeHome - ? await ctx.deps.reconcileSharedRuntimeResumeHome(codexResumeHome, async () => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - await selectLaunchCodexHome() - ) - ) - : await selectLaunchCodexHome() - ) - : null - if (!ctx.preAdoptedStablePane && args.launchAgent === 'codex' && args.sessionId === undefined) { - const resolution = resolveCodexHomeAfterManagedAuthReadiness({ - selectedCodexHomePath: ctx.selectedCodexHomePath, - getSettings: () => ctx.deps.getSettings?.(), - requiredCodexHomePath: codexResumeHome?.codexHomePath, - target: ctx.codexSelectionTarget, - resolveCurrent: async () => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { - launchesCodex - })) ?? null - ), - resolveAfterUnavailable: async (unavailableManagedHomePath) => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.baseEnv, { - unavailableManagedHomePath, - launchesCodex - })) ?? null - ) - }) - ctx.selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution - } - if (args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { - await ensureCodexStateDbBackfillRecoveryStarted(ctx.selectedCodexHomePath) - } - ctx.spawnTiming.mark('codex_home') - ctx.codexResumeHomeSelected = Boolean( - codexResumeHome && codexHomePathsEqual(ctx.selectedCodexHomePath, codexResumeHome.codexHomePath) - ) - ctx.skipCodexHomeEnv = - ctx.isDaemonHostSpawn && - shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd) && - !ctx.selectedCodexHomePath - const ptySettings = ctx.isDaemonHostSpawn ? ctx.deps.getSettings?.() : undefined - ctx.stripInheritedOrcaCodexHome = - ctx.isDaemonHostSpawn && - shouldStripInheritedOrcaCodexHome({ - target: ctx.codexSelectionTarget, - selectedCodexHomePath: ctx.selectedCodexHomePath, - skipCodexHomeEnv: ctx.skipCodexHomeEnv, - settings: ptySettings - }) - if (ctx.isDaemonHostSpawn && !ctx.preAdoptedStablePane) { - if (ctx.effectiveSessionId === undefined) { - // Should be unreachable: effectiveSessionId is a string when isDaemonHostSpawn; defense-in-depth. - throw new Error('Invariant violation: daemon spawn without sessionId') - } - const sessionIdForEnv = ctx.effectiveSessionId - // Why: this id reaches filesystem paths; reject traversal/separators so a crafted IPC payload can't escape the expected roots. - if (!isSafePtySessionId(sessionIdForEnv, getAppEnvironment().getPath('userData'))) { - throw new Error('Invalid PTY session id') - } - // Why: clone before mutating so injections don't leak back into args.env (renderer may reuse it). - ctx.env = { ...ctx.baseEnv } - try { - await inheritOmpLaunchEnvironment(ctx.env, { - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), - launchAgent: args.launchAgent, - launchCommand: ctx.launchCommand - }) - buildPtyHostEnv(sessionIdForEnv, ctx.env, { - isPackaged: getAppEnvironment().isPackaged(), - resourcesPath: process.resourcesPath, - userDataPath: getAppEnvironment().getPath('userData'), - selectedCodexHomePath: ctx.selectedCodexHomePath, - skipCodexHomeEnv: ctx.skipCodexHomeEnv, - stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, - launchCommand: ctx.launchCommand, - launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - shellPath: ctx.effectiveShellOverride, - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), - wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, - agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - terminalInlineImages: ptySettings?.terminalInlineImages, - disabledTuiAgents: ptySettings?.disabledTuiAgents, - networkProxySettings: ptySettings, - routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), - deferGitConfigGuardToDaemon: - ctx.provider.supportsGitCredentialGuardHost?.(ctx.effectiveSessionId) === true - }) - stampWslOrchestrationCompatibilityHost( - ctx.env, - ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), - ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null - ) - stampRuntimeSourceEnv( - ctx.env, - getRuntimeSourceStamp(ctx.deps.runtime, getAppEnvironment().getPath('userData')) - ) - promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath) - } catch (err) { - // Why: buildPtyHostEnv has fs side-effects (Pi/OMP install); clear per-PTY state on throw, but only minted ids — caller ids may name existing PTYs. - if (ctx.isMintedSessionId) { - clearProviderPtyState(sessionIdForEnv) - } - throw err - } - } + const hostEnv = await resolvePtySpawnHostEnv({ + launchAgent: args.launchAgent, + command: args.command, + resumeProviderSession: args.resumeProviderSession, + env: ctx.baseEnv, + cwd: ctx.cwd, + shellOverride: ctx.effectiveShellOverride, + expectedWslDistro: ctx.expectedWslDistro, + codexSelectionTarget: ctx.codexSelectionTarget, + preAdopted: Boolean(ctx.preAdoptedStablePane), + isDaemonHostSpawn: ctx.isDaemonHostSpawn, + sessionId: ctx.effectiveSessionId, + callerNamedSession: args.sessionId !== undefined, + mintedSessionId: ctx.isMintedSessionId, + resumeOnSelectedAccount: Boolean(args.replacesPtyId), + awaitsWslPiOmpGuestReadiness: false, + emptySessionIdSkipsHostEnv: false, + requestedAgentTeamsPath: ctx.requestedAgentTeamsPath, + provider: ctx.provider, + deps: ctx.deps, + mark: (phase) => ctx.spawnTiming.mark(phase) + }) + ctx.codexResumeLaunch = hostEnv.codexResumeLaunch + ctx.launchCommand = hostEnv.launchCommand + ctx.baseEnv = hostEnv.baseEnv + ctx.env = hostEnv.env + ctx.selectedCodexHomePath = hostEnv.selectedCodexHomePath + ctx.codexResumeHomeSelected = hostEnv.codexResumeHomeSelected + ctx.skipCodexHomeEnv = hostEnv.skipCodexHomeEnv + ctx.stripInheritedOrcaCodexHome = hostEnv.stripInheritedOrcaCodexHome ctx.spawnTiming.mark('host_env') } diff --git a/src/main/ipc/pty/ipc/spawn-types.ts b/src/main/ipc/pty/ipc/spawn-types.ts index ee8a15fac26..4166cbd5399 100644 --- a/src/main/ipc/pty/ipc/spawn-types.ts +++ b/src/main/ipc/pty/ipc/spawn-types.ts @@ -10,17 +10,15 @@ import type { TerminalStartupCwdMissingDirFallback } from '../../../../shared/te import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' import type { Store } from '../../../persistence' import type { PtySpawnResult } from '../../../providers/pty-provider-contract' -import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection' -import type { CodexSessionResumePreparation } from '../../../codex/codex-session-resume-home' import type { CodexHomePtySpawnedLifecycleArgs, GetSelectedCodexHomePath, PrepareClaudeAuth, PrepareCodexSessionResume } from '../host-env/types' -import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/codex-resume' import type { StablePaneOwner } from '../pane/stable-owner' import type { ResolvedPtyHost } from '../provider/registry' +import type { PtySpawnCodexResumeDeps } from '../spawn-host-env' export type PtySpawnIpcArgs = { cols: number @@ -86,7 +84,7 @@ export type AdoptStablePaneResult = { materialized?: true } -export type PtySpawnIpcDeps = { +export type PtySpawnIpcDeps = PtySpawnCodexResumeDeps & { runtime?: OrcaRuntimeService store?: Store getSettings?: () => GlobalSettings @@ -108,27 +106,6 @@ export type PtySpawnIpcDeps = { missingDirFallback?: TerminalStartupCwdMissingDirFallback ) => string | undefined localStartupCwdDirectoryExists: (path: string) => boolean - prepareCodexResumeHome: (args: { - connectionId?: string | null - launchAgent?: TuiAgent - providerSession?: AgentProviderSessionMetadata - target: CodexAccountSelectionTarget - launchEnv?: NodeJS.ProcessEnv - useSelectedAccount?: boolean - }) => PreparedCodexResumeHome | null - noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch - resolveCodexResumeLaunch: ( - command: string | undefined, - prepared: PreparedCodexResumeHome - ) => Promise - reconcileSharedRuntimeResumeHome: ( - resumeHome: Extract, - resolveCurrent: () => string | null | Promise - ) => Promise - stripSequencedStartupResumeArgv: | undefined>( - env: T, - launch: CodexResumeLaunch - ) => T transitionSpawnHiddenRendererPtyDeliveryState: (id: string, hidden: boolean) => void trustedTerminalHandleEnv: Set sendPtySpawnedToRenderer: (id: string) => void diff --git a/src/main/ipc/pty/runtime/controller-deps.ts b/src/main/ipc/pty/runtime/controller-deps.ts index bc8acc606c7..667900bf474 100644 --- a/src/main/ipc/pty/runtime/controller-deps.ts +++ b/src/main/ipc/pty/runtime/controller-deps.ts @@ -3,49 +3,25 @@ import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' import type { Store } from '../../../persistence' import type { IPtyProvider } from '../../../providers/pty-provider-contract' import type { GlobalSettings } from '../../../../shared/global-settings-types' -import type { TuiAgent } from '../../../../shared/tui-agent' -import type { AgentProviderSessionMetadata } from '../../../../shared/agent-session-resume' import type { TerminalStartupCwdMissingDirFallback } from '../../../../shared/terminal-startup-cwd' -import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection' -import type { CodexSessionResumePreparation } from '../../../codex/codex-session-resume-home' import type { CodexHomePtySpawnedLifecycleArgs, GetSelectedCodexHomePath, PrepareClaudeAuth, PrepareCodexSessionResume } from '../host-env/types' -import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/codex-resume' import type { StablePaneOwner } from '../pane/stable-owner' import type { AdoptStablePaneArgs, AdoptStablePaneResult } from '../ipc/spawn-types' import type { finishPtyShutdown } from '../provider/liveness' import type { ResolvedPtyHost } from '../provider/registry' +import type { PtySpawnCodexResumeDeps } from '../spawn-host-env' -export type PtyRuntimeControllerDeps = { +export type PtyRuntimeControllerDeps = PtySpawnCodexResumeDeps & { runtime?: OrcaRuntimeService store?: Store adoptStablePane: (args: AdoptStablePaneArgs) => Promise getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined - prepareCodexResumeHome: (args: { - connectionId?: string | null - launchAgent?: TuiAgent - providerSession?: AgentProviderSessionMetadata - target: CodexAccountSelectionTarget - launchEnv?: NodeJS.ProcessEnv - }) => PreparedCodexResumeHome | null - resolveCodexResumeLaunch: ( - command: string | undefined, - prepared: PreparedCodexResumeHome - ) => Promise - noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch - reconcileSharedRuntimeResumeHome: ( - resumeHome: Extract, - resolveCurrent: () => string | null | Promise - ) => Promise - stripSequencedStartupResumeArgv: | undefined>( - env: T, - launch: CodexResumeLaunch - ) => T assertWorkspaceAcceptsNewPty: ( worktreeId: string | undefined, sessionId?: string diff --git a/src/main/ipc/pty/runtime/spawn-preflight.ts b/src/main/ipc/pty/runtime/spawn-preflight.ts index 5cc79cebdac..24418388f74 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight.ts @@ -1,41 +1,22 @@ import { withClaudeProfileTerminalEnv } from '../../../claude-accounts/claude-profile-installed-router' -import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment' -import { getAppEnvironment } from '../../../../shared/app-environment' import type { PtySpawnResult } from '../../../providers/pty-provider-contract' import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { isTerminalLeafId } from '../../../../shared/stable-pane-id' import { getProvider } from '../provider/registry' -import { buildPtyHostEnv } from '../host-env/assembly' -import { - getCompatibleSelectedCodexHomePath, - resolveCodexHomeAfterManagedAuthReadiness, - shouldSkipCodexHomeEnvForWindowsShell, - shouldStripInheritedOrcaCodexHome, - codexHomePathsEqual -} from '../host-env/codex-home' -import { promoteAgentTeamsShimPath } from '../host-env/path' import { isClaudeLaunchCommand, recoverFreshSpawnProviderRouting, routesFreshSpawnsToLocalProvider } from '../host-env/fresh-spawn-routing' -import { isTuiAgent } from '../../../../shared/tui-agent-config' import { - isSafePtySessionId, mintPtySessionId, ptySessionIdForAgentCreateOperation } from '../../../daemon/pty-session-id' -import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls' import { resolvePathEnvKey } from '../../../pty/windows-environment-path' -import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env' -import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env' -import { getRuntimeSourceStamp } from '../../../runtime/host-descriptor' -import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery' -import { clearProviderPtyState } from '../provider/state-cleanup' -import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness' import type { RuntimePtySpawnState } from './spawn-state' import { resolvePtySpawnStartupTarget } from '../spawn-startup-target' +import { resolvePtySpawnHostEnv } from '../spawn-host-env' import { getConnectionExecutionHostId } from '../../../../shared/execution-host' export async function prepareRuntimePtySpawn( @@ -90,23 +71,6 @@ export async function prepareRuntimePtySpawn( ctx.daemonShellOverride = ctx.terminalRuntimeOptions.shellOverride ctx.expectedWslDistro = target.expectedWslDistro ctx.codexSelectionTarget = target.codexSelectionTarget - const codexResumePreparation = ctx.preAdoptedStablePane - ? null - : ctx.deps.prepareCodexResumeHome({ - connectionId: args.connectionId, - launchAgent: args.launchAgent, - providerSession: args.resumeProviderSession, - target: ctx.codexSelectionTarget, - launchEnv: args.env - }) - const codexResumeLaunch = codexResumePreparation - ? await ctx.deps.resolveCodexResumeLaunch(args.command, codexResumePreparation) - : ctx.deps.noCodexResumeLaunch(ctx.preAdoptedStablePane ? undefined : args.command) - const codexResumeHome = codexResumeLaunch.codexResumeHome - // Why: the drop still applies here, but this controller's result has no field for - // notifyResumeUnavailable — runtime/relay panes start fresh without the notice. - ctx.launchCommand = codexResumeLaunch.command - ctx.codexResumeArgvDropped = codexResumeLaunch.droppedResumeArgv args.env = withClaudeProfileTerminalEnv(args.env, ctx.codexSelectionTarget) ctx.claudeAuth = ctx.isClaudeLaunch && ctx.deps.prepareClaudeAuth @@ -138,131 +102,39 @@ export async function prepareRuntimePtySpawn( ctx.requestedAgentTeamsPath = ctx.env?.ORCA_AGENT_TEAMS_TEAM_ID ? ctx.env[resolvePathEnvKey(ctx.env, process.platform)] : undefined - ctx.env = ctx.deps.stripSequencedStartupResumeArgv(ctx.env, codexResumeLaunch) if (args.preAllocatedHandle) { ctx.env = { ...ctx.env, ORCA_TERMINAL_HANDLE: args.preAllocatedHandle } } - const launchesCodex = args.launchAgent === 'codex' - const selectLaunchCodexHome = async (): Promise => - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { - launchesCodex - })) ?? null - ctx.selectedCodexHomePath = !ctx.preAdoptedStablePane - ? getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - codexResumeHome - ? await ctx.deps.reconcileSharedRuntimeResumeHome(codexResumeHome, async () => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - await selectLaunchCodexHome() - ) - ) - : await selectLaunchCodexHome() - ) - : null - if ( - !ctx.preAdoptedStablePane && - args.launchAgent === 'codex' && - ctx.callerRequestedSessionId === undefined - ) { - const resolution = resolveCodexHomeAfterManagedAuthReadiness({ - selectedCodexHomePath: ctx.selectedCodexHomePath, - getSettings: () => ctx.deps.getSettings?.(), - requiredCodexHomePath: codexResumeHome?.codexHomePath, - target: ctx.codexSelectionTarget, - resolveCurrent: async () => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { - launchesCodex - })) ?? null - ), - resolveAfterUnavailable: async (unavailableManagedHomePath) => - getCompatibleSelectedCodexHomePath( - ctx.codexSelectionTarget, - (await ctx.deps.getSelectedCodexHomePath?.(ctx.codexSelectionTarget, ctx.env, { - unavailableManagedHomePath, - launchesCodex - })) ?? null - ) - }) - ctx.selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution - } - if (args.launchAgent === 'codex' && ctx.selectedCodexHomePath) { - await ensureCodexStateDbBackfillRecoveryStarted(ctx.selectedCodexHomePath) - } - ctx.codexResumeHomeSelected = Boolean( - codexResumeHome && codexHomePathsEqual(ctx.selectedCodexHomePath, codexResumeHome.codexHomePath) - ) - ctx.skipCodexHomeEnv = - ctx.isDaemonHostSpawn && - shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd) && - !ctx.selectedCodexHomePath - const ptySettings = ctx.isDaemonHostSpawn ? ctx.deps.getSettings?.() : undefined - ctx.stripInheritedOrcaCodexHome = - ctx.isDaemonHostSpawn && - shouldStripInheritedOrcaCodexHome({ - target: ctx.codexSelectionTarget, - selectedCodexHomePath: ctx.selectedCodexHomePath, - skipCodexHomeEnv: ctx.skipCodexHomeEnv, - settings: ptySettings - }) - if (ctx.isDaemonHostSpawn && ctx.sessionId && !ctx.preAdoptedStablePane) { - if (!isSafePtySessionId(ctx.sessionId, getAppEnvironment().getPath('userData'))) { - throw new Error('Invalid PTY session id') - } - try { - ctx.env ??= {} - await inheritOmpLaunchEnvironment(ctx.env, { - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - launchAgent: args.launchAgent, - launchCommand: ctx.launchCommand - }) - await awaitExplicitPiOmpGuestReadiness({ - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - distro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, - codexHomePath: ctx.selectedCodexHomePath, - launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - launchCommand: ctx.launchCommand - }) - ctx.env = buildPtyHostEnv(ctx.sessionId, ctx.env ?? {}, { - isPackaged: getAppEnvironment().isPackaged(), - resourcesPath: process.resourcesPath, - userDataPath: getAppEnvironment().getPath('userData'), - selectedCodexHomePath: ctx.selectedCodexHomePath, - skipCodexHomeEnv: ctx.skipCodexHomeEnv, - stripInheritedOrcaCodexHome: ctx.stripInheritedOrcaCodexHome, - launchCommand: ctx.launchCommand, - launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, - shellPath: ctx.daemonShellOverride, - isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), - wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, - agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - terminalInlineImages: ptySettings?.terminalInlineImages, - disabledTuiAgents: ptySettings?.disabledTuiAgents, - networkProxySettings: ptySettings, - routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), - deferGitConfigGuardToDaemon: - ctx.provider.supportsGitCredentialGuardHost?.(ctx.sessionId) === true - }) - stampWslOrchestrationCompatibilityHost( - ctx.env, - ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(), - ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null - ) - stampRuntimeSourceEnv( - ctx.env, - getRuntimeSourceStamp(ctx.deps.runtime, getAppEnvironment().getPath('userData')) - ) - promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath) - } catch (error) { - // Why: host-env setup can materialize agent hooks/extensions before failing. - if (ctx.requestedSessionId === undefined) { - clearProviderPtyState(ctx.sessionId) - } - throw error - } - } + // Why: a dropped resume still applies here, but this controller's result has no field for + // notifyResumeUnavailable — runtime/relay panes start fresh without the notice. + const hostEnv = await resolvePtySpawnHostEnv({ + launchAgent: args.launchAgent, + command: args.command, + resumeProviderSession: args.resumeProviderSession, + env: ctx.env, + cwd: ctx.cwd, + shellOverride: ctx.daemonShellOverride, + expectedWslDistro: ctx.expectedWslDistro, + codexSelectionTarget: ctx.codexSelectionTarget, + preAdopted: Boolean(ctx.preAdoptedStablePane), + isDaemonHostSpawn: ctx.isDaemonHostSpawn, + sessionId: ctx.sessionId, + callerNamedSession: ctx.callerRequestedSessionId !== undefined, + mintedSessionId: ctx.requestedSessionId === undefined, + resumeOnSelectedAccount: false, + awaitsWslPiOmpGuestReadiness: true, + emptySessionIdSkipsHostEnv: true, + requestedAgentTeamsPath: ctx.requestedAgentTeamsPath, + provider: ctx.provider, + deps: ctx.deps + }) + ctx.launchCommand = hostEnv.launchCommand + ctx.codexResumeArgvDropped = hostEnv.codexResumeLaunch.droppedResumeArgv + ctx.env = hostEnv.env + ctx.selectedCodexHomePath = hostEnv.selectedCodexHomePath + ctx.codexResumeHomeSelected = hostEnv.codexResumeHomeSelected + ctx.skipCodexHomeEnv = hostEnv.skipCodexHomeEnv + ctx.stripInheritedOrcaCodexHome = hostEnv.stripInheritedOrcaCodexHome return null } diff --git a/src/main/ipc/pty/spawn-host-env.ts b/src/main/ipc/pty/spawn-host-env.ts new file mode 100644 index 00000000000..3b329df2e8c --- /dev/null +++ b/src/main/ipc/pty/spawn-host-env.ts @@ -0,0 +1,256 @@ +// The env a new PTY starts with and the Codex home it runs, for both the window and the host spawn lane. + +import type { AgentProviderSessionMetadata } from '../../../shared/agent-session-resume' +import { getAppEnvironment } from '../../../shared/app-environment' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import { stampRuntimeSourceEnv } from '../../../shared/runtime-source-env' +import type { TuiAgent } from '../../../shared/tui-agent' +import { isTuiAgent } from '../../../shared/tui-agent-config' +import { isAgentStatusHooksEnabled } from '../../agent-hooks/managed-agent-hook-controls' +import { awaitExplicitPiOmpGuestReadiness } from '../../agent-hooks/wsl-pi-omp-guest-readiness' +import { ensureCodexStateDbBackfillRecoveryStarted } from '../../codex/codex-state-db-backfill-recovery' +import type { CodexSessionResumePreparation } from '../../codex/codex-session-resume-home' +import type { CodexAccountSelectionTarget } from '../../codex-accounts/runtime-selection' +import { isSafePtySessionId } from '../../daemon/pty-session-id' +import type { IPtyProvider } from '../../providers/pty-provider-contract' +import { stampWslOrchestrationCompatibilityHost } from '../../pty/wsl-orca-env' +import { getRuntimeSourceStamp } from '../../runtime/host-descriptor' +import type { OrcaRuntimeService } from '../../runtime/orca-runtime' +import { buildPtyHostEnv } from './host-env/assembly' +import { + codexHomePathsEqual, + getCompatibleSelectedCodexHomePath, + resolveCodexHomeAfterManagedAuthReadiness, + shouldSkipCodexHomeEnvForWindowsShell, + shouldStripInheritedOrcaCodexHome +} from './host-env/codex-home' +import type { + CodexResumeLaunch, + PrepareCodexResumeHomeArgs, + PreparedCodexResumeHome +} from './host-env/codex-resume' +import { inheritOmpLaunchEnvironment } from './host-env/omp-launch-environment' +import { promoteAgentTeamsShimPath } from './host-env/path' +import type { GetSelectedCodexHomePath } from './host-env/types' +import { clearProviderPtyState } from './provider/state-cleanup' + +/** The Codex resume steps both lanes are handed (bound in register-handlers). */ +export type PtySpawnCodexResumeDeps = { + prepareCodexResumeHome: (args: PrepareCodexResumeHomeArgs) => PreparedCodexResumeHome | null + resolveCodexResumeLaunch: ( + command: string | undefined, + prepared: PreparedCodexResumeHome + ) => Promise + noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch + reconcileSharedRuntimeResumeHome: ( + resumeHome: Extract, + resolveCurrent: () => string | null | Promise + ) => Promise + stripSequencedStartupResumeArgv: | undefined>( + env: T, + launch: CodexResumeLaunch + ) => T +} + +export type PtySpawnHostEnv = { + codexResumeLaunch: CodexResumeLaunch + launchCommand: string | undefined + /** Before Orca's host env; TEMPORARY (row 6 removes it): the window reports this one at commit. */ + baseEnv: Record | undefined + env: Record | undefined + selectedCodexHomePath: string | null + codexResumeHomeSelected: boolean + skipCodexHomeEnv: boolean + stripInheritedOrcaCodexHome: boolean +} + +type PtySpawnHostEnvArgs = { + launchAgent: TuiAgent | undefined + command: string | undefined + resumeProviderSession: AgentProviderSessionMetadata | undefined + /** The lane's spawn env before Orca's host env. */ + env: Record | undefined + cwd: string | undefined + shellOverride: string | undefined + expectedWslDistro: string | null + codexSelectionTarget: CodexAccountSelectionTarget + /** An adopted stable pane launches nothing, so it gets no launch prep. */ + preAdopted: boolean + isDaemonHostSpawn: boolean + sessionId: string | undefined + /** Only a launch whose caller named no session waits on a managed Codex login. */ + callerNamedSession: boolean + /** A failed env build sweeps per-PTY state only for an id this request minted. */ + mintedSessionId: boolean + /** An account-switch restart resumes onto the selected account; only the window has one. */ + resumeOnSelectedAccount: boolean + /** TEMPORARY (its own stacked PR removes it): only host starts wait for a WSL Pi/OMP relay. */ + awaitsWslPiOmpGuestReadiness: boolean + /** TEMPORARY (row 7 removes it): the host builds no host env for an empty id; the window refuses it. */ + emptySessionIdSkipsHostEnv: boolean + requestedAgentTeamsPath: string | undefined + provider: IPtyProvider + deps: PtySpawnCodexResumeDeps & { + runtime?: OrcaRuntimeService + getSettings?: () => GlobalSettings | undefined + getSelectedCodexHomePath?: GetSelectedCodexHomePath + } + mark?: (phase: string) => void +} + +export async function resolvePtySpawnHostEnv(args: PtySpawnHostEnvArgs): Promise { + const { deps, codexSelectionTarget: target } = args + const codexResumePreparation = args.preAdopted + ? null + : deps.prepareCodexResumeHome({ + launchAgent: args.launchAgent, + providerSession: args.resumeProviderSession, + target, + // Why: a host runtime skips resume prep for a launch whose env names a client-sent CODEX_HOME. + launchEnv: args.env, + ...(args.resumeOnSelectedAccount ? { useSelectedAccount: true } : {}) + }) + const codexResumeLaunch = codexResumePreparation + ? await deps.resolveCodexResumeLaunch(args.command, codexResumePreparation) + : deps.noCodexResumeLaunch(args.preAdopted ? undefined : args.command) + // Why: these three phases have unrelated costs (session provenance + hook + // repair, account/auth resolution, then the synchronous env build). One + // `host_env` label hid all of them behind the name of the cheapest. + args.mark?.('codex_resume') + const codexResumeHome = codexResumeLaunch.codexResumeHome + const launchCommand = codexResumeLaunch.command + const baseEnv = deps.stripSequencedStartupResumeArgv(args.env, codexResumeLaunch) + const launchesCodex = args.launchAgent === 'codex' + const selectLaunchCodexHome = async (): Promise => + (await deps.getSelectedCodexHomePath?.(target, baseEnv, { launchesCodex })) ?? null + let selectedCodexHomePath = !args.preAdopted + ? getCompatibleSelectedCodexHomePath( + target, + codexResumeHome + ? await deps.reconcileSharedRuntimeResumeHome(codexResumeHome, async () => + getCompatibleSelectedCodexHomePath(target, await selectLaunchCodexHome()) + ) + : await selectLaunchCodexHome() + ) + : null + if (!args.preAdopted && launchesCodex && !args.callerNamedSession) { + const resolution = resolveCodexHomeAfterManagedAuthReadiness({ + selectedCodexHomePath, + getSettings: () => deps.getSettings?.(), + requiredCodexHomePath: codexResumeHome?.codexHomePath, + target, + resolveCurrent: async () => + getCompatibleSelectedCodexHomePath( + target, + (await deps.getSelectedCodexHomePath?.(target, baseEnv, { launchesCodex })) ?? null + ), + resolveAfterUnavailable: async (unavailableManagedHomePath) => + getCompatibleSelectedCodexHomePath( + target, + (await deps.getSelectedCodexHomePath?.(target, baseEnv, { + unavailableManagedHomePath, + launchesCodex + })) ?? null + ) + }) + selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution + } + if (launchesCodex && selectedCodexHomePath) { + await ensureCodexStateDbBackfillRecoveryStarted(selectedCodexHomePath) + } + args.mark?.('codex_home') + const isWsl = shouldSkipCodexHomeEnvForWindowsShell(args.shellOverride, args.cwd) + const skipCodexHomeEnv = args.isDaemonHostSpawn && isWsl && !selectedCodexHomePath + const ptySettings = args.isDaemonHostSpawn ? deps.getSettings?.() : undefined + const hostEnv: PtySpawnHostEnv = { + codexResumeLaunch, + launchCommand, + baseEnv, + env: baseEnv, + selectedCodexHomePath, + codexResumeHomeSelected: Boolean( + codexResumeHome && codexHomePathsEqual(selectedCodexHomePath, codexResumeHome.codexHomePath) + ), + skipCodexHomeEnv, + stripInheritedOrcaCodexHome: + args.isDaemonHostSpawn && + shouldStripInheritedOrcaCodexHome({ + target, + selectedCodexHomePath, + skipCodexHomeEnv, + settings: ptySettings + }) + } + const sessionId = args.sessionId + if ( + !args.isDaemonHostSpawn || + args.preAdopted || + (args.emptySessionIdSkipsHostEnv && !sessionId) + ) { + return hostEnv + } + if (sessionId === undefined) { + // Should be unreachable: a daemon spawn always has an id; defense-in-depth. + throw new Error('Invariant violation: daemon spawn without sessionId') + } + // Why: this id reaches filesystem paths; reject traversal/separators so a crafted payload can't escape the expected roots. + if (!isSafePtySessionId(sessionId, getAppEnvironment().getPath('userData'))) { + throw new Error('Invalid PTY session id') + } + const wslDistro = target.runtime === 'wsl' ? args.expectedWslDistro : null + // Why: clone before mutating so injections don't leak back into the caller's env (renderer may reuse it). + const env = { ...baseEnv } + try { + await inheritOmpLaunchEnvironment(env, { + isWsl, + launchAgent: args.launchAgent, + launchCommand + }) + if (args.awaitsWslPiOmpGuestReadiness) { + await awaitExplicitPiOmpGuestReadiness({ + isWsl, + distro: wslDistro, + codexHomePath: selectedCodexHomePath, + launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, + launchCommand + }) + } + buildPtyHostEnv(sessionId, env, { + isPackaged: getAppEnvironment().isPackaged(), + resourcesPath: process.resourcesPath, + userDataPath: getAppEnvironment().getPath('userData'), + selectedCodexHomePath, + skipCodexHomeEnv, + stripInheritedOrcaCodexHome: hostEnv.stripInheritedOrcaCodexHome, + launchCommand, + launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined, + shellPath: args.shellOverride, + isWsl, + wslDistro, + agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), + terminalInlineImages: ptySettings?.terminalInlineImages, + disabledTuiAgents: ptySettings?.disabledTuiAgents, + networkProxySettings: ptySettings, + routeBrowserOpensToClient: deps.runtime?.shouldRelayTerminalBrowserOpens?.(), + deferGitConfigGuardToDaemon: + args.provider.supportsGitCredentialGuardHost?.(sessionId) === true + }) + stampWslOrchestrationCompatibilityHost( + env, + deps.runtime?.getOrchestrationCompatibilityHostId?.(), + wslDistro + ) + stampRuntimeSourceEnv( + env, + getRuntimeSourceStamp(deps.runtime, getAppEnvironment().getPath('userData')) + ) + promoteAgentTeamsShimPath(env, args.requestedAgentTeamsPath) + } catch (err) { + // Why: host-env setup has fs side-effects (hooks, Pi/OMP install); only a minted id's state is ours to clear. + if (args.mintedSessionId) { + clearProviderPtyState(sessionId) + } + throw err + } + return { ...hostEnv, env } +}