fix(node-host): enforce system.run rawCommand/argv consistency

This commit is contained in:
Peter Steinberger
2026-02-14 18:52:13 +01:00
parent 71f357d949
commit cb3290fca3
6 changed files with 241 additions and 23 deletions

View File

@@ -0,0 +1,54 @@
import { describe, expect, test } from "vitest";
import {
extractShellCommandFromArgv,
formatExecCommand,
validateSystemRunCommandConsistency,
} from "./system-run-command.js";
describe("system run command helpers", () => {
test("formatExecCommand quotes args with spaces", () => {
expect(formatExecCommand(["echo", "hi there"])).toBe('echo "hi there"');
});
test("extractShellCommandFromArgv extracts sh -lc command", () => {
expect(extractShellCommandFromArgv(["/bin/sh", "-lc", "echo hi"])).toBe("echo hi");
});
test("extractShellCommandFromArgv extracts cmd.exe /c command", () => {
expect(extractShellCommandFromArgv(["cmd.exe", "/d", "/s", "/c", "echo hi"])).toBe("echo hi");
});
test("validateSystemRunCommandConsistency accepts rawCommand matching direct argv", () => {
const res = validateSystemRunCommandConsistency({
argv: ["echo", "hi"],
rawCommand: "echo hi",
});
expect(res.ok).toBe(true);
if (!res.ok) {
throw new Error("unreachable");
}
expect(res.shellCommand).toBe(null);
expect(res.cmdText).toBe("echo hi");
});
test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs direct argv", () => {
const res = validateSystemRunCommandConsistency({
argv: ["uname", "-a"],
rawCommand: "echo hi",
});
expect(res.ok).toBe(false);
if (res.ok) {
throw new Error("unreachable");
}
expect(res.message).toContain("rawCommand does not match command");
expect(res.details?.code).toBe("RAW_COMMAND_MISMATCH");
});
test("validateSystemRunCommandConsistency accepts rawCommand matching sh wrapper argv", () => {
const res = validateSystemRunCommandConsistency({
argv: ["/bin/sh", "-lc", "echo hi"],
rawCommand: "echo hi",
});
expect(res.ok).toBe(true);
});
});

View File

@@ -0,0 +1,106 @@
import path from "node:path";
export type SystemRunCommandValidation =
| {
ok: true;
shellCommand: string | null;
cmdText: string;
}
| {
ok: false;
message: string;
details?: Record<string, unknown>;
};
function basenameLower(token: string): string {
const win = path.win32.basename(token);
const posix = path.posix.basename(token);
const base = win.length < posix.length ? win : posix;
return base.trim().toLowerCase();
}
export function formatExecCommand(argv: string[]): string {
return argv
.map((arg) => {
const trimmed = arg.trim();
if (!trimmed) {
return '""';
}
const needsQuotes = /\s|"/.test(trimmed);
if (!needsQuotes) {
return trimmed;
}
return `"${trimmed.replace(/"/g, '\\"')}"`;
})
.join(" ");
}
export function extractShellCommandFromArgv(argv: string[]): string | null {
const token0 = argv[0]?.trim();
if (!token0) {
return null;
}
const base0 = basenameLower(token0);
// POSIX-style shells: sh -lc "<cmd>"
if (
base0 === "sh" ||
base0 === "bash" ||
base0 === "zsh" ||
base0 === "dash" ||
base0 === "ksh"
) {
const flag = argv[1]?.trim();
if (flag !== "-lc" && flag !== "-c") {
return null;
}
const cmd = argv[2];
return typeof cmd === "string" ? cmd : null;
}
// Windows cmd.exe: cmd.exe /d /s /c "<cmd>"
if (base0 === "cmd.exe" || base0 === "cmd") {
const idx = argv.findIndex((item) => String(item).trim().toLowerCase() === "/c");
if (idx === -1) {
return null;
}
const cmd = argv[idx + 1];
return typeof cmd === "string" ? cmd : null;
}
return null;
}
export function validateSystemRunCommandConsistency(params: {
argv: string[];
rawCommand?: string | null;
}): SystemRunCommandValidation {
const raw =
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
? params.rawCommand.trim()
: null;
const shellCommand = extractShellCommandFromArgv(params.argv);
const inferred = shellCommand ? shellCommand.trim() : formatExecCommand(params.argv);
if (raw && raw !== inferred) {
return {
ok: false,
message: "INVALID_REQUEST: rawCommand does not match command",
details: {
code: "RAW_COMMAND_MISMATCH",
rawCommand: raw,
inferred,
},
};
}
return {
ok: true,
// Only treat this as a shell command when argv is a recognized shell wrapper.
// For direct argv execution, rawCommand is purely display/approval text and
// must match the formatted argv.
shellCommand: shellCommand ? (raw ?? shellCommand) : null,
cmdText: raw ?? shellCommand ?? inferred,
};
}