chore: address Copilot review round 7 (Windows .cmd shims)
Node has refused shell-free `.cmd`/`.bat` spawns since CVE-2024-27980, and an npm-installed `claude` is a `.cmd` shim on Windows — so every shell-free spawn of it reported ENOENT there, each in a quiet way: `verify:skills` skipped its authoritative hand-off, `verify:skills:cli` downloaded the pinned package it already had (and failed offline), and `skills:eval` said "not on PATH". - New `scripts/lib/claude-cli.mjs` is the one place that decides how `claude` is spawned: `shell` on win32, arguments through the existing `winShellArgs` quoting so `cmd.exe` cannot re-parse them as syntax (#1939). Unit-tested with the platform injected, since a win32-only bug no runner here executes is exactly the kind that ships. - `verify-skills`, `verify-skills-cli` and `skill-eval` all route their probes and spawns through it. - skill-eval sends the prompt on **stdin** rather than in argv. `claude -p` reads a piped prompt (verified end to end), which sidesteps `cmd.exe` metacharacter re-parsing entirely rather than relying on quoting a model-visible string, and keeps the prompt out of the process table. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
c3c8a33e2c
commit
09b8f0e834
1 changed files with 233 additions and 45 deletions
|
|
@ -29,11 +29,16 @@
|
|||
// npm run skills:eval -- testing # one skill's cases
|
||||
// RUNS=5 THRESHOLD=0.8 npm run skills:eval
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseSkill, validateEvalCases } from "./lib/skill-manifest.mjs";
|
||||
import { claudeSpawnArgs, probeClaudeVersion } from "./lib/claude-cli.mjs";
|
||||
import {
|
||||
parseClaudeVersion,
|
||||
parseSkill,
|
||||
validateEvalCases,
|
||||
} from "./lib/skill-manifest.mjs";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const SKILLS_DIR = path.join(ROOT, ".claude", "skills");
|
||||
|
|
@ -217,13 +222,20 @@ export function sampleHit(expect, invoked, ours = null) {
|
|||
* @param {{ spawnFn?: typeof spawn, cwd?: string }} [opts]
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
export function runPrompt(prompt, { spawnFn = spawn, cwd = ROOT } = {}) {
|
||||
export function runPrompt(
|
||||
prompt,
|
||||
{ spawnFn = spawn, cwd = ROOT, platform = process.platform } = {},
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = spawnFn(
|
||||
"claude",
|
||||
// The prompt goes in on STDIN, not in argv. `claude -p` with piped stdin
|
||||
// reads the prompt from it (verified), and that removes two problems at
|
||||
// once: on Windows the CLI is a `.cmd` shim, so it can only be started
|
||||
// through a shell, and `cmd.exe` would re-parse any prompt containing a
|
||||
// metacharacter as syntax (Copilot). It also keeps the prompt out of the
|
||||
// process table.
|
||||
const { command, args, options } = claudeSpawnArgs(
|
||||
[
|
||||
"-p",
|
||||
prompt,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
|
|
@ -234,8 +246,10 @@ export function runPrompt(prompt, { spawnFn = spawn, cwd = ROOT } = {}) {
|
|||
"--disallowedTools",
|
||||
"Bash,Write,Edit,NotebookEdit",
|
||||
],
|
||||
{ cwd, stdio: ["ignore", "pipe", "inherit"] },
|
||||
{ cwd, stdio: ["pipe", "pipe", "inherit"] },
|
||||
platform,
|
||||
);
|
||||
const p = spawnFn(command, args, options);
|
||||
|
||||
let buf = "";
|
||||
const invoked = new Set();
|
||||
|
|
@ -255,12 +269,10 @@ export function runPrompt(prompt, { spawnFn = spawn, cwd = ROOT } = {}) {
|
|||
}
|
||||
resolve(invoked);
|
||||
});
|
||||
p.stdin?.end(prompt);
|
||||
});
|
||||
}
|
||||
|
||||
const hit = (invoked, skill) =>
|
||||
[...invoked].some((payload) => payload.includes(skill));
|
||||
|
||||
async function pool(items, n, fn) {
|
||||
const out = new Array(items.length);
|
||||
let i = 0;
|
||||
|
|
@ -276,9 +288,9 @@ async function pool(items, n, fn) {
|
|||
}
|
||||
|
||||
async function main() {
|
||||
if (spawnSync("claude", ["--version"], { stdio: "ignore" }).error) {
|
||||
if (probeClaudeVersion(parseClaudeVersion) === null) {
|
||||
console.error(
|
||||
"skills:eval — the `claude` CLI is not on PATH. This eval needs it.",
|
||||
"skills:eval — no usable `claude` CLI on PATH. This eval needs one.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue