fix(codex): use WSL login shell for account setup (#7993)

This commit is contained in:
Jinwoo Hong 2026-07-09 19:40:11 -07:00 committed by GitHub
parent 27f30c2548
commit f55d753721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 47 deletions

View File

@ -15,6 +15,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PassThrough } from 'node:stream'
import type { GlobalSettings } from '../../shared/types'
import { buildWslCodexAvailabilityArgs, buildWslCodexLoginArgs } from './wsl-codex-command'
const testState = {
userDataDir: '',
@ -766,20 +767,20 @@ describe('CodexAccountService config sync', () => {
if (script.includes('readlink -f')) {
return `${wslLinuxHomePath}\n`
}
if (script.includes('command -v codex')) {
throw new Error('bash -ic does not inherit the distro login-shell PATH')
}
if (args.slice(2, 5).join(' ') === '-- sh -c') {
expect(args).toEqual(buildWslCodexAvailabilityArgs('Debian'))
return ''
}
mkdirSync(wslManagedHomePath, { recursive: true })
writeFileSync(join(wslManagedHomePath, '.orca-managed-home'), 'account-id-for-test\n')
return ''
})
const spawnMock = vi.fn((command: string, args: string[]) => {
expect(command).toBe('wsl.exe')
expect(args).toEqual([
'-d',
'Debian',
'--exec',
'bash',
'-ic',
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
])
expect(args).toEqual(buildWslCodexLoginArgs('Debian', wslLinuxHomePath))
// Why: codex login runs inside WSL, so the rewritten path must be the
// Linux-side ~/.codex, not a Windows UNC path.
expect(readFileSync(join(wslManagedHomePath, 'config.toml'), 'utf-8')).toBe(
@ -876,8 +877,8 @@ describe('CodexAccountService config sync', () => {
if (script.includes('readlink -f')) {
return `${wslLinuxHomePath}\n`
}
if (script.includes('command -v codex')) {
expect(args.slice(0, 5)).toEqual(['-d', 'Debian', '--exec', 'bash', '-ic'])
if (args.slice(2, 5).join(' ') === '-- sh -c') {
expect(args).toEqual(buildWslCodexAvailabilityArgs('Debian'))
throw new Error('codex missing')
}
mkdirSync(wslManagedHomePath, { recursive: true })
@ -960,14 +961,7 @@ describe('CodexAccountService config sync', () => {
})
const spawnMock = vi.fn((command: string, args: string[]) => {
expect(command).toBe('wsl.exe')
expect(args).toEqual([
'-d',
'Ubuntu',
'--exec',
'bash',
'-ic',
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
])
expect(args).toEqual(buildWslCodexLoginArgs('Ubuntu', wslLinuxHomePath))
const child = new EventEmitter() as EventEmitter & {
stdout: PassThrough
stderr: PassThrough
@ -1075,14 +1069,7 @@ describe('CodexAccountService config sync', () => {
})
const spawnMock = vi.fn((command: string, args: string[]) => {
expect(command).toBe('wsl.exe')
expect(args).toEqual([
'-d',
'Ubuntu',
'--exec',
'bash',
'-ic',
`export CODEX_HOME='${wslLinuxHomePath}'; exec codex login`
])
expect(args).toEqual(buildWslCodexLoginArgs('Ubuntu', wslLinuxHomePath))
expect(readFileSync(join(wslManagedHomePath, '.orca-managed-home'), 'utf-8')).toBe(
'account-1\n'
)

View File

@ -22,6 +22,11 @@ import type { RateLimitService } from '../rate-limits/service'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { toWindowsWslPath } from '../wsl'
import { buildEncodedWslBashCommand } from '../wsl-bash-command'
import {
buildWslCodexAvailabilityArgs,
buildWslCodexLoginArgs,
WSL_CODEX_AVAILABILITY_TIMEOUT_MS
} from './wsl-codex-command'
import {
getCodexSelectionTargetForAccount,
getSelectedCodexAccountIdForTarget,
@ -805,15 +810,7 @@ export class CodexAccountService {
const spawnConfig = wslInfo
? {
command: 'wsl.exe',
// Why: nvm and similar WSL installs often initialize PATH from interactive shell config.
args: [
'-d',
wslInfo.distro,
'--exec',
'bash',
'-ic',
`export CODEX_HOME=${shellQuote(wslInfo.linuxPath)}; exec codex login`
],
args: buildWslCodexLoginArgs(wslInfo.distro, wslInfo.linuxPath),
env: process.env,
codexCommand: 'codex'
}
@ -923,18 +920,10 @@ export class CodexAccountService {
private assertWslCodexCliAvailable(wslInfo: { distro: string; linuxPath: string }): void {
try {
execFileSync(
'wsl.exe',
[
'-d',
wslInfo.distro,
'--exec',
'bash',
'-ic',
buildEncodedWslBashCommand('command -v codex >/dev/null 2>&1')
],
{ encoding: 'utf-8', timeout: 5000 }
)
execFileSync('wsl.exe', buildWslCodexAvailabilityArgs(wslInfo.distro), {
encoding: 'utf-8',
timeout: WSL_CODEX_AVAILABILITY_TIMEOUT_MS
})
} catch (error) {
throw new Error(
`Codex CLI is not available in WSL ${wslInfo.distro}. Install Codex in that distro or switch Account location to Windows.`,

View File

@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { buildWslCodexAvailabilityArgs, buildWslCodexLoginArgs } from './wsl-codex-command'
describe('WSL Codex commands', () => {
it('checks the alias-neutral PATH from the distro login shell', () => {
const args = buildWslCodexAvailabilityArgs('Ubuntu24-Dev')
expect(args.slice(0, 5)).toEqual(['-d', 'Ubuntu24-Dev', '--', 'sh', '-c'])
expect(args.at(-1)).toContain('getent passwd')
expect(args.at(-1)).toContain('_orca_lookup_command=')
expect(args.at(-1)).toContain('codex')
expect(args.at(-1)).not.toContain('bash -ic')
})
it('launches the resolved Codex executable with its quoted managed home', () => {
const args = buildWslCodexLoginArgs('Ubuntu', '/home/alice/managed-home')
const command = args.at(-1)
expect(command).toContain('export CODEX_HOME=')
expect(command).toContain('/home/alice/managed-home')
expect(command).toContain('exec "\\$resolved" login')
})
})

View File

@ -0,0 +1,43 @@
import { buildPosixCommandPathLookupScript } from '../../shared/posix-command-path-lookup'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows,
quotePosixShell
} from '../../shared/wsl-login-shell-command'
export const WSL_CODEX_AVAILABILITY_TIMEOUT_MS = 5_000
export function buildWslCodexAvailabilityArgs(distro: string): string[] {
const command = [buildCodexPathLookup(), '[ -n "$resolved" ]'].join('\n')
return buildWslCodexShellArgs(distro, command)
}
export function buildWslCodexLoginArgs(distro: string, linuxHomePath: string): string[] {
const command = [
buildCodexPathLookup(),
'if [ -z "$resolved" ]; then',
" printf '%s\\n' 'Codex CLI not found in the WSL login-shell PATH.' >&2",
' exit 127',
'fi',
`export CODEX_HOME=${quotePosixShell(linuxHomePath)}`,
'exec "$resolved" login'
].join('\n')
return buildWslCodexShellArgs(distro, command)
}
function buildCodexPathLookup(): string {
return buildPosixCommandPathLookupScript({ kind: 'literal', value: 'codex' })
}
function buildWslCodexShellArgs(distro: string, command: string): string[] {
// Why: Codex must use the distro user's configured login shell, whose PATH
// can differ from a hard-coded non-login bash invocation.
return [
'-d',
distro,
'--',
'sh',
'-c',
escapeWslShCommandForWindows(buildWslLoginShellCommand(command))
]
}