Files
orca/src/shared/posix-command-path-lookup.test.ts
T
BingZandJinwoo Hong 5b1b8cc61f fix(preflight): resolve WSL/SSH agent paths past shell aliases (#7867)
* fix(preflight): resolve WSL/SSH agent paths past shell aliases

LeanCTX and similar tools wrap claude/codex as interactive shell aliases.
command -v then returns alias text, which fails absolute-path detection and
hides installed agents (#7816).

Prefer bash type -P, then zsh type -p, then command -v for dash/sh fallback
in WSL agent discovery, WSL isCommandOnPath, and remote relay probes.

* fix(preflight): harden alias-safe PATH lookup chain

Require non-empty results between type -P, type -p, and command -v so bash
type -p empty success cannot skip later lookups.

* fix(preflight): resolve agent executables directly from PATH

---------

Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
2026-07-09 17:08:35 -07:00

313 lines
10 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import {
accessSync,
chmodSync,
constants,
existsSync,
mkdtempSync,
mkdirSync,
realpathSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, delimiter, dirname, isAbsolute, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { buildPosixCommandPathLookupScript } from './posix-command-path-lookup'
import { buildWslLoginShellCommand, escapeWslShCommandForWindows } from './wsl-login-shell-command'
type ShellCase = {
name: string
path: string | null
}
const isWindows = process.platform === 'win32'
const WSL_TEST_COMMAND_TIMEOUT_MS = 10_000
let wslShAvailable: boolean | null = null
const shellCases: ShellCase[] = [
{ name: 'sh', path: executablePath(['/bin/sh']) },
{ name: 'bash', path: executablePath(['/bin/bash', '/usr/bin/bash']) },
{ name: 'zsh', path: executablePath(['/bin/zsh', '/usr/bin/zsh']) },
{ name: 'dash', path: executablePath(['/bin/dash', '/usr/bin/dash']) }
]
describe('buildPosixCommandPathLookupScript', () => {
for (const shell of shellCases) {
it.skipIf(isWindows || shell.path === null)(
`resolves without mutating alias and function masks in ${shell.name}`,
() => {
const commandName = basename(process.execPath)
const script = [
`${commandName}() { printf '%s\\n' masked-function; }`,
`alias ${commandName}='printf "%s\\n" masked-alias'`,
buildPosixCommandPathLookupScript({ kind: 'literal', value: commandName }),
`printf '%s\\n' "$resolved"`,
`alias ${commandName} >/dev/null`,
`unalias ${commandName}`,
`${commandName}`
].join('\n')
const resolved = execFileSync(shell.path!, ['-c', script], {
encoding: 'utf8',
env: {
...process.env,
PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? ''}`
}
})
.trim()
.split('\n')
expect(isAbsolute(resolved[0])).toBe(true)
expect(realpathSync(resolved[0])).toBe(realpathSync(process.execPath))
expect(resolved[1]).toBe('masked-function')
}
)
}
it.skipIf(isWindows || executablePath(['/bin/sh']) === null)(
'resolves a command held in a validated shell variable',
() => {
const commandName = basename(process.execPath)
const script = [
`cmd='${commandName}'`,
`${commandName}() { printf '%s\\n' masked-function; }`,
`alias ${commandName}='printf "%s\\n" masked-alias'`,
buildPosixCommandPathLookupScript({ kind: 'shell-variable', name: 'cmd' }),
`printf '%s\\n' "$resolved"`
].join('\n')
const resolved = execFileSync('/bin/sh', ['-c', script], {
encoding: 'utf8',
env: {
...process.env,
PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? ''}`
}
}).trim()
expect(isAbsolute(resolved)).toBe(true)
expect(realpathSync(resolved)).toBe(realpathSync(process.execPath))
}
)
it.skipIf(isWindows || executablePath(['/bin/bash', '/usr/bin/bash']) === null)(
'resolves past a readonly bash function mask',
() => {
const commandName = basename(process.execPath)
const script = [
`${commandName}() { printf '%s\\n' masked-function; }`,
`readonly -f ${commandName}`,
buildPosixCommandPathLookupScript({ kind: 'literal', value: commandName }),
`printf '%s\\n' "$resolved"`
].join('\n')
const resolved = execFileSync(
executablePath(['/bin/bash', '/usr/bin/bash'])!,
['-c', script],
{
encoding: 'utf8',
env: {
...process.env,
PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? ''}`
}
}
).trim()
expect(realpathSync(resolved)).toBe(realpathSync(process.execPath))
}
)
it.skipIf(isWindows || executablePath(['/bin/sh']) === null)(
'prefers the first external executable even when its name is a shell builtin',
() => {
withExecutableFixture('printf', (directory, executable, root) => {
const secondDirectory = join(root, 'second-bin')
const secondExecutable = join(secondDirectory, 'printf')
mkdirSync(secondDirectory)
writeFileSync(secondExecutable, '#!/bin/sh\nexit 0\n')
chmodSync(secondExecutable, 0o755)
const script = [
buildPosixCommandPathLookupScript({ kind: 'literal', value: 'printf' }),
`printf '%s\\n' "$resolved"`
].join('\n')
const resolved = execFileSync('/bin/sh', ['-c', script], {
encoding: 'utf8',
env: { ...process.env, PATH: `${directory}:${secondDirectory}` }
}).trim()
expectResolvedExecutable(resolved, executable)
})
}
)
it.skipIf(isWindows || executablePath(['/bin/sh']) === null)(
'makes relative and trailing-empty PATH matches absolute',
() => {
withExecutableFixture('relative-agent', (directory, executable, root) => {
const relativeDirectory = basename(directory)
const script = [
buildPosixCommandPathLookupScript({ kind: 'literal', value: 'relative-agent' }),
`printf '%s\\n' "$resolved"`
].join('\n')
const relativeResolved = execFileSync('/bin/sh', ['-c', script], {
cwd: root,
encoding: 'utf8',
env: { ...process.env, PATH: `${relativeDirectory}:` }
}).trim()
const trailingResolved = execFileSync('/bin/sh', ['-c', script], {
cwd: directory,
encoding: 'utf8',
env: { ...process.env, PATH: '/missing:' }
}).trim()
const emptyResolved = execFileSync('/bin/sh', ['-c', script], {
cwd: directory,
encoding: 'utf8',
env: { ...process.env, PATH: ':/missing' }
}).trim()
expectResolvedExecutable(relativeResolved, executable)
expectResolvedExecutable(trailingResolved, executable)
expectResolvedExecutable(emptyResolved, executable)
})
}
)
it.skipIf(isWindows || executablePath(['/bin/sh']) === null)(
'handles leading-dash names and explicit relative paths',
() => {
withExecutableFixture('-agent', (directory, executable) => {
const script = [
buildPosixCommandPathLookupScript({ kind: 'shell-variable', name: 'cmd' }),
`printf '%s\\n' "$resolved"`,
buildPosixCommandPathLookupScript({ kind: 'literal', value: './-agent' }),
`printf '%s\\n' "$resolved"`
].join('\n')
const output = execFileSync('/bin/sh', ['-c', script], {
cwd: directory,
encoding: 'utf8',
env: { ...process.env, PATH: directory, cmd: '-agent' }
})
.trim()
.split('\n')
expectResolvedExecutable(output[0], executable)
expectResolvedExecutable(output[1], executable)
})
}
)
it.skipIf(isWindows || executablePath(['/bin/sh']) === null)(
'keeps set -e callers running when the command is absent',
() => {
const script = [
'set -e',
buildPosixCommandPathLookupScript({
kind: 'literal',
value: '__orca_missing_command_path_lookup__'
}),
`printf '%s\\n' survived`
].join('\n')
expect(execFileSync('/bin/sh', ['-c', script], { encoding: 'utf8' }).trim()).toBe('survived')
}
)
it.each(['', '$cmd', 'cmd-name', 'cmd; echo injected'])(
'rejects an unsafe shell variable name: %s',
(name) => {
expect(() => buildPosixCommandPathLookupScript({ kind: 'shell-variable', name })).toThrow(
'Invalid shell variable name'
)
}
)
it('quotes literal targets before assigning them in the generated shell fragment', () => {
const script = buildPosixCommandPathLookupScript({
kind: 'literal',
value: "agent'; echo injected; '"
})
expect(script).toContain(`_orca_lookup_command='agent'\\''; echo injected; '\\'''`)
})
it.skipIf(!canRunWslSh())(
'resolves through the Windows-to-WSL login-shell boundary with inline masks',
() => {
const lookup = buildPosixCommandPathLookupScript({ kind: 'literal', value: 'sh' })
const command = buildWslLoginShellCommand(
[
`sh() { printf '%s\\n' masked-function; }`,
`alias sh='printf masked-alias'`,
lookup,
`printf '%s' "$resolved"`
].join('\n')
)
const resolved = execFileSync(
'wsl.exe',
['--', 'sh', '-lc', escapeWslShCommandForWindows(command)],
{ encoding: 'utf8', timeout: WSL_TEST_COMMAND_TIMEOUT_MS }
).trim()
expect(resolved).toMatch(/^\/.+\/sh$/)
},
30_000
)
})
function expectResolvedExecutable(resolved: string, executable: string): void {
expect(isAbsolute(resolved)).toBe(true)
expect(realpathSync(resolved)).toBe(realpathSync(executable))
}
function canRunWslSh(): boolean {
if (!isWindows) {
return false
}
if (wslShAvailable !== null) {
return wslShAvailable
}
try {
execFileSync('wsl.exe', ['--', 'sh', '-lc', 'true'], {
timeout: WSL_TEST_COMMAND_TIMEOUT_MS
})
wslShAvailable = true
} catch {
wslShAvailable = false
}
return wslShAvailable
}
function withExecutableFixture(
name: string,
run: (directory: string, executable: string, root: string) => void
): void {
const root = mkdtempSync(join(tmpdir(), 'orca-path-lookup-'))
const directory = join(root, 'bin')
const executable = join(directory, name)
try {
mkdirSync(directory)
writeFileSync(executable, '#!/bin/sh\nexit 0\n')
chmodSync(executable, 0o755)
run(directory, executable, root)
} finally {
rmSync(root, { force: true, recursive: true })
}
}
function executablePath(candidates: readonly string[]): string | null {
if (isWindows) {
return null
}
for (const candidate of candidates) {
if (!existsSync(candidate)) {
continue
}
try {
accessSync(candidate, constants.X_OK)
return candidate
} catch {
// Keep checking alternate standard locations when this entry is not executable.
}
}
return null
}