From f54448bfb7e31c0fe89041aab8d904b6ec5ea66c Mon Sep 17 00:00:00 2001 From: Chen Date: Wed, 28 Oct 2026 21:48:10 +0800 Subject: [PATCH] Limit literal PATH probes to agent detection --- src/main/ipc/command-path-resolver.test.ts | 10 +--------- src/main/ipc/command-path-resolver.ts | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/ipc/command-path-resolver.test.ts b/src/main/ipc/command-path-resolver.test.ts index d81aa45e4cf..44bc2ef03de 100644 --- a/src/main/ipc/command-path-resolver.test.ts +++ b/src/main/ipc/command-path-resolver.test.ts @@ -80,7 +80,6 @@ describe('isCommandOnLocalPath', () => { dir = await mkdtemp(path.join(tmpdir(), 'cmd-resolver-win32-')) // Match PATHEXT case in fixtures; real Windows handles filename case while Linux test filesystems do not. await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n') - await writeFile(path.join(dir, 'literal-cli'), '') }) afterAll(async () => { await rm(dir, { recursive: true, force: true }) @@ -113,14 +112,6 @@ describe('isCommandOnLocalPath', () => { ).resolves.toBe(true) }) - it('matches an extensionless executable alongside PATHEXT candidates', async () => { - await expect( - isCommandOnLocalPath('literal-cli', { - platform: 'win32', - env: { Path: dir, PATHEXT: '.EXE' } - }) - ).resolves.toBe(true) - }) }) // Executable-but-broken shims can shadow working binaries; retain later PATH candidates (#22975). @@ -223,6 +214,7 @@ describe('isCommandOnLocalPath', () => { it('lists every PATHEXT permutation on win32, in resolver order', async () => { const dir = await mkdtemp(path.join(tmpdir(), 'cmd-list-win32-')) try { + await writeFile(path.join(dir, 'tool'), '') await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n') await writeFile(path.join(dir, 'tool.EXE'), '') await expect( diff --git a/src/main/ipc/command-path-resolver.ts b/src/main/ipc/command-path-resolver.ts index 897fdddaf72..08d983c7902 100644 --- a/src/main/ipc/command-path-resolver.ts +++ b/src/main/ipc/command-path-resolver.ts @@ -29,14 +29,22 @@ function readEnvCaseInsensitive(env: NodeJS.ProcessEnv, key: string): string | u return undefined } -function getWindowsExtensions(env: NodeJS.ProcessEnv): string[] { +function getWindowsPathSearchExtensions(env: NodeJS.ProcessEnv, command: string): string[] { const pathext = readEnvCaseInsensitive(env, 'PATHEXT') ?? '.EXE;.CMD;.BAT;.COM' const extensions = pathext.split(';').filter((ext) => ext.length > 0) - // CLI fallback resolution also checks a literal executable name, even without an extension. - extensions.unshift('') + // Explicit extensions also permit exact-name matches alongside PATHEXT variants. + if (command.includes('.')) { + extensions.unshift('') + } return extensions } +/** Agent CLI fallback also checks a literal executable name without an extension. */ +function getWindowsBatchDetectionExtensions(env: NodeJS.ProcessEnv, command: string): string[] { + const extensions = getWindowsPathSearchExtensions(env, command) + return extensions.includes('') ? extensions : ['', ...extensions] +} + type LocalCommandSelection = { scope: string selected?: { binary: string; stamp: string; cwd?: string } @@ -169,8 +177,8 @@ export async function isCommandOnLocalPath( /** Batched PATH lookup for detection: the subset of `commands` present on PATH. * Reads each absolute PATH directory once instead of stat()ing every command-extension - * candidate (#34636); hits still go through `isExecutableFile`, so symlink, directory, - * permission and case semantics match `isCommandOnLocalPath`. */ + * candidate (#34636); hits still go through `isExecutableFile`, preserving symlink, directory, + * permission and case semantics. It also matches literal Windows CLI names used by agent fallback. */ export async function findCommandsOnLocalPath( commands: readonly string[], options: ResolveCommandOptions = {} @@ -231,7 +239,7 @@ export async function findCommandsOnLocalPath( ) for (const command of bareCommands) { - const extensions = isWin ? getWindowsExtensions(env) : [''] + const extensions = isWin ? getWindowsBatchDetectionExtensions(env, command) : [''] // Deduplicate candidates per command: two commands (for example `tool` and // `tool.CMD`) may resolve to the same on-disk file, and each must still be found. const seenCandidates = new Set() @@ -307,7 +315,7 @@ async function findLocalCommandPaths( const pathDirs = (readEnvCaseInsensitive(env, 'PATH') ?? '').split(delimiter) // Separators bypass PATH; Windows searches cwd first. const searchDirs = hasPathSeparator ? [''] : isWin ? [cwd, ...pathDirs] : pathDirs - const extensions = isWin ? getWindowsExtensions(env) : [''] + const extensions = isWin ? getWindowsPathSearchExtensions(env, command) : [''] const found: string[] = [] const seen = new Set()