fix(coding-agent): prevented non-node Windows cmd shims from being launched as node

- Updated the Windows npm shim resolver to resolve shim files against `cwd` and inspect `_prog` before treating a batch wrapper as a node launcher.
- Added a node-only guard so non-node wrappers, such as python shims, fall back to standard cmd.exe execution.
- Adjusted mcp stdio tests with a new non-node shim fixture and a simplified notify race case to validate transport teardown behavior.
This commit is contained in:
can1357
2026-06-12 10:01:55 +02:00
parent 21be63e97d
commit 1ceae07764
3 changed files with 110 additions and 50 deletions
@@ -128,27 +128,51 @@ function extractWindowsNpmShimTarget(content: string): string | null {
return match?.[1] ?? null;
}
/**
* Extract the shim's PATH-fallback interpreter (`SET "_prog=node"`). The
* `IF EXIST` branch assigns a `%dp0%`-prefixed value, so requiring a
* non-`%`-leading value picks the bare program name.
*/
function extractWindowsNpmShimProg(content: string): string | null {
const match = /SET\s+"_prog=([^%"][^"]*)"/i.exec(content);
return match?.[1] ?? null;
}
async function resolveWindowsNpmShimCommand(
command: string,
args: readonly string[],
cwd: string,
): Promise<StdioSpawnCommand | null> {
if (!isWindowsBatchCommand(command)) return null;
if (!hasPathSegment(command) && !path.isAbsolute(command)) return null;
if (!hasPathSegment(command)) return null;
const commandPath = path.resolve(cwd, command);
let content: string;
try {
content = await Bun.file(command).text();
content = await Bun.file(commandPath).text();
} catch {
return null;
}
// cmd-shim emits the same invocation line for every interpreter; only
// bypass cmd.exe when the shim's fallback interpreter is actually node.
const prog = extractWindowsNpmShimProg(content);
if (
!prog ||
path
.basename(prog)
.replace(/\.exe$/i, "")
.toLowerCase() !== "node"
)
return null;
const rawTarget = extractWindowsNpmShimTarget(content);
if (!rawTarget) return null;
const target = resolveWindowsShimPath(rawTarget, path.dirname(command));
const target = resolveWindowsShimPath(rawTarget, path.dirname(commandPath));
if (!target) return null;
const siblingNode = path.join(path.dirname(command), "node.exe");
const siblingNode = path.join(path.dirname(commandPath), "node.exe");
const nodeCommand = (await fileExists(siblingNode)) ? siblingNode : "node";
return {
cmd: [nodeCommand, target, ...args],
@@ -198,7 +222,7 @@ export async function resolveStdioSpawnCommand(
const resolvedCommand =
(await resolveWindowsCommandPath(config.command, options.cwd, options.env)) ?? config.command;
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args);
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args, options.cwd);
if (npmShimCommand) return npmShimCommand;
if (!isWindowsBatchCommand(resolvedCommand)) return { cmd: [resolvedCommand, ...args] };