mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
fix(wsl): a passive restore's env prep never starts a stopped WSL distro (E9) (#35858)
* fix(wsl): a passive restore's env prep never starts a stopped WSL distro (E9) resolvePtySpawnHostEnv runs before #35813's hold. Every spawn but a passive restore now runs its prep as a user act on its distro, so explicit launches are unchanged. prepareForCodexLaunchAsync checks blockedWslDistroForExec once for WSL targets and, when blocked, skips the guest-home probe, the legacy auth drain, the config and instructions sync and the session bridge, returning the home without probing. When not blocked, the home is warmed with getWslHomeAsync so the sync reads hit the cache instead of a 5 s wsl.exe. The state-DB recovery and the explicit plugin readiness wait are gated the same way. * fix(wsl): only a launch someone asked for acts on its distro; gate the relay start A passive restore or a non-WSL launch never runs as a user act, not even on the default distro (a null distro resolved to the default before). The daemon-off local provider applies the same rule, receiving wslPassiveRestore from the spawn options. Starting the hook relay (ensureForDistro from every WSL spawn's env build) now checks blockedForExec before the state check, so a passive restore on a stopped distro starts no relay.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
blocked: vi.fn(async (): Promise<{ distro: string; state: string } | null> => null)
|
||||
}))
|
||||
vi.mock('../wsl/wsl-background-exec-gate', () => ({ blockedWslDistroForExec: mocks.blocked }))
|
||||
|
||||
import { awaitExplicitAgentPluginGuestReadiness } from './wsl-agent-plugin-readiness'
|
||||
|
||||
function manager() {
|
||||
return {
|
||||
ensureForDistro: vi.fn(async () => {}),
|
||||
getGuestEndpointFilePath: vi.fn(() => '/tmp/endpoint'),
|
||||
getGuestAgentPath: vi.fn(() => '/tmp/pi')
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.blocked.mockReset().mockResolvedValue(null)
|
||||
})
|
||||
|
||||
// Ensuring the hook relay launches wsl.exe, before a passive restore's hold (#35813, E9).
|
||||
describe('explicit agent plugin readiness on WSL', () => {
|
||||
it('does not ensure the relay of a stopped distro', async () => {
|
||||
mocks.blocked.mockResolvedValue({ distro: 'Ubuntu', state: 'stopped' })
|
||||
const relay = manager()
|
||||
await expect(
|
||||
awaitExplicitAgentPluginGuestReadiness({
|
||||
isWsl: true,
|
||||
distro: 'Ubuntu',
|
||||
launchAgent: 'pi',
|
||||
manager: relay
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
expect(mocks.blocked).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(relay.ensureForDistro).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ensures it for a running distro, or inside a user act', async () => {
|
||||
const relay = manager()
|
||||
await expect(
|
||||
awaitExplicitAgentPluginGuestReadiness({
|
||||
isWsl: true,
|
||||
distro: 'Ubuntu',
|
||||
launchAgent: 'pi',
|
||||
manager: relay
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
expect(relay.ensureForDistro).toHaveBeenCalledWith('Ubuntu', undefined, 'pi')
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../shared/pi-agent-kind'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { wslHookRelayManager, type WslHookRelayManager } from './wsl-hook-relay-manager'
|
||||
import { blockedWslDistroForExec } from '../wsl/wsl-background-exec-gate'
|
||||
|
||||
export async function awaitExplicitAgentPluginGuestReadiness(args: {
|
||||
isWsl: boolean
|
||||
@@ -41,6 +42,10 @@ export async function awaitExplicitAgentPluginGuestReadiness(args: {
|
||||
return true
|
||||
}
|
||||
const distro = args.distro ?? null
|
||||
// Ensuring the relay launches wsl.exe; a passive restore must not start a stopped distro (E9).
|
||||
if (await blockedWslDistroForExec(distro)) {
|
||||
return false
|
||||
}
|
||||
const manager = args.manager ?? wslHookRelayManager
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
readDaemonBridgeSessions,
|
||||
type DaemonBridgeSessionVerdict
|
||||
} from './wsl-bridge-daemon-sessions'
|
||||
import { blockedWslDistroForExec } from '../wsl/wsl-background-exec-gate'
|
||||
|
||||
// Concurrent wsl.exe spawns can trigger transient E_UNEXPECTED failures.
|
||||
export const WSL_RELAY_TRANSIENT_RETRY_DELAY_MS = 2_000
|
||||
@@ -55,6 +56,8 @@ export type WslHookRelayManagerDeps = {
|
||||
/** Recovery must not boot a distro the user shut down. */
|
||||
isDistroRunning: typeof isWslDistroRunning
|
||||
deferUntilRunning: typeof deferUntilWslDistroRunning
|
||||
/** Starting a relay runs wsl.exe; only a running distro, or a user act's, may get one (E9). */
|
||||
blockedForExec: typeof blockedWslDistroForExec
|
||||
spawnRelay: typeof spawnWslRelayProcess
|
||||
runInstall: typeof runWslInstallProcess
|
||||
waitForSentinel: typeof waitForWslRelaySentinel
|
||||
@@ -106,6 +109,7 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = {
|
||||
listDistros: () => listWslDistrosAsync(),
|
||||
isDistroRunning: isWslDistroRunning,
|
||||
deferUntilRunning: deferUntilWslDistroRunning,
|
||||
blockedForExec: blockedWslDistroForExec,
|
||||
spawnRelay: spawnWslRelayProcess,
|
||||
runInstall: runWslInstallProcess,
|
||||
waitForSentinel: waitForWslRelaySentinel,
|
||||
|
||||
@@ -237,6 +237,7 @@ describe('WslHookRelayManager', () => {
|
||||
listDistros: async () => ['Ubuntu'],
|
||||
isDistroRunning: vi.fn(async () => true),
|
||||
deferUntilRunning: vi.fn(),
|
||||
blockedForExec: vi.fn(async () => null),
|
||||
spawnRelay: vi.fn(() => fakeChild()),
|
||||
runInstall: vi.fn(async () => ({ code: 0, stderr: '' })),
|
||||
waitForSentinel: vi.fn(async () => guestTransport()),
|
||||
@@ -261,6 +262,16 @@ describe('WslHookRelayManager', () => {
|
||||
return { manager: new WslHookRelayManager(deps), deps }
|
||||
}
|
||||
|
||||
it('starts no relay for a stopped distro outside a user act (a passive restore, E9)', async () => {
|
||||
const { manager, deps } = createManager({
|
||||
blockedForExec: vi.fn(async () => ({ distro: 'Ubuntu', state: 'stopped' as const }))
|
||||
})
|
||||
await manager.ensureForDistro('Ubuntu', codexHome)
|
||||
expect(deps.blockedForExec).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(deps.spawnRelay).not.toHaveBeenCalled()
|
||||
expect(manager.getGuestEndpointFilePath('Ubuntu')).toBeNull()
|
||||
})
|
||||
|
||||
it('starts one relay per distro, installs hooks, exposes the guest endpoint path, and forwards envelopes', async () => {
|
||||
const { manager, deps } = createManager({})
|
||||
manager.ensureForDistro('Ubuntu', codexHome)
|
||||
|
||||
@@ -126,6 +126,11 @@ export class WslHookRelayManager {
|
||||
if (!distro || this.disposed) {
|
||||
return
|
||||
}
|
||||
// Before the state check, so concurrent ensures still join one start. A passive restore's env
|
||||
// prep ensures the relay before its hold (#35813): its wsl.exe must not start the distro (E9).
|
||||
if ((await this.deps.blockedForExec(distro)) || this.disposed) {
|
||||
return
|
||||
}
|
||||
const key = wslHookRelayStateKey(distro)
|
||||
this.idle.restoreParked(distro)
|
||||
const existing = this.states.get(key)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { posix as pathPosix } from 'node:path'
|
||||
import { parseWslUncPath, toLinuxPath, toWindowsWslUncPath } from '../../shared/wsl-paths'
|
||||
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
|
||||
import { getDefaultWslDistro, getWslHome } from '../wsl'
|
||||
import { getCachedWslHome } from '../wsl-home-cache'
|
||||
import { isWslDistroRunning } from '../wsl-background-distro-gate'
|
||||
import {
|
||||
getSystemCodexHomePath,
|
||||
@@ -186,7 +187,10 @@ export abstract class CodexRuntimeHomeRouting extends CodexRuntimeHomeManagedHom
|
||||
await Promise.all(drains)
|
||||
}
|
||||
|
||||
protected getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null {
|
||||
protected getWslSystemCodexHomePath(
|
||||
target: CodexAccountSelectionTarget,
|
||||
{ probeHome = true }: { probeHome?: boolean } = {}
|
||||
): string | null {
|
||||
if (process.platform !== 'win32') {
|
||||
return null
|
||||
}
|
||||
@@ -194,7 +198,8 @@ export abstract class CodexRuntimeHomeRouting extends CodexRuntimeHomeManagedHom
|
||||
if (!distro) {
|
||||
return null
|
||||
}
|
||||
const home = getWslHome(distro)
|
||||
// Null (unknown home) means the guest's own ~/.codex, which is the system default anyway.
|
||||
const home = probeHome ? getWslHome(distro) : (getCachedWslHome(distro) ?? null)
|
||||
if (home && /^[A-Za-z]:[\\/]/.test(home)) {
|
||||
const linuxHome = toLinuxPath(home).trim()
|
||||
return linuxHome.startsWith('/')
|
||||
|
||||
@@ -20,6 +20,8 @@ import { resolveCodexSessionBackfillPaths } from '../codex/codex-session-backfil
|
||||
import type { CodexSessionBackfillDate } from '../codex/codex-session-backfill-types'
|
||||
import { ManagedCodexHomeTemporarilyUnavailableError } from './host-codex-managed-home-ownership'
|
||||
import { CodexRuntimeHomeRouting } from './runtime-home-service-home-routing'
|
||||
import { blockedWslDistroForExec } from '../wsl/wsl-background-exec-gate'
|
||||
import { getWslHomeAsync } from '../wsl'
|
||||
|
||||
export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting {
|
||||
protected initializeLastSyncedState(): void {
|
||||
@@ -134,6 +136,16 @@ export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting {
|
||||
return this.prepareForCodexLaunch(target, launchEnv, options)
|
||||
}
|
||||
const wslTarget = this.resolveWslDefaultTarget(target)
|
||||
// A passive restore reaches this before its hold (#35813); the probe, drain, sync and bridge
|
||||
// below would each start a stopped distro (E9). A user act passes and does them all.
|
||||
if (await blockedWslDistroForExec(wslTarget.wslDistro)) {
|
||||
return this.getWslCodexHomePathForSelection(wslTarget, { probeHome: false })
|
||||
}
|
||||
const distro = wslTarget.wslDistro?.trim()
|
||||
if (distro && process.platform === 'win32') {
|
||||
// Warm the guest-home cache off the main thread, so the sync reads below skip a 5 s wsl.exe.
|
||||
await getWslHomeAsync(distro)
|
||||
}
|
||||
const homePath = this.getWslCodexHomePathForSelection(wslTarget)
|
||||
// Drain potentially fresher retired-home credentials before the first direct-home spawn.
|
||||
await this.startLegacyWslAuthDrain(wslTarget, { throwOnFailure: true })
|
||||
|
||||
@@ -94,7 +94,10 @@ export abstract class CodexRuntimeHomeState {
|
||||
abstract reconcileLegacySharedHomeForRetainedPanes(): void
|
||||
abstract syncActiveWslSelectionsBeforeRestart(): Promise<void>
|
||||
|
||||
protected abstract getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null
|
||||
protected abstract getWslSystemCodexHomePath(
|
||||
target: CodexAccountSelectionTarget,
|
||||
options?: { probeHome?: boolean }
|
||||
): string | null
|
||||
protected abstract finishWslLaunchPreparation(
|
||||
target: CodexAccountSelectionTarget,
|
||||
homePath: string | null
|
||||
|
||||
@@ -20,7 +20,11 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore {
|
||||
return this.getWslCodexHomePathForSelection(target)
|
||||
}
|
||||
|
||||
protected getWslCodexHomePathForSelection(target: CodexAccountSelectionTarget): string | null {
|
||||
/** `probeHome: false` never runs wsl.exe for the guest home: an uncached one reads as unknown. */
|
||||
protected getWslCodexHomePathForSelection(
|
||||
target: CodexAccountSelectionTarget,
|
||||
options: { probeHome?: boolean } = {}
|
||||
): string | null {
|
||||
const settings = this.store.getSettings()
|
||||
const account = this.getActiveAccount(
|
||||
settings.codexManagedAccounts,
|
||||
@@ -33,7 +37,7 @@ export abstract class CodexRuntimeHomeWsl extends CodexRuntimeHomeWslCore {
|
||||
return accountHome
|
||||
}
|
||||
}
|
||||
return this.getWslSystemCodexHomePath(target)
|
||||
return this.getWslSystemCodexHomePath(target, options)
|
||||
}
|
||||
|
||||
protected getWslLaunchCodexHomePath(
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
testState
|
||||
} from './runtime-home-service-test-harness'
|
||||
|
||||
// These launches are the user's own, so the E9 exec gate lets their prep run.
|
||||
vi.mock('../wsl/wsl-background-exec-gate', () => ({ blockedWslDistroForExec: async () => null }))
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => testState.userDataDir
|
||||
@@ -46,7 +48,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const runtimeAuthPath = join(testState.fakeHomeDir, '.codex', 'auth.json')
|
||||
writeFileSync(runtimeAuthPath, '{"account":"host-system"}\n', 'utf-8')
|
||||
@@ -116,7 +119,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system-token')
|
||||
const managedHomePath = createManagedAuth(
|
||||
@@ -182,7 +186,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const systemCodexHomePath = join(wslHome, '.codex')
|
||||
mkdirSync(systemCodexHomePath, { recursive: true })
|
||||
@@ -240,7 +245,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const firstAuth = createCodexAuthJson('first@example.com', 'acct-first', 'first-token')
|
||||
const secondAuth = createCodexAuthJson('second@example.com', 'acct-second', 'second-token')
|
||||
@@ -320,7 +326,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
let finishDrain: (() => void) | undefined
|
||||
const startLegacyWslRuntimeAuthDrain = vi.fn(
|
||||
@@ -373,9 +380,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const launch = service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
await Promise.resolve()
|
||||
|
||||
expect(startLegacyWslRuntimeAuthDrain).toHaveBeenCalledTimes(1)
|
||||
// The E9 gate and the async home warm-up come first.
|
||||
await vi.waitFor(() => expect(startLegacyWslRuntimeAuthDrain).toHaveBeenCalledTimes(1))
|
||||
expect(startLegacyWslRuntimeAuthDrain.mock.calls[0]?.[1]).toEqual({ throwOnFailure: true })
|
||||
expect(startWslCodexSessionBridgeInBackground).not.toHaveBeenCalled()
|
||||
|
||||
@@ -400,7 +406,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
vi.doMock('../codex/codex-pane-account-registry', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof CodexPaneAccountRegistry>()),
|
||||
@@ -438,7 +445,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const wslManagedAuth = createCodexAuthJson(
|
||||
'wsl@example.com',
|
||||
@@ -520,7 +528,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const linuxHomePath = '/mnt/c/Users/alice/orca/codex-accounts/drive-account/home'
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => 'C:\\Users\\alice'
|
||||
getWslHome: () => 'C:\\Users\\alice',
|
||||
getWslHomeAsync: async () => 'C:\\Users\\alice'
|
||||
}))
|
||||
vi.doMock('./wsl-codex-auth-batch-reader', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslCodexAuthBatchReader>()),
|
||||
@@ -629,7 +638,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
)
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => 'C:\\Users\\alice'
|
||||
getWslHome: () => 'C:\\Users\\alice',
|
||||
getWslHomeAsync: async () => 'C:\\Users\\alice'
|
||||
}))
|
||||
vi.doMock('./wsl-codex-auth-batch-reader', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslCodexAuthBatchReader>()),
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import type * as CodexConfigMirror from '../codex/codex-config-mirror'
|
||||
import type * as CodexHomePaths from '../codex/codex-home-paths'
|
||||
import { createSettings } from './runtime-home-settings-test-fixtures'
|
||||
import {
|
||||
createCodexAuthJson,
|
||||
createManagedAuth,
|
||||
createStore,
|
||||
setupRuntimeHomeTest,
|
||||
teardownRuntimeHomeTest,
|
||||
testState
|
||||
} from './runtime-home-service-test-harness'
|
||||
|
||||
// The real rule: a stopped distro blocks, except for a user act on it.
|
||||
vi.mock('../wsl/wsl-background-exec-gate', async () => {
|
||||
const { isWslUserActFor, runAsWslUserAct } = await import('../wsl/wsl-user-act-scope')
|
||||
return {
|
||||
blockedWslDistroForExec: async (distro: string | null | undefined) =>
|
||||
distro && !isWslUserActFor(distro) ? { distro, state: 'stopped' } : null,
|
||||
// The same scope instance the gate reads, whatever the module registry did between tests.
|
||||
runAsWslExecUserAct: runAsWslUserAct
|
||||
}
|
||||
})
|
||||
vi.mock('electron', () => ({ app: { getPath: () => testState.userDataDir } }))
|
||||
vi.mock(import('node:os'), async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
homedir: () => testState.fakeHomeDir
|
||||
}))
|
||||
|
||||
const LINUX_ACCOUNT_HOME = '/home/alice/.local/share/orca/codex-accounts/account-1/home'
|
||||
|
||||
function wslMocks() {
|
||||
const mocks = {
|
||||
getWslHome: vi.fn(() => join(testState.userDataDir, 'wsl-home')),
|
||||
getWslHomeAsync: vi.fn(async () => join(testState.userDataDir, 'wsl-home')),
|
||||
drain: vi.fn(async () => {}),
|
||||
bridge: vi.fn(async () => {}),
|
||||
syncInstructions: vi.fn(),
|
||||
syncConfig: vi.fn()
|
||||
}
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: mocks.getWslHome,
|
||||
getWslHomeAsync: mocks.getWslHomeAsync
|
||||
}))
|
||||
vi.doMock('./legacy-wsl-runtime-auth-drain', () => ({
|
||||
startLegacyWslRuntimeAuthDrain: mocks.drain
|
||||
}))
|
||||
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
|
||||
startWslCodexSessionBridgeInBackground: mocks.bridge
|
||||
}))
|
||||
vi.doMock('../codex/codex-home-paths', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof CodexHomePaths>()),
|
||||
syncCodexGlobalInstructionsIntoManagedHome: mocks.syncInstructions
|
||||
}))
|
||||
vi.doMock('../codex/codex-config-mirror', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof CodexConfigMirror>()),
|
||||
syncSystemConfigIntoManagedCodexHome: mocks.syncConfig
|
||||
}))
|
||||
return mocks
|
||||
}
|
||||
|
||||
function accountStore(selected: boolean) {
|
||||
const managedHomePath = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
createCodexAuthJson('wsl@example.com', 'acct-wsl', 'managed-token')
|
||||
)
|
||||
return createStore(
|
||||
createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'wsl@example.com',
|
||||
managedHomePath,
|
||||
managedHomeRuntime: 'wsl',
|
||||
wslDistro: 'Ubuntu',
|
||||
wslLinuxHomePath: LINUX_ACCOUNT_HOME,
|
||||
providerAccountId: 'acct-wsl',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-wsl',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountIdsByRuntime: {
|
||||
host: null,
|
||||
wsl: { Ubuntu: selected ? 'account-1' : null }
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
beforeEach(() => {
|
||||
setupRuntimeHomeTest()
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
teardownRuntimeHomeTest()
|
||||
})
|
||||
|
||||
// A passive restore reaches launch prep before its hold (#35813); prep must not start the distro (E9).
|
||||
describe('Codex launch prep for a stopped WSL distro', () => {
|
||||
it('a passive restore gets the account home without a probe, drain, sync or bridge', async () => {
|
||||
const mocks = wslMocks()
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the Store methods the runtime home service reads.
|
||||
const service = new CodexRuntimeHomeService(accountStore(true) as never)
|
||||
|
||||
const home = await service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
|
||||
expect(home).toBeTruthy()
|
||||
expect(mocks.getWslHome).not.toHaveBeenCalled()
|
||||
expect(mocks.getWslHomeAsync).not.toHaveBeenCalled()
|
||||
expect(mocks.drain).not.toHaveBeenCalled()
|
||||
expect(mocks.syncInstructions).not.toHaveBeenCalled()
|
||||
expect(mocks.syncConfig).not.toHaveBeenCalled()
|
||||
expect(mocks.bridge).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('a passive restore on system default reads an unknown home as null, never probing', async () => {
|
||||
const mocks = wslMocks()
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the Store methods the runtime home service reads.
|
||||
const service = new CodexRuntimeHomeService(accountStore(false) as never)
|
||||
|
||||
await expect(
|
||||
service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
).resolves.toBeNull()
|
||||
expect(mocks.getWslHome).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('the same launch as a user act warms the home off the main thread and drains', async () => {
|
||||
const mocks = wslMocks()
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const { runAsWslExecUserAct } = await import('../wsl/wsl-background-exec-gate')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the Store methods the runtime home service reads.
|
||||
const service = new CodexRuntimeHomeService(accountStore(true) as never)
|
||||
|
||||
await runAsWslExecUserAct('Ubuntu', () =>
|
||||
service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
)
|
||||
|
||||
expect(mocks.getWslHomeAsync).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(mocks.drain).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
testState
|
||||
} from './runtime-home-service-test-harness'
|
||||
|
||||
// These launches are the user's own, so the E9 exec gate lets their prep run.
|
||||
vi.mock('../wsl/wsl-background-exec-gate', () => ({ blockedWslDistroForExec: async () => null }))
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => testState.userDataDir
|
||||
@@ -51,7 +53,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const wslSystemHomePath = join(wslHome, '.codex')
|
||||
mkdirSync(wslSystemHomePath, { recursive: true })
|
||||
@@ -101,7 +104,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
@@ -158,7 +162,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
@@ -198,7 +203,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'debian-wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => null,
|
||||
getWslHome: (distro: string) => (distro === 'Debian' ? wslHome : null)
|
||||
getWslHome: (distro: string) => (distro === 'Debian' ? wslHome : null),
|
||||
getWslHomeAsync: async (distro: string) => (distro === 'Debian' ? wslHome : null)
|
||||
}))
|
||||
vi.doMock('../../shared/wsl-paths', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof WslPaths>()
|
||||
@@ -273,7 +279,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = join(testState.userDataDir, 'ubuntu-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const managedHomePath = join(wslHome, '.local', 'share', 'orca', 'codex-accounts', 'a', 'home')
|
||||
const retiredBridgeRuns = vi.fn()
|
||||
@@ -345,7 +352,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslHome = `\\\\wsl.localhost\\Ubuntu${guestHome.replaceAll('/', '\\')}`
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
getWslHome: () => wslHome,
|
||||
getWslHomeAsync: async () => wslHome
|
||||
}))
|
||||
const ownerAuth = createCodexAuthJson(
|
||||
'owner@example.com',
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis
|
||||
mintedSessionId: ctx.ownsNewIncarnation,
|
||||
resumeOnSelectedAccount: Boolean(args.replacesPtyId),
|
||||
emptySessionIdSkipsHostEnv: false,
|
||||
wslPassiveRestore: args.wslPassiveRestore === true,
|
||||
requestedAgentTeamsPath: ctx.requestedAgentTeamsPath,
|
||||
provider: ctx.provider,
|
||||
deps: ctx.deps,
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls'
|
||||
import { isPwshAvailableAsync } from '../../../pwsh'
|
||||
import { LocalPtyProvider } from '../../../providers/local-pty-provider'
|
||||
import type { LocalPtyProviderOptions } from '../../../providers/local-pty-provider-types'
|
||||
import { runAsWslLaunchUserAct } from '../../../wsl/wsl-background-exec-gate'
|
||||
import {
|
||||
addOrcaWslInteropEnv,
|
||||
stampWslOrchestrationCompatibilityHost
|
||||
@@ -38,6 +40,101 @@ export function configureLocalPtyProvider(args: {
|
||||
return
|
||||
}
|
||||
const { runtime, getSettings, getSelectedCodexHomePath, trustedTerminalHandleEnv } = args
|
||||
const buildLocalSpawnEnv: NonNullable<LocalPtyProviderOptions['buildSpawnEnv']> = async (
|
||||
id,
|
||||
baseEnv,
|
||||
ctx
|
||||
) => {
|
||||
await prepareAntigravityAccountForLaunch({
|
||||
launchAgent: ctx?.launchAgent,
|
||||
command: ctx?.command,
|
||||
isWsl: ctx?.isWsl,
|
||||
env: baseEnv,
|
||||
envIsComplete: true
|
||||
})
|
||||
const codexSelectionTarget: CodexAccountSelectionTarget =
|
||||
ctx?.isWsl === true
|
||||
? { runtime: 'wsl', wslDistro: ctx.wslDistro ?? null }
|
||||
: { runtime: 'host' }
|
||||
const selectedCodexHomePath = getCompatibleSelectedCodexHomePath(
|
||||
codexSelectionTarget,
|
||||
ctx?.codexHomePathOverride
|
||||
? ctx.codexHomePathOverride.value
|
||||
: ((await getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv)) ?? null)
|
||||
)
|
||||
const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath
|
||||
const ptySettings = getSettings?.()
|
||||
await inheritOmpLaunchEnvironment(baseEnv, {
|
||||
shellPath: ctx?.shellPath,
|
||||
explicitEnv: ctx?.explicitEnv,
|
||||
isWsl: ctx?.isWsl,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
await awaitExplicitAgentPluginGuestReadiness({
|
||||
settings: ptySettings,
|
||||
isWsl: ctx?.isWsl === true,
|
||||
distro: ctx?.wslDistro,
|
||||
codexHomePath: selectedCodexHomePath,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
await getClaudeAgentViewService().readyForLaunch({
|
||||
settings: ptySettings,
|
||||
isWsl: ctx?.isWsl,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
const env = buildPtyHostEnv(id, baseEnv, {
|
||||
isPackaged: getAppEnvironment().isPackaged(),
|
||||
resourcesPath: process.resourcesPath,
|
||||
userDataPath: getAppEnvironment().getPath('userData'),
|
||||
selectedCodexHomePath,
|
||||
skipCodexHomeEnv,
|
||||
stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({
|
||||
target: codexSelectionTarget,
|
||||
selectedCodexHomePath,
|
||||
skipCodexHomeEnv,
|
||||
settings: ptySettings
|
||||
}),
|
||||
launchCommand: ctx?.command,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
shellPath: ctx?.shellPath,
|
||||
isWsl: ctx?.isWsl,
|
||||
wslDistro: ctx?.wslDistro ?? null,
|
||||
agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings),
|
||||
claudeSubagentNavigationEnabled: ptySettings?.claudeSubagentNavigationEnabled,
|
||||
terminalInlineImages: ptySettings?.terminalInlineImages,
|
||||
disabledTuiAgents: ptySettings?.disabledTuiAgents,
|
||||
networkProxySettings: ptySettings,
|
||||
routeBrowserOpensToClient: runtime?.shouldRelayTerminalBrowserOpens?.()
|
||||
})
|
||||
// Supply terminal identity at process start so agents can address orchestration without another RPC.
|
||||
const requestedHandle = baseEnv.ORCA_TERMINAL_HANDLE
|
||||
const preAllocatedHandle =
|
||||
requestedHandle && trustedTerminalHandleEnv.has(requestedHandle)
|
||||
? requestedHandle
|
||||
: runtime?.preAllocateHandleForPty(id)
|
||||
if (requestedHandle && requestedHandle !== preAllocatedHandle) {
|
||||
delete env.ORCA_TERMINAL_HANDLE
|
||||
}
|
||||
if (preAllocatedHandle) {
|
||||
env.ORCA_TERMINAL_HANDLE = preAllocatedHandle
|
||||
}
|
||||
stampWslOrchestrationCompatibilityHost(
|
||||
env,
|
||||
runtime?.getOrchestrationCompatibilityHostId?.(),
|
||||
ctx?.isWsl === true ? ctx.wslDistro : null
|
||||
)
|
||||
stampRuntimeSourceEnv(
|
||||
env,
|
||||
getRuntimeSourceStamp(runtime, getAppEnvironment().getPath('userData'))
|
||||
)
|
||||
if (ctx?.isWsl === true) {
|
||||
addOrcaWslInteropEnv(env)
|
||||
}
|
||||
return env
|
||||
}
|
||||
localProvider.configure({
|
||||
isHistoryEnabled: () => getSettings?.()?.terminalScopeHistoryByWorktree ?? true,
|
||||
getWindowsShell: () => getSettings?.()?.terminalWindowsShell,
|
||||
@@ -45,97 +142,16 @@ export function configureLocalPtyProvider(args: {
|
||||
getWindowsPowerShellImplementation: () =>
|
||||
getSettings ? (getSettings()?.terminalWindowsPowerShellImplementation ?? 'auto') : undefined,
|
||||
pwshAvailable: () => isPwshAvailableAsync(),
|
||||
buildSpawnEnv: async (id, baseEnv, ctx) => {
|
||||
await prepareAntigravityAccountForLaunch({
|
||||
launchAgent: ctx?.launchAgent,
|
||||
command: ctx?.command,
|
||||
isWsl: ctx?.isWsl,
|
||||
env: baseEnv,
|
||||
envIsComplete: true
|
||||
})
|
||||
const codexSelectionTarget: CodexAccountSelectionTarget =
|
||||
ctx?.isWsl === true
|
||||
? { runtime: 'wsl', wslDistro: ctx.wslDistro ?? null }
|
||||
: { runtime: 'host' }
|
||||
const selectedCodexHomePath = getCompatibleSelectedCodexHomePath(
|
||||
codexSelectionTarget,
|
||||
ctx?.codexHomePathOverride
|
||||
? ctx.codexHomePathOverride.value
|
||||
: ((await getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv)) ?? null)
|
||||
)
|
||||
const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath
|
||||
const ptySettings = getSettings?.()
|
||||
await inheritOmpLaunchEnvironment(baseEnv, {
|
||||
shellPath: ctx?.shellPath,
|
||||
explicitEnv: ctx?.explicitEnv,
|
||||
isWsl: ctx?.isWsl,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
await awaitExplicitAgentPluginGuestReadiness({
|
||||
settings: ptySettings,
|
||||
isWsl: ctx?.isWsl === true,
|
||||
distro: ctx?.wslDistro,
|
||||
codexHomePath: selectedCodexHomePath,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
await getClaudeAgentViewService().readyForLaunch({
|
||||
settings: ptySettings,
|
||||
isWsl: ctx?.isWsl,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
launchCommand: ctx?.command
|
||||
})
|
||||
const env = buildPtyHostEnv(id, baseEnv, {
|
||||
isPackaged: getAppEnvironment().isPackaged(),
|
||||
resourcesPath: process.resourcesPath,
|
||||
userDataPath: getAppEnvironment().getPath('userData'),
|
||||
selectedCodexHomePath,
|
||||
skipCodexHomeEnv,
|
||||
stripInheritedOrcaCodexHome: shouldStripInheritedOrcaCodexHome({
|
||||
target: codexSelectionTarget,
|
||||
selectedCodexHomePath,
|
||||
skipCodexHomeEnv,
|
||||
settings: ptySettings
|
||||
}),
|
||||
launchCommand: ctx?.command,
|
||||
launchAgent: ctx?.launchAgent,
|
||||
shellPath: ctx?.shellPath,
|
||||
isWsl: ctx?.isWsl,
|
||||
wslDistro: ctx?.wslDistro ?? null,
|
||||
agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings),
|
||||
claudeSubagentNavigationEnabled: ptySettings?.claudeSubagentNavigationEnabled,
|
||||
terminalInlineImages: ptySettings?.terminalInlineImages,
|
||||
disabledTuiAgents: ptySettings?.disabledTuiAgents,
|
||||
networkProxySettings: ptySettings,
|
||||
routeBrowserOpensToClient: runtime?.shouldRelayTerminalBrowserOpens?.()
|
||||
})
|
||||
// Supply terminal identity at process start so agents can address orchestration without another RPC.
|
||||
const requestedHandle = baseEnv.ORCA_TERMINAL_HANDLE
|
||||
const preAllocatedHandle =
|
||||
requestedHandle && trustedTerminalHandleEnv.has(requestedHandle)
|
||||
? requestedHandle
|
||||
: runtime?.preAllocateHandleForPty(id)
|
||||
if (requestedHandle && requestedHandle !== preAllocatedHandle) {
|
||||
delete env.ORCA_TERMINAL_HANDLE
|
||||
}
|
||||
if (preAllocatedHandle) {
|
||||
env.ORCA_TERMINAL_HANDLE = preAllocatedHandle
|
||||
}
|
||||
stampWslOrchestrationCompatibilityHost(
|
||||
env,
|
||||
runtime?.getOrchestrationCompatibilityHostId?.(),
|
||||
ctx?.isWsl === true ? ctx.wslDistro : null
|
||||
)
|
||||
stampRuntimeSourceEnv(
|
||||
env,
|
||||
getRuntimeSourceStamp(runtime, getAppEnvironment().getPath('userData'))
|
||||
)
|
||||
if (ctx?.isWsl === true) {
|
||||
addOrcaWslInteropEnv(env)
|
||||
}
|
||||
return env
|
||||
},
|
||||
// Daemon-off launches build their env here: the same user-act rule as the daemon path.
|
||||
buildSpawnEnv: (id, baseEnv, ctx) =>
|
||||
runAsWslLaunchUserAct(
|
||||
{
|
||||
isWsl: ctx?.isWsl === true,
|
||||
passiveRestore: ctx?.wslPassiveRestore === true,
|
||||
distro: ctx?.wslDistro
|
||||
},
|
||||
async () => buildLocalSpawnEnv(id, baseEnv, ctx)
|
||||
),
|
||||
onSpawned: (id, incarnationId) => runtime?.onPtySpawned(id, incarnationId),
|
||||
onExit: (id, code, incarnationId, cause) => {
|
||||
if (!isCurrentPtyExit({ id, incarnationId })) {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ensureBackfill: vi.fn(async () => {}),
|
||||
blockedPath: vi.fn(async (): Promise<{ distro: string; state: string } | null> => null)
|
||||
}))
|
||||
|
||||
// The common single-distro setup: a null distro resolves to this default.
|
||||
vi.mock('../../wsl', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
listWslDistrosAsync: async () => ['Ubuntu']
|
||||
}))
|
||||
vi.mock('../../codex/codex-state-db-backfill-recovery', () => ({
|
||||
ensureCodexStateDbBackfillRecoveryStarted: mocks.ensureBackfill
|
||||
}))
|
||||
vi.mock('../../wsl/wsl-unc-access-gate', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
blockedWslDistroForPath: mocks.blockedPath
|
||||
}))
|
||||
|
||||
import { resolvePtySpawnHostEnv } from './spawn-host-env'
|
||||
import { noCodexResumeLaunch, stripSequencedStartupResumeArgv } from './host-env/codex-resume'
|
||||
import { isWslUserActFor } from '../../wsl/wsl-user-act-scope'
|
||||
import type { IPtyProvider } from '../../providers/pty-provider-contract'
|
||||
import type { CodexAccountSelectionTarget } from '../../codex-accounts/runtime-selection'
|
||||
|
||||
const HOME = '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex'
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
function resolve(
|
||||
wslPassiveRestore: boolean,
|
||||
seen: boolean[],
|
||||
launch: { target?: CodexAccountSelectionTarget; expectedWslDistro?: string | null } = {}
|
||||
) {
|
||||
return resolvePtySpawnHostEnv({
|
||||
launchAgent: 'codex',
|
||||
command: 'codex',
|
||||
resumeProviderSession: undefined,
|
||||
env: {},
|
||||
cwd: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo',
|
||||
shellOverride: undefined,
|
||||
expectedWslDistro: launch.expectedWslDistro === undefined ? 'Ubuntu' : launch.expectedWslDistro,
|
||||
codexSelectionTarget: launch.target ?? { runtime: 'wsl', wslDistro: 'Ubuntu' },
|
||||
preAdopted: false,
|
||||
isDaemonHostSpawn: false,
|
||||
sessionId: 'session-1',
|
||||
callerNamedSession: true,
|
||||
mintedSessionId: false,
|
||||
resumeOnSelectedAccount: false,
|
||||
emptySessionIdSkipsHostEnv: false,
|
||||
wslPassiveRestore,
|
||||
requestedAgentTeamsPath: undefined,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a non-daemon spawn returns before any provider call.
|
||||
provider: {} as IPtyProvider,
|
||||
deps: {
|
||||
prepareCodexResumeHome: () => null,
|
||||
resolveCodexResumeLaunch: async () => noCodexResumeLaunch('codex'),
|
||||
noCodexResumeLaunch,
|
||||
reconcileSharedRuntimeResumeHome: async (home) => home.codexHomePath,
|
||||
stripSequencedStartupResumeArgv,
|
||||
getSelectedCodexHomePath: async () => {
|
||||
seen.push(isWslUserActFor('Ubuntu'))
|
||||
return HOME
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
mocks.ensureBackfill.mockClear()
|
||||
mocks.blockedPath.mockReset().mockResolvedValue(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
})
|
||||
|
||||
// Spawn env prep runs before #35813's hold: only a launch someone asked for may start the distro (E9).
|
||||
describe('WSL spawn host env', () => {
|
||||
it('runs a launch someone asked for as a user act on its distro', async () => {
|
||||
const seen: boolean[] = []
|
||||
await resolve(false, seen)
|
||||
expect(seen).toEqual([true])
|
||||
})
|
||||
|
||||
it('runs a passive restore outside any user act, so its prep is gated', async () => {
|
||||
const seen: boolean[] = []
|
||||
await resolve(true, seen)
|
||||
expect(seen).toEqual([false])
|
||||
})
|
||||
|
||||
it('never turns a passive restore with no named distro into a user act on the default one', async () => {
|
||||
const seen: boolean[] = []
|
||||
await resolve(true, seen, {
|
||||
target: { runtime: 'wsl', wslDistro: null },
|
||||
expectedWslDistro: null
|
||||
})
|
||||
expect(seen).toEqual([false])
|
||||
})
|
||||
|
||||
it('never turns a host-target spawn into a user act on the default distro', async () => {
|
||||
const seen: boolean[] = []
|
||||
await resolve(false, seen, { target: { runtime: 'host' }, expectedWslDistro: null })
|
||||
expect(seen).toEqual([false])
|
||||
})
|
||||
|
||||
it('a WSL launch someone asked for, with no named distro, acts on the default one', async () => {
|
||||
const seen: boolean[] = []
|
||||
await resolve(false, seen, {
|
||||
target: { runtime: 'wsl', wslDistro: null },
|
||||
expectedWslDistro: null
|
||||
})
|
||||
expect(seen).toEqual([true])
|
||||
})
|
||||
|
||||
it('skips the state-DB recovery (a UNC read) when the distro is stopped', async () => {
|
||||
mocks.blockedPath.mockResolvedValue({ distro: 'Ubuntu', state: 'stopped' })
|
||||
await resolve(true, [])
|
||||
expect(mocks.blockedPath).toHaveBeenCalledWith(HOME)
|
||||
expect(mocks.ensureBackfill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still starts the state-DB recovery when the distro runs', async () => {
|
||||
await resolve(true, [])
|
||||
expect(mocks.ensureBackfill).toHaveBeenCalledWith(HOME)
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,8 @@ 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 { blockedWslDistroForPath } from '../../wsl/wsl-unc-access-gate'
|
||||
import { runAsWslLaunchUserAct } from '../../wsl/wsl-background-exec-gate'
|
||||
import {
|
||||
codexHomePathsEqual,
|
||||
getCompatibleSelectedCodexHomePath,
|
||||
@@ -86,6 +88,8 @@ type PtySpawnHostEnvArgs = {
|
||||
resumeOnSelectedAccount: boolean
|
||||
/** Host accepts empty IDs without host env; window rejects them. */
|
||||
emptySessionIdSkipsHostEnv: boolean
|
||||
/** A restore no user act asked for (#35813): its prep must not start a stopped WSL distro. */
|
||||
wslPassiveRestore?: boolean
|
||||
requestedAgentTeamsPath: string | undefined
|
||||
provider: IPtyProvider
|
||||
deps: PtySpawnCodexResumeDeps & {
|
||||
@@ -96,7 +100,23 @@ type PtySpawnHostEnvArgs = {
|
||||
mark?: (phase: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Every spawn but a passive restore is a launch someone asked for, so its prep may start the WSL
|
||||
* distro it runs in. A passive restore's prep goes through the E9 gates and skips on a stopped one.
|
||||
*/
|
||||
export async function resolvePtySpawnHostEnv(args: PtySpawnHostEnvArgs): Promise<PtySpawnHostEnv> {
|
||||
const target = args.codexSelectionTarget
|
||||
return runAsWslLaunchUserAct(
|
||||
{
|
||||
isWsl: target.runtime === 'wsl',
|
||||
passiveRestore: args.wslPassiveRestore === true,
|
||||
distro: args.expectedWslDistro || target.wslDistro?.trim() || null
|
||||
},
|
||||
() => resolvePtySpawnHostEnvForAct(args)
|
||||
)
|
||||
}
|
||||
|
||||
async function resolvePtySpawnHostEnvForAct(args: PtySpawnHostEnvArgs): Promise<PtySpawnHostEnv> {
|
||||
const { deps, codexSelectionTarget: target } = args
|
||||
const codexResumePreparation = args.preAdopted
|
||||
? null
|
||||
@@ -151,7 +171,12 @@ export async function resolvePtySpawnHostEnv(args: PtySpawnHostEnvArgs): Promise
|
||||
})
|
||||
selectedCodexHomePath = resolution instanceof Promise ? await resolution : resolution
|
||||
}
|
||||
if (launchesCodex && selectedCodexHomePath) {
|
||||
// Its pending check reads the home's state DB, which starts a stopped distro's UNC home (E9).
|
||||
if (
|
||||
launchesCodex &&
|
||||
selectedCodexHomePath &&
|
||||
!(await blockedWslDistroForPath(selectedCodexHomePath))
|
||||
) {
|
||||
await ensureCodexStateDbBackfillRecoveryStarted(selectedCodexHomePath)
|
||||
}
|
||||
args.mark?.('codex_home')
|
||||
|
||||
@@ -75,4 +75,31 @@ describe('independent local terminal Pi ownership', () => {
|
||||
}
|
||||
expect(env).toMatchObject({ ...terminalEnv, APP_ENV: 'kept' })
|
||||
})
|
||||
|
||||
it('tells the daemon-off env builder whether this spawn is a passive restore (#35813)', async () => {
|
||||
const seen: (boolean | undefined)[] = []
|
||||
const getOptions = () => ({
|
||||
buildSpawnEnv: (
|
||||
_id: string,
|
||||
baseEnv: Record<string, string>,
|
||||
ctx?: { wslPassiveRestore?: boolean }
|
||||
) => {
|
||||
seen.push(ctx?.wslPassiveRestore)
|
||||
return baseEnv
|
||||
}
|
||||
})
|
||||
await buildLocalPtySpawnEnvironment({
|
||||
id: 'restored',
|
||||
spawn: { cols: 80, rows: 24, wslPassiveRestore: true },
|
||||
getOptions,
|
||||
plan
|
||||
})
|
||||
await buildLocalPtySpawnEnvironment({
|
||||
id: 'new',
|
||||
spawn: { cols: 80, rows: 24 },
|
||||
getOptions,
|
||||
plan
|
||||
})
|
||||
expect(seen).toEqual([true, false])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,8 @@ export type LocalPtyProviderOptions = {
|
||||
shellPath?: string
|
||||
isWsl?: boolean
|
||||
wslDistro?: string | null
|
||||
/** A restore no user act asked for (#35813): its prep must not start a stopped distro. */
|
||||
wslPassiveRestore?: boolean
|
||||
}
|
||||
// Async Codex hook/trust preparation must keep Electron main responsive (#16441).
|
||||
) => Record<string, string> | Promise<Record<string, string>>
|
||||
|
||||
@@ -46,7 +46,8 @@ export function buildLocalPtySpawnEnvironment(args: {
|
||||
cwd: plan.cwd,
|
||||
shellPath: plan.shellPath,
|
||||
isWsl: plan.isWslShell,
|
||||
wslDistro: plan.launchWslDistro
|
||||
wslDistro: plan.launchWslDistro,
|
||||
wslPassiveRestore: spawn.wslPassiveRestore === true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../wsl', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
listWslDistrosAsync: async () => ['Ubuntu']
|
||||
}))
|
||||
|
||||
import { runAsWslLaunchUserAct } from './wsl-background-exec-gate'
|
||||
import { isWslUserActFor } from './wsl-user-act-scope'
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
})
|
||||
|
||||
async function actsOnUbuntu(launch: Parameters<typeof runAsWslLaunchUserAct>[0]): Promise<boolean> {
|
||||
return runAsWslLaunchUserAct(launch, async () => isWslUserActFor('Ubuntu'))
|
||||
}
|
||||
|
||||
// The default distro stands in for a null one only for a WSL launch someone asked for (E9).
|
||||
describe('runAsWslLaunchUserAct', () => {
|
||||
it('acts on the named or default distro for a WSL launch someone asked for', async () => {
|
||||
await expect(
|
||||
actsOnUbuntu({ isWsl: true, passiveRestore: false, distro: 'Ubuntu' })
|
||||
).resolves.toBe(true)
|
||||
await expect(actsOnUbuntu({ isWsl: true, passiveRestore: false, distro: null })).resolves.toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('never acts for a passive restore or a non-WSL launch, even with a default distro', async () => {
|
||||
await expect(actsOnUbuntu({ isWsl: true, passiveRestore: true, distro: null })).resolves.toBe(
|
||||
false
|
||||
)
|
||||
await expect(
|
||||
actsOnUbuntu({ isWsl: true, passiveRestore: true, distro: 'Ubuntu' })
|
||||
).resolves.toBe(false)
|
||||
await expect(actsOnUbuntu({ isWsl: false, passiveRestore: false, distro: null })).resolves.toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -36,6 +36,19 @@ export async function assertWslDistroRunningForExec(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A terminal launch someone asked for runs as a user act on its distro (null: the default); a
|
||||
* passive restore (#35813) or a non-WSL launch runs as-is, so the E9 gates apply to it.
|
||||
*/
|
||||
export function runAsWslLaunchUserAct<T>(
|
||||
launch: { isWsl: boolean; passiveRestore: boolean; distro: string | null | undefined },
|
||||
act: () => Promise<T>
|
||||
): Promise<T> {
|
||||
return launch.isWsl && !launch.passiveRestore
|
||||
? runAsWslExecUserAct(launch.distro ?? null, act)
|
||||
: act()
|
||||
}
|
||||
|
||||
/** Runs `act` as a user act on `distro` (null: the default), so its `wsl.exe` runs may start it. */
|
||||
export async function runAsWslExecUserAct<T>(
|
||||
distro: string | null | undefined,
|
||||
|
||||
Reference in New Issue
Block a user