Support WSL Codex launch homes (#4318)
This commit is contained in:
parent
34c6aa4bc2
commit
c05cc3150a
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue