mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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
This commit is contained in:
@@ -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<string, unknown>) => Promise<unknown> } | 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<string, unknown>) =>
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<typeof makeRuntime>,
|
||||
prepareCodexSessionResume?: () => Promise<never>
|
||||
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<PrepareCodexSessionResume>(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 () => {
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -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<string | null> =>
|
||||
(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')
|
||||
}
|
||||
|
||||
@@ -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<CodexResumeLaunch>
|
||||
reconcileSharedRuntimeResumeHome: (
|
||||
resumeHome: Extract<CodexSessionResumePreparation, { outcome: 'resume' }>,
|
||||
resolveCurrent: () => string | null | Promise<string | null>
|
||||
) => Promise<string>
|
||||
stripSequencedStartupResumeArgv: <T extends Record<string, string> | undefined>(
|
||||
env: T,
|
||||
launch: CodexResumeLaunch
|
||||
) => T
|
||||
transitionSpawnHiddenRendererPtyDeliveryState: (id: string, hidden: boolean) => void
|
||||
trustedTerminalHandleEnv: Set<string>
|
||||
sendPtySpawnedToRenderer: (id: string) => void
|
||||
|
||||
@@ -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<AdoptStablePaneResult | null>
|
||||
getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
prepareCodexResumeHome: (args: {
|
||||
connectionId?: string | null
|
||||
launchAgent?: TuiAgent
|
||||
providerSession?: AgentProviderSessionMetadata
|
||||
target: CodexAccountSelectionTarget
|
||||
launchEnv?: NodeJS.ProcessEnv
|
||||
}) => PreparedCodexResumeHome | null
|
||||
resolveCodexResumeLaunch: (
|
||||
command: string | undefined,
|
||||
prepared: PreparedCodexResumeHome
|
||||
) => Promise<CodexResumeLaunch>
|
||||
noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch
|
||||
reconcileSharedRuntimeResumeHome: (
|
||||
resumeHome: Extract<CodexSessionResumePreparation, { outcome: 'resume' }>,
|
||||
resolveCurrent: () => string | null | Promise<string | null>
|
||||
) => Promise<string>
|
||||
stripSequencedStartupResumeArgv: <T extends Record<string, string> | undefined>(
|
||||
env: T,
|
||||
launch: CodexResumeLaunch
|
||||
) => T
|
||||
assertWorkspaceAcceptsNewPty: (
|
||||
worktreeId: string | undefined,
|
||||
sessionId?: string
|
||||
|
||||
@@ -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<string | null> =>
|
||||
(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
|
||||
}
|
||||
|
||||
@@ -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<CodexResumeLaunch>
|
||||
noCodexResumeLaunch: (command: string | undefined) => CodexResumeLaunch
|
||||
reconcileSharedRuntimeResumeHome: (
|
||||
resumeHome: Extract<CodexSessionResumePreparation, { outcome: 'resume' }>,
|
||||
resolveCurrent: () => string | null | Promise<string | null>
|
||||
) => Promise<string>
|
||||
stripSequencedStartupResumeArgv: <T extends Record<string, string> | 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<string, string> | undefined
|
||||
env: Record<string, string> | 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<string, string> | 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<PtySpawnHostEnv> {
|
||||
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<string | null> =>
|
||||
(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 }
|
||||
}
|
||||
Reference in New Issue
Block a user