Support WSL Codex launch homes (#4318)

This commit is contained in:
Jinwoo Hong 2026-05-31 17:29:37 -04:00 committed by GitHub
parent 34c6aa4bc2
commit c05cc3150a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 706 additions and 162 deletions

View File

@ -159,6 +159,28 @@ function getSystemLaunchCodexHomePath(): string {
return join(testState.userDataDir, 'codex-runtime-home', 'launch', 'host', 'system', 'home')
}
function getWslRuntimeCodexHomePath(wslHome: string): string {
return join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
}
function getWslLaunchCodexHomePath(wslHome: string, accountId: string | null): string {
const segment =
accountId === null
? 'system'
: `account-${createHash('sha256').update(accountId).digest('hex').slice(0, 32)}`
return join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'launch',
'wsl',
segment,
'home'
)
}
function normalizeLinkTarget(linkTarget: string): string {
return process.platform === 'win32'
? linkTarget.replace(/^\\\\\?\\/, '').toLowerCase()
@ -990,26 +1012,23 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslRuntimeHomePath = getWslRuntimeCodexHomePath(wslHome)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"host-system"}\n')
expect(service.prepareForCodexLaunch()).toBe(getSystemLaunchCodexHomePath())
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslLaunchHomePath
)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(
'{"account":"wsl"}\n'
)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(
'{"account":"wsl"}\n'
)
expect(service.prepareForRateLimitFetch()).toBe(getSystemLaunchCodexHomePath())
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslLaunchHomePath
)
} finally {
if (originalPlatform) {
@ -1062,23 +1081,16 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslLaunchHomePath
)
expect(store.updateSettings).toHaveBeenCalledWith({
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
})
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(systemAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(systemAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@ -1086,7 +1098,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('switches WSL accounts by rewriting one stable WSL runtime home', async () => {
it('switches WSL accounts by using separate selected WSL launch homes', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@ -1139,16 +1151,12 @@ describe('CodexRuntimeHomeService', () => {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslRuntimeHomePath = getWslRuntimeCodexHomePath(wslHome)
const firstLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
const secondLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-2')
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(firstLaunchHomePath)
expect(readFileSync(join(firstLaunchHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
store.updateSettings({
@ -1156,7 +1164,9 @@ describe('CodexRuntimeHomeService', () => {
})
service.syncForCurrentSelection(target)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(secondLaunchHomePath)
expect(readFileSync(join(secondLaunchHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
expect(readFileSync(join(firstLaunchHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
} finally {
if (originalPlatform) {
@ -1165,6 +1175,87 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('shares WSL non-auth runtime state across selected launch homes', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const firstAuth = createCodexAuthJson('first@example.com', 'acct-first', 'first-token')
const secondAuth = createCodexAuthJson('second@example.com', 'acct-second', 'second-token')
const firstManagedHomePath = createManagedAuth(testState.userDataDir, 'account-1', firstAuth)
const secondManagedHomePath = createManagedAuth(testState.userDataDir, 'account-2', secondAuth)
const wslRuntimeHomePath = getWslRuntimeCodexHomePath(wslHome)
mkdirSync(join(wslRuntimeHomePath, 'sessions'), { recursive: true })
writeFileSync(join(wslRuntimeHomePath, 'config.toml'), 'model = "gpt-5"\n', 'utf-8')
const settings = createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'first@example.com',
managedHomePath: firstManagedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-first',
workspaceLabel: null,
workspaceAccountId: 'acct-first',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
},
{
id: 'account-2',
email: 'second@example.com',
managedHomePath: secondManagedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-2/home',
providerAccountId: 'acct-second',
workspaceLabel: null,
workspaceAccountId: 'acct-second',
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2
}
],
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'account-1' } }
})
const store = createStore(settings)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const firstLaunchHomePath = service.prepareForCodexLaunch(target)
const firstConfigPath = join(firstLaunchHomePath!, 'config.toml')
rmSync(firstConfigPath, { force: true })
writeFileSync(firstConfigPath, 'model = "gpt-5.5"\nfast_mode = true\n', 'utf-8')
settings.activeCodexManagedAccountIdsByRuntime = { host: null, wsl: { Ubuntu: 'account-2' } }
const secondLaunchHomePath = service.prepareForCodexLaunch(target)
expect(readFileSync(join(wslRuntimeHomePath, 'config.toml'), 'utf-8')).toBe(
'model = "gpt-5.5"\nfast_mode = true\n'
)
expect(readFileSync(join(secondLaunchHomePath!, 'config.toml'), 'utf-8')).toBe(
'model = "gpt-5.5"\nfast_mode = true\n'
)
expectResourceLinkedOrCopied(
join(secondLaunchHomePath!, 'sessions'),
join(wslRuntimeHomePath, 'sessions')
)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('does not use host auth baseline to accept stale WSL runtime auth', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
@ -1192,14 +1283,8 @@ describe('CodexRuntimeHomeService', () => {
'wsl-account',
wslManagedAuth
)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslRuntimeHomePath = getWslRuntimeCodexHomePath(wslHome)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'wsl-account')
mkdirSync(wslRuntimeHomePath, { recursive: true })
writeFileSync(join(wslRuntimeHomePath, 'auth.json'), staleWslRuntimeAuth, 'utf-8')
const store = createStore(
@ -1245,10 +1330,10 @@ describe('CodexRuntimeHomeService', () => {
expect(readFileSync(getRuntimeCodexAuthPath(), 'utf-8')).toBe(hostAuth)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslLaunchHomePath
)
expect(readFileSync(join(wslManagedHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@ -1270,15 +1355,8 @@ describe('CodexRuntimeHomeService', () => {
const reauthedAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'reauthed', 2_000)
const managedHomePath = createManagedAuth(testState.userDataDir, 'account-1', originalAuth)
const managedAuthPath = join(managedHomePath, 'auth.json')
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const runtimeAuthPath = join(wslRuntimeHomePath, 'auth.json')
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
const runtimeAuthPath = join(wslLaunchHomePath, 'auth.json')
const store = createStore(
createSettings({
codexManagedAccounts: [
@ -1308,7 +1386,7 @@ describe('CodexRuntimeHomeService', () => {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
writeFileSync(runtimeAuthPath, staleRuntimeAuth, 'utf-8')
writeFileSync(managedAuthPath, reauthedAuth, 'utf-8')
@ -1324,7 +1402,69 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('uses the stable WSL runtime home for WSL system-default rate-limit fetches', async () => {
it('reads WSL managed token refreshes from selected launch homes after app restart', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const managedAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'managed-old', 1_000)
const refreshedAuth = createCodexAuthJson(
'wsl@example.com',
'acct-wsl',
'launch-refreshed',
2_000
)
const managedHomePath = createManagedAuth(testState.userDataDir, 'account-1', managedAuth)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
mkdirSync(wslLaunchHomePath, { recursive: true })
writeFileSync(join(wslLaunchHomePath, 'auth.json'), refreshedAuth, 'utf-8')
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'wsl@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-wsl',
workspaceLabel: null,
workspaceAccountId: 'acct-wsl',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'account-1' }
}
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(getWslRuntimeCodexHomePath(wslHome), 'auth.json'), 'utf-8')).toBe(
refreshedAuth
)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('uses the selected WSL launch home for WSL system-default rate-limit fetches', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@ -1344,7 +1484,7 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
getWslLaunchCodexHomePath(wslHome, null)
)
} finally {
if (originalPlatform) {
@ -1365,15 +1505,7 @@ describe('CodexRuntimeHomeService', () => {
const debianAuth = createCodexAuthJson('debian@example.com', 'acct-debian', 'debian-token')
const ubuntuHomePath = createManagedAuth(testState.userDataDir, 'ubuntu-account', ubuntuAuth)
const debianHomePath = createManagedAuth(testState.userDataDir, 'debian-account', debianAuth)
const runtimeAuthPath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home',
'auth.json'
)
const runtimeAuthPath = join(getWslLaunchCodexHomePath(wslHome, 'ubuntu-account'), 'auth.json')
const store = createStore(
createSettings({
codexManagedAccounts: [
@ -1418,7 +1550,7 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: null })).toBe(
join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
getWslLaunchCodexHomePath(wslHome, 'ubuntu-account')
)
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(ubuntuAuth)
} finally {
@ -1428,6 +1560,76 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('preserves WSL system-default refreshes before switching to a managed WSL account', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system-old', 1_000)
const refreshedSystemAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system-refreshed',
2_000
)
const managedAuth = createCodexAuthJson('managed@example.com', 'acct-managed', 'managed', 1_000)
const managedHomePath = createManagedAuth(testState.userDataDir, 'account-1', managedAuth)
const systemCodexHomePath = join(wslHome, '.codex')
mkdirSync(systemCodexHomePath, { recursive: true })
writeFileSync(join(systemCodexHomePath, 'auth.json'), systemAuth, 'utf-8')
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'managed@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-managed',
workspaceLabel: null,
workspaceAccountId: 'acct-managed',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const systemLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
const managedLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
expect(service.prepareForCodexLaunch(target)).toBe(systemLaunchHomePath)
writeFileSync(join(systemLaunchHomePath, 'auth.json'), refreshedSystemAuth, 'utf-8')
store.updateSettings({
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'account-1' } }
})
expect(service.prepareForCodexLaunch(target)).toBe(managedLaunchHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(
refreshedSystemAuth
)
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(managedAuth)
expect(readFileSync(join(managedLaunchHomePath, 'auth.json'), 'utf-8')).toBe(managedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('does not write WSL system-default auth into managed accounts', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
@ -1473,20 +1675,13 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslLaunchHomePath
)
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(managedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(systemDefaultAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(systemDefaultAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@ -1523,21 +1718,14 @@ describe('CodexRuntimeHomeService', () => {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
writeFileSync(join(wslRuntimeHomePath, 'auth.json'), refreshedAuth, 'utf-8')
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
writeFileSync(join(wslLaunchHomePath, 'auth.json'), refreshedAuth, 'utf-8')
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@ -1561,14 +1749,8 @@ describe('CodexRuntimeHomeService', () => {
2_000
)
const systemCodexHomePath = join(wslHome, '.codex')
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const wslRuntimeHomePath = getWslRuntimeCodexHomePath(wslHome)
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
mkdirSync(systemCodexHomePath, { recursive: true })
mkdirSync(wslRuntimeHomePath, { recursive: true })
writeFileSync(join(systemCodexHomePath, 'auth.json'), systemAuth, 'utf-8')
@ -1585,9 +1767,9 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)

View File

@ -25,8 +25,11 @@ import {
} from '../codex/codex-home-paths'
import {
ensureOrcaCodexLaunchHome,
ensureScopedCodexLaunchHome,
materializeOrcaCodexLaunchHome,
removeOrcaCodexLaunchHome
materializeScopedCodexLaunchHome,
removeOrcaCodexLaunchHome,
removeScopedCodexLaunchHome
} from '../codex/codex-launch-home-paths'
import { syncSystemCodexSessionsIntoManagedHome } from '../codex/codex-session-bridge'
import { syncSystemConfigIntoManagedCodexHome } from '../codex/codex-config-mirror'
@ -85,7 +88,7 @@ export class CodexRuntimeHomeService {
// Why: WSL terminals have their own stable runtime homes per distro. They
// cannot share the host baseline or host sync can make stale WSL auth look
// newer than managed storage.
private readonly lastWrittenWslAuthJsonByDistro = new Map<string, string | null>()
private readonly lastWrittenWslAuthJsonBySelection = new Map<string, string | null>()
private readonly lastSyncedWslAccountIdByDistro = new Map<string, string | null>()
private skipNextReadBackForAccountId: string | null = null
@ -315,6 +318,21 @@ export class CodexRuntimeHomeService {
this.lastWrittenHostAuthJsonBySelection.delete(this.getHostLaunchSelectionKey(accountId))
}
removeLaunchHomeForAccount(account: CodexManagedAccount): void {
const distro = this.getWslDistroForAccount(account)
if (distro) {
const launchRootPath = this.getWslLaunchRootPath(distro)
if (launchRootPath) {
removeScopedCodexLaunchHome(launchRootPath, account.id)
}
this.lastWrittenWslAuthJsonBySelection.delete(
this.getWslLaunchSelectionKey(distro, account.id)
)
return
}
this.removeHostLaunchHomeForAccount(account.id)
}
private readBackRefreshedTokens(options: {
updateLastWrittenAuthJson: boolean
}): CodexReadBackResult {
@ -477,29 +495,36 @@ export class CodexRuntimeHomeService {
if (!runtimeHomePath) {
return null
}
const launchRootPath = this.getWslLaunchRootPathFromRuntimeHome(runtimeHomePath)
mkdirSync(runtimeHomePath, { recursive: true })
this.seedWslRuntimeHome(runtimeHomePath, activeAccount, distro)
const runtimeAuthPath = join(runtimeHomePath, 'auth.json')
const previousWslAccountId = this.lastSyncedWslAccountIdByDistro.get(distro) ?? null
if (previousWslAccountId) {
if (this.skipNextReadBackForAccountId === previousWslAccountId) {
const hadPreviousWslSelection = this.lastSyncedWslAccountIdByDistro.has(distro)
const previousWslAccountId = hadPreviousWslSelection
? (this.lastSyncedWslAccountIdByDistro.get(distro) ?? null)
: null
if (activeAccount && hadPreviousWslSelection && previousWslAccountId === null) {
this.preserveWslSystemDefaultRefresh(distro, launchRootPath, runtimeAuthPath)
}
const readBackAccountId =
previousWslAccountId ?? (!hadPreviousWslSelection ? activeAccount?.id : null)
if (readBackAccountId) {
if (this.skipNextReadBackForAccountId === readBackAccountId) {
this.skipNextReadBackForAccountId = null
} else {
const previousWslAccount = this.getActiveAccount(
settings.codexManagedAccounts,
previousWslAccountId
readBackAccountId
)
if (previousWslAccount) {
this.readBackRefreshedTokensFromPath(runtimeAuthPath, {
updateLastWrittenAuthJson: true,
lastWrittenAuthJson: this.lastWrittenWslAuthJsonByDistro.get(distro) ?? null,
setLastWrittenAuthJson: (contents) => {
this.lastWrittenWslAuthJsonByDistro.set(distro, contents)
},
expectedAccountId: previousWslAccount.id
})
this.readBackWslManagedAccountRefresh(
distro,
launchRootPath,
runtimeAuthPath,
previousWslAccount
)
}
}
}
@ -508,9 +533,13 @@ export class CodexRuntimeHomeService {
if (activeAccount && activeAuthPath && existsSync(activeAuthPath)) {
const activeAuth = readFileSync(activeAuthPath, 'utf-8')
this.writeRuntimeAuthAtPath(runtimeAuthPath, activeAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, activeAuth)
this.writeRuntimeAuthAtPath(
this.getWslLaunchAuthPath(launchRootPath, activeAccount.id),
activeAuth
)
this.setLastWrittenWslAuthJson(distro, activeAccount.id, activeAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, activeAccount.id)
return runtimeHomePath
return materializeScopedCodexLaunchHome(runtimeHomePath, launchRootPath, activeAccount.id)
}
if (activeAccount && activeAuthPath) {
console.warn(
@ -529,36 +558,40 @@ export class CodexRuntimeHomeService {
const systemAuthPath = this.getWslSystemCodexAuthPath({ runtime: 'wsl', wslDistro: distro })
if (systemAuthPath && existsSync(systemAuthPath)) {
const systemAuth = readFileSync(systemAuthPath, 'utf-8')
const mirroredSystemDefaultAuth = this.lastWrittenWslAuthJsonByDistro.get(distro) ?? null
const mirroredSystemDefaultAuth = this.getLastWrittenWslAuthJson(distro, null)
const runtimeAuth = existsSync(runtimeAuthPath)
? readFileSync(runtimeAuthPath, 'utf-8')
: null
if (
runtimeAuth !== null &&
runtimeAuth !== systemAuth &&
this.runtimeAuthMatchesSystemDefaultIdentity(runtimeAuth, systemAuth) &&
((mirroredSystemDefaultAuth !== null && systemAuth === mirroredSystemDefaultAuth) ||
(mirroredSystemDefaultAuth === null &&
this.runtimeAuthIsFresher(runtimeAuth, systemAuth)))
) {
// Why: WSL runtime homes are per-distro and their in-memory baseline is
// lost on app restart. A same-identity fresher runtime auth is a Codex
// token refresh and should be copied back before we mirror ~/.codex.
this.writeRuntimeAuthAtPath(systemAuthPath, runtimeAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, runtimeAuth)
const systemLaunchAuthPath = this.getWslLaunchAuthPath(launchRootPath, null)
const launchAuth = existsSync(systemLaunchAuthPath)
? readFileSync(systemLaunchAuthPath, 'utf-8')
: null
const refreshedAuth = this.selectWslSystemDefaultRefreshCandidate({
launchAuth,
runtimeAuth,
systemAuth,
mirroredSystemDefaultAuth
})
if (refreshedAuth) {
this.writeRuntimeAuthAtPath(systemAuthPath, refreshedAuth)
this.writeRuntimeAuthAtPath(runtimeAuthPath, refreshedAuth)
this.writeRuntimeAuthAtPath(systemLaunchAuthPath, refreshedAuth)
this.setLastWrittenWslAuthJson(distro, null, refreshedAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
return materializeScopedCodexLaunchHome(runtimeHomePath, launchRootPath, null)
}
this.writeRuntimeAuthAtPath(runtimeAuthPath, systemAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, systemAuth)
this.writeRuntimeAuthAtPath(systemLaunchAuthPath, systemAuth)
this.setLastWrittenWslAuthJson(distro, null, systemAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
return materializeScopedCodexLaunchHome(runtimeHomePath, launchRootPath, null)
}
rmSync(runtimeAuthPath, { force: true })
this.lastWrittenWslAuthJsonByDistro.set(distro, null)
rmSync(this.getWslLaunchAuthPath(launchRootPath, null), { force: true })
this.setLastWrittenWslAuthJson(distro, null, null)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
return materializeScopedCodexLaunchHome(runtimeHomePath, launchRootPath, null)
}
private getWslRuntimeHomePath(distro: string): string | null {
@ -568,6 +601,133 @@ export class CodexRuntimeHomeService {
: null
}
private getWslLaunchRootPath(distro: string): string | null {
const runtimeHomePath = this.getWslRuntimeHomePath(distro)
return runtimeHomePath ? this.getWslLaunchRootPathFromRuntimeHome(runtimeHomePath) : null
}
private getWslLaunchRootPathFromRuntimeHome(runtimeHomePath: string): string {
return this.joinWslPath(dirname(runtimeHomePath), 'launch', 'wsl')
}
private getWslLaunchAuthPath(launchRootPath: string, accountId: string | null): string {
return join(ensureScopedCodexLaunchHome(launchRootPath, accountId), 'auth.json')
}
private getWslDistroForAccount(account: CodexManagedAccount): string | null {
if (!this.getWslManagedHomePath(account)) {
return null
}
return account.wslDistro ?? parseWslUncPath(account.managedHomePath)?.distro ?? null
}
private getWslLaunchSelectionKey(distro: string, accountId: string | null): string {
return `${distro}\0${accountId ?? 'system'}`
}
private getLastWrittenWslAuthJson(distro: string, accountId: string | null): string | null {
return (
this.lastWrittenWslAuthJsonBySelection.get(
this.getWslLaunchSelectionKey(distro, accountId)
) ?? null
)
}
private setLastWrittenWslAuthJson(
distro: string,
accountId: string | null,
contents: string | null
): void {
this.lastWrittenWslAuthJsonBySelection.set(
this.getWslLaunchSelectionKey(distro, accountId),
contents
)
}
private readBackWslManagedAccountRefresh(
distro: string,
launchRootPath: string,
runtimeAuthPath: string,
account: CodexManagedAccount
): void {
const launchResult = this.readBackRefreshedTokensFromPath(
this.getWslLaunchAuthPath(launchRootPath, account.id),
{
updateLastWrittenAuthJson: true,
lastWrittenAuthJson: this.getLastWrittenWslAuthJson(distro, account.id),
setLastWrittenAuthJson: (contents) => {
this.setLastWrittenWslAuthJson(distro, account.id, contents)
},
expectedAccountId: account.id
}
)
if (launchResult === 'unchanged') {
this.readBackRefreshedTokensFromPath(runtimeAuthPath, {
updateLastWrittenAuthJson: true,
lastWrittenAuthJson: this.getLastWrittenWslAuthJson(distro, account.id),
setLastWrittenAuthJson: (contents) => {
this.setLastWrittenWslAuthJson(distro, account.id, contents)
},
expectedAccountId: account.id
})
}
}
private preserveWslSystemDefaultRefresh(
distro: string,
launchRootPath: string,
runtimeAuthPath: string
): void {
const systemAuthPath = this.getWslSystemCodexAuthPath({ runtime: 'wsl', wslDistro: distro })
if (!systemAuthPath || !existsSync(systemAuthPath)) {
return
}
const systemAuth = readFileSync(systemAuthPath, 'utf-8')
const systemLaunchAuthPath = this.getWslLaunchAuthPath(launchRootPath, null)
const refreshedAuth = this.selectWslSystemDefaultRefreshCandidate({
launchAuth: existsSync(systemLaunchAuthPath)
? readFileSync(systemLaunchAuthPath, 'utf-8')
: null,
runtimeAuth: existsSync(runtimeAuthPath) ? readFileSync(runtimeAuthPath, 'utf-8') : null,
systemAuth,
mirroredSystemDefaultAuth: this.getLastWrittenWslAuthJson(distro, null)
})
if (!refreshedAuth) {
return
}
this.writeRuntimeAuthAtPath(systemAuthPath, refreshedAuth)
this.writeRuntimeAuthAtPath(runtimeAuthPath, refreshedAuth)
this.writeRuntimeAuthAtPath(systemLaunchAuthPath, refreshedAuth)
this.setLastWrittenWslAuthJson(distro, null, refreshedAuth)
}
private selectWslSystemDefaultRefreshCandidate(options: {
launchAuth: string | null
runtimeAuth: string | null
systemAuth: string
mirroredSystemDefaultAuth: string | null
}): string | null {
const candidates = [options.launchAuth, options.runtimeAuth].filter((value): value is string =>
Boolean(value)
)
const refreshedCandidates = candidates.filter((candidate) => {
if (candidate === options.systemAuth) {
return false
}
if (!this.runtimeAuthMatchesSystemDefaultIdentity(candidate, options.systemAuth)) {
return false
}
return options.mirroredSystemDefaultAuth !== null
? options.systemAuth === options.mirroredSystemDefaultAuth
: this.runtimeAuthIsFresher(candidate, options.systemAuth)
})
return (
refreshedCandidates.sort((left, right) =>
this.runtimeAuthIsFresher(right, left) ? 1 : -1
)[0] ?? null
)
}
private joinWslPath(basePath: string, ...segments: string[]): string {
return parseWslUncPath(basePath)
? pathWin32.join(basePath, ...segments)

View File

@ -229,7 +229,7 @@ export class CodexAccountService {
})
this.runtimeHome.syncForCurrentSelection()
this.runtimeHome.removeHostLaunchHomeForAccount?.(accountId)
this.runtimeHome.removeLaunchHomeForAccount?.(account)
this.safeRemoveManagedHome(account.managedHomePath)
// Why: a removed account can no longer appear in the switcher dropdown,
// so purge its cached usage to avoid stale entries.

View File

@ -16,8 +16,10 @@ import {
unlinkSync,
writeFileSync
} from 'node:fs'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { getOrcaManagedCodexHomePath } from './codex-home-paths'
const LAUNCH_HOME_MARKER = '.orca-managed-launch-home'
@ -52,21 +54,53 @@ type LaunchEntryMarker = {
}
export function getOrcaCodexLaunchHomePath(accountId: string | null): string {
const launchHomePath = resolveOrcaCodexLaunchHomePath(accountId, { create: true })
return getScopedCodexLaunchHomePath(getOrcaCodexLaunchHostRootPath(), accountId)
}
export function ensureOrcaCodexLaunchHome(accountId: string | null): string {
return ensureScopedCodexLaunchHome(getOrcaCodexLaunchHostRootPath(), accountId)
}
export function materializeOrcaCodexLaunchHome(accountId: string | null): string {
return materializeScopedCodexLaunchHome(
getOrcaManagedCodexHomePath(),
getOrcaCodexLaunchHostRootPath(),
accountId
)
}
export function removeOrcaCodexLaunchHome(accountId: string): void {
removeScopedCodexLaunchHome(
getOrcaCodexLaunchHostRootPathWithOptions({ create: false }),
accountId
)
}
export function getScopedCodexLaunchHomePath(
launchRootPath: string,
accountId: string | null
): string {
const launchHomePath = resolveCodexLaunchHomePath(launchRootPath, accountId)
mkdirSync(launchHomePath, { recursive: true })
return launchHomePath
}
export function ensureOrcaCodexLaunchHome(accountId: string | null): string {
const launchHomePath = getOrcaCodexLaunchHomePath(accountId)
export function ensureScopedCodexLaunchHome(
launchRootPath: string,
accountId: string | null
): string {
const launchHomePath = getScopedCodexLaunchHomePath(launchRootPath, accountId)
writeLaunchHomeMarker(launchHomePath, accountId)
return launchHomePath
}
export function materializeOrcaCodexLaunchHome(accountId: string | null): string {
reconcileMutableLaunchHomeFilesIntoSharedHome()
const sharedHomePath = getOrcaManagedCodexHomePath()
const launchHomePath = getOrcaCodexLaunchHomePath(accountId)
export function materializeScopedCodexLaunchHome(
sharedHomePath: string,
launchRootPath: string,
accountId: string | null
): string {
reconcileMutableLaunchHomeFilesIntoSharedHome(sharedHomePath, launchRootPath)
const launchHomePath = getScopedCodexLaunchHomePath(launchRootPath, accountId)
writeLaunchHomeMarker(launchHomePath, accountId)
const sharedEntries = new Set<string>()
@ -78,8 +112,8 @@ export function materializeOrcaCodexLaunchHome(accountId: string | null): string
return launchHomePath
}
export function removeOrcaCodexLaunchHome(accountId: string): void {
const launchHomePath = resolveOrcaCodexLaunchHomePath(accountId, { create: false })
export function removeScopedCodexLaunchHome(launchRootPath: string, accountId: string): void {
const launchHomePath = resolveCodexLaunchHomePath(launchRootPath, accountId)
if (!existsSync(launchHomePath)) {
return
}
@ -94,10 +128,13 @@ export function removeOrcaCodexLaunchHome(accountId: string): void {
rmSync(join(launchHomePath, 'auth.json'), { force: true })
return
}
if (!isContainedPath(getOrcaCodexLaunchHostRootPath(), launchHomePath)) {
if (!isContainedPath(launchRootPath, launchHomePath)) {
console.warn('[codex-home] Refusing to remove launch home outside host root:', launchHomePath)
return
}
if (removeWslPathIfPossible(launchHomePath)) {
return
}
rmSync(launchHomePath, { recursive: true, force: true })
}
@ -113,15 +150,8 @@ function getOrcaCodexLaunchHostRootPathWithOptions(options: { create: boolean })
return rootPath
}
function resolveOrcaCodexLaunchHomePath(
accountId: string | null,
options: { create: boolean }
): string {
return join(
getOrcaCodexLaunchHostRootPathWithOptions(options),
getLaunchSelectionSegment(accountId),
'home'
)
function resolveCodexLaunchHomePath(launchRootPath: string, accountId: string | null): string {
return join(launchRootPath, getLaunchSelectionSegment(accountId), 'home')
}
function getLaunchSelectionSegment(accountId: string | null): string {
@ -170,12 +200,7 @@ function linkSharedEntryIntoLaunchHome(
}
try {
const sourceStat = lstatSync(sourcePath)
symlinkSync(
sourcePath,
targetPath,
sourceStat.isDirectory() && process.platform === 'win32' ? 'junction' : undefined
)
createSharedEntryLink(sourcePath, targetPath)
markLaunchEntry(launchHomePath, entryName, sourcePath, 'link')
} catch (error) {
if (!copyFallbackAllowed(sourcePath, entryName)) {
@ -197,6 +222,116 @@ function linkSharedEntryIntoLaunchHome(
}
}
function createSharedEntryLink(sourcePath: string, targetPath: string): void {
if (createWslSymlinkIfPossible(sourcePath, targetPath)) {
return
}
const sourceStat = lstatSync(sourcePath)
symlinkSync(
sourcePath,
targetPath,
sourceStat.isDirectory() && process.platform === 'win32' ? 'junction' : undefined
)
}
function createWslSymlinkIfPossible(sourcePath: string, targetPath: string): boolean {
const paths = getSameDistroWslPaths(sourcePath, targetPath)
if (!paths) {
return false
}
execFileSync(
'wsl.exe',
['-d', paths.distro, '--', 'ln', '-s', paths.sourceLinuxPath, paths.targetLinuxPath],
{
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 5000
}
)
return true
}
function getSameDistroWslPaths(
sourcePath: string,
targetPath: string
): { distro: string; sourceLinuxPath: string; targetLinuxPath: string } | null {
if (process.platform !== 'win32') {
return null
}
const sourceWsl = parseWslUncPath(sourcePath)
const targetWsl = parseWslUncPath(targetPath)
if (!sourceWsl || !targetWsl || sourceWsl.distro !== targetWsl.distro) {
return null
}
return {
distro: sourceWsl.distro,
sourceLinuxPath: sourceWsl.linuxPath,
targetLinuxPath: targetWsl.linuxPath
}
}
function runWslPathCommand(distro: string, args: string[]): boolean {
try {
execFileSync('wsl.exe', ['-d', distro, '--', ...args], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 5000
})
return true
} catch {
return false
}
}
function wslPathExists(targetPath: string): boolean | null {
if (process.platform !== 'win32') {
return null
}
const targetWsl = parseWslUncPath(targetPath)
if (!targetWsl) {
return null
}
return runWslPathCommand(targetWsl.distro, [
'sh',
'-c',
'test -e "$1" || test -L "$1"',
'sh',
targetWsl.linuxPath
])
}
function removeWslPathIfPossible(targetPath: string): boolean {
if (process.platform !== 'win32') {
return false
}
const targetWsl = parseWslUncPath(targetPath)
if (!targetWsl) {
return false
}
return runWslPathCommand(targetWsl.distro, ['rm', '-rf', '--', targetWsl.linuxPath])
}
function readWslSymlinkTarget(targetPath: string): string | null {
if (process.platform !== 'win32') {
return null
}
const targetWsl = parseWslUncPath(targetPath)
if (!targetWsl) {
return null
}
try {
return execFileSync(
'wsl.exe',
['-d', targetWsl.distro, '--', 'readlink', targetWsl.linuxPath],
{
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 5000
}
).trim()
} catch {
return null
}
}
function copyFallbackAllowed(sourcePath: string, entryName: string): boolean {
if (entryName === 'hooks.json') {
return false
@ -215,24 +350,26 @@ function isContainedPath(rootPath: string, candidatePath: string): boolean {
)
}
function reconcileMutableLaunchHomeFilesIntoSharedHome(): void {
const hostRootPath = getOrcaCodexLaunchHostRootPath()
function reconcileMutableLaunchHomeFilesIntoSharedHome(
sharedHomePath: string,
launchRootPath: string
): void {
let selectionEntries: string[]
try {
selectionEntries = readdirSync(hostRootPath)
selectionEntries = readdirSync(launchRootPath)
} catch {
return
}
for (const selectionEntry of selectionEntries.sort()) {
const launchHomePath = join(hostRootPath, selectionEntry, 'home')
const launchHomePath = join(launchRootPath, selectionEntry, 'home')
if (!existsSync(join(launchHomePath, LAUNCH_HOME_MARKER))) {
continue
}
reconcileMarkedMutableFiles(launchHomePath)
reconcileMarkedMutableFiles(sharedHomePath, launchHomePath)
}
}
function reconcileMarkedMutableFiles(launchHomePath: string): void {
function reconcileMarkedMutableFiles(sharedHomePath: string, launchHomePath: string): void {
const markerDir = join(launchHomePath, LAUNCH_HOME_LINK_MARKERS_DIR)
let markerFiles: string[]
try {
@ -246,6 +383,9 @@ function reconcileMarkedMutableFiles(launchHomePath: string): void {
if (!marker || !MUTABLE_SHARED_FILE_ENTRIES.has(entryName)) {
continue
}
if (marker.sourcePath !== join(sharedHomePath, entryName)) {
continue
}
reconcileMutableLaunchEntryIfNeeded(marker.sourcePath, join(launchHomePath, entryName), marker)
}
}
@ -261,6 +401,9 @@ function reconcileMutableLaunchEntryIfNeeded(
if (!targetExistsForLaunchRemoval(targetPath)) {
return
}
if (targetAlreadyPointsToSource(targetPath, sourcePath)) {
return
}
try {
if (lstatSync(targetPath).isSymbolicLink() || !statSync(targetPath).isFile()) {
return
@ -329,6 +472,9 @@ function removeLaunchEntry(targetPath: string): void {
return
}
try {
if (removeWslPathIfPossible(targetPath)) {
return
}
const stat = lstatSync(targetPath)
if (stat.isSymbolicLink()) {
try {
@ -348,6 +494,10 @@ function removeLaunchEntry(targetPath: string): void {
}
function targetExistsForLaunchRemoval(targetPath: string): boolean {
const wslExists = wslPathExists(targetPath)
if (wslExists !== null) {
return wslExists
}
try {
lstatSync(targetPath)
return true
@ -436,6 +586,10 @@ function readLaunchEntryMarker(
}
function targetAlreadyPointsToSource(targetPath: string, sourcePath: string): boolean {
const paths = getSameDistroWslPaths(sourcePath, targetPath)
if (paths) {
return readWslSymlinkTarget(targetPath) === paths.sourceLinuxPath
}
try {
return (
lstatSync(targetPath).isSymbolicLink() &&
@ -447,6 +601,10 @@ function targetAlreadyPointsToSource(targetPath: string, sourcePath: string): bo
}
function linkTargetsMatch(actualTarget: string, expectedTarget: string): boolean {
const expectedWsl = parseWslUncPath(expectedTarget)
if (expectedWsl && actualTarget === expectedWsl.linuxPath) {
return true
}
if (process.platform !== 'win32') {
return actualTarget === expectedTarget
}

View File

@ -2709,6 +2709,50 @@ describe('registerPtyHandlers', () => {
expect(spawnOptions.env.CODEX_HOME).toBeUndefined()
expect(spawnOptions.env.ORCA_CODEX_HOME).toBeUndefined()
})
it('converts selected WSL Codex homes to Linux paths for wsl.exe shells', () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
process.env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe'
isPwshAvailableMock.mockReturnValue(false)
try {
registerPtyHandlers(
mainWindow as never,
undefined,
() =>
'\\\\wsl.localhost\\Ubuntu\\home\\test\\.local\\share\\orca\\codex-runtime-home\\launch\\wsl\\system\\home',
() =>
({
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'powershell.exe'
}) as never
)
handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
shellOverride: 'wsl.exe'
})
const spawnOptions = spawnMock.mock.calls.at(-1)?.[2] as { env: Record<string, string> }
expect(spawnOptions.env.CODEX_HOME).toBe(
'/home/test/.local/share/orca/codex-runtime-home/launch/wsl/system/home'
)
expect(spawnOptions.env.ORCA_CODEX_HOME).toBe(
'/home/test/.local/share/orca/codex-runtime-home/launch/wsl/system/home'
)
expect(spawnOptions.env.WSLENV).toContain('CODEX_HOME')
expect(spawnOptions.env.WSLENV).toContain('ORCA_CODEX_HOME')
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,
value: originalPlatform
})
}
})
})
it('rejects missing WSL worktree cwd instead of validating only the fallback Windows cwd', async () => {