fix(verify): keep Windows host variables in isolated client verifiers (#94)

The Qwen, Gemini, Command Code and Harness client verifiers built an isolated environment with only PATH and their own home variables. On Windows a child Node process without SystemRoot aborts with `Assertion failed: ncrypto::CSPRNG(nullptr, 0)`, and npm .cmd shims also need ComSpec and PATHEXT. Keep those four host variables on Windows only, and test the selection, its use in each verifier, and a child Node start on Windows.
This commit is contained in:
Roel authored and GitHub committed 2026-11-03 06:34:42 +01:00
1 parent 8136e8dfb7
commit de1c0c8e5d
7 files changed
+106

No files matched your search

@@ -0,0 +1,5 @@
/** Host variables to keep in an isolated verifier environment. */
export function verifierHostEnvironment(
source?: Readonly<Record<string, string | undefined>>,
platform?: NodeJS.Platform,
): Record<string, string>;
+25
View File
@@ -0,0 +1,25 @@
/**
* Variables a Windows child process needs from the host even when a verifier
* isolates everything else. Without SystemRoot, Node aborts at startup with
* `Assertion failed: ncrypto::CSPRNG(nullptr, 0)`; without ComSpec and PATHEXT
* npm `.cmd` shims such as `qwen.cmd` cannot be resolved or run.
*/
const WINDOWS_HOST_VARIABLES = ["SystemRoot", "WINDIR", "ComSpec", "PATHEXT"];
/** Host variables to keep in an isolated verifier environment. */
export const verifierHostEnvironment = (
source = process.env,
platform = process.platform,
) => {
if (platform !== "win32") return {};
const kept = {};
for (const name of WINDOWS_HOST_VARIABLES) {
// Windows environment names are case-insensitive; Node keeps the host spelling.
const key = Object.keys(source).find(
(candidate) => candidate.toUpperCase() === name.toUpperCase(),
);
if (key !== undefined && source[key] !== undefined)
kept[name] = source[key];
}
return kept;
};
+2
View File
@@ -11,6 +11,7 @@ import {
} from "../dist/contracts/toolContracts.js";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { verifierHostEnvironment } from "./lib/verifier-host-environment.mjs";
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
@@ -85,6 +86,7 @@ await withVerifierCleanup(async () => {
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
);
const environment = {
...verifierHostEnvironment(),
PATH: process.env.PATH,
NODE_OPTIONS: process.env.NODE_OPTIONS,
USERPROFILE: account,
+2
View File
@@ -13,6 +13,7 @@ import {
import { pathExists } from "./lib/verify-package-core.mjs";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { verifierHostEnvironment } from "./lib/verifier-host-environment.mjs";
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
@@ -61,6 +62,7 @@ await withVerifierCleanup(async () => {
mkdir(target, { recursive: true }),
]);
const environment = {
...verifierHostEnvironment(),
PATH: process.env.PATH ?? "",
HOME: account,
USERPROFILE: account,
+2
View File
@@ -17,6 +17,7 @@ import {
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { verifierHostEnvironment } from "./lib/verifier-host-environment.mjs";
import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createGeminiModelFixture } from "./verify/clients/gemini-model-fixture.mjs";
@@ -78,6 +79,7 @@ await withVerifierCleanup(async () => {
await writeFile(join(lab, "system-settings.json"), "{}\n");
await writeFile(join(lab, "system-defaults.json"), "{}\n");
const environment = {
...verifierHostEnvironment(),
PATH: process.env.PATH ?? "",
USERPROFILE: account,
GEMINI_CLI_HOME: profile,
+2
View File
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { verifierHostEnvironment } from "./lib/verifier-host-environment.mjs";
import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import {
@@ -66,6 +67,7 @@ await withVerifierCleanup(async () => {
mkdir(workspace),
]);
const baseEnvironment = {
...verifierHostEnvironment(),
PATH: process.env.PATH ?? "",
HOME: account,
USERPROFILE: account,
@@ -0,0 +1,68 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { verifierHostEnvironment } from "../../../scripts/lib/verifier-host-environment.mjs";
const execute = promisify(execFile);
const project = fileURLToPath(new URL("../../../", import.meta.url));
describe("isolated verifier environments", () => {
it("keep the Windows host variables a child Node process needs", () => {
// A Windows child started without SystemRoot aborts with
// `Assertion failed: ncrypto::CSPRNG(nullptr, 0)` before printing anything.
expect(
verifierHostEnvironment(
{
SYSTEMROOT: "C:\\Windows",
windir: "C:\\Windows",
ComSpec: "C:\\Windows\\system32\\cmd.exe",
PATHEXT: ".COM;.EXE;.BAT;.CMD",
USERPROFILE: "C:\\Users\\someone",
SECRET: "not copied",
},
"win32",
),
).toEqual({
SystemRoot: "C:\\Windows",
WINDIR: "C:\\Windows",
ComSpec: "C:\\Windows\\system32\\cmd.exe",
PATHEXT: ".COM;.EXE;.BAT;.CMD",
});
});
it("add nothing outside Windows", () => {
expect(
verifierHostEnvironment({ SystemRoot: "C:\\Windows" }, "linux"),
).toEqual({});
});
it.each(["qwen", "gemini", "commandcode", "deepseek"])(
"is used by the %s client verifier, which runs on Windows",
async (client) => {
const source = await readFile(
`${project}scripts/verify-${client}-client.mjs`,
"utf8",
);
// `\s+` also spans the CRLF of a Windows autocrlf checkout.
expect(source).toMatch(/\.\.\.verifierHostEnvironment\(\),\s+PATH:/u);
},
);
it.runIf(process.platform === "win32")(
"lets a child Node process start on Windows",
async () => {
const { stdout } = await execute(
process.execPath,
[
"-e",
"process.stdout.write(String(require('node:crypto').randomInt(1, 2)))",
],
{ env: { ...verifierHostEnvironment(), PATH: process.env.PATH ?? "" } },
);
expect(stdout).toBe("1");
},
);
});