independent-tests 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/backends.mjs CHANGED
@@ -45,8 +45,12 @@
45
45
  //
46
46
  // `placement`'s denial is checked by evidence/backends.mjs `sandbox`, a
47
47
  // negative test that reads a hidden-suite path and the home from inside.
48
+ // Windows has neither sandbox: there `placement` runs the command as it
49
+ // is, the shell being Git for Windows' bash, and says so once (lib.mjs
50
+ // `warnUnsandboxed`); the staged workdir and the transcript audit are what
51
+ // keep a writer blind, and anthropic-api's file tools stay confined.
48
52
 
49
- import { spawn, spawnSync } from "node:child_process";
53
+ import { spawn } from "node:child_process";
50
54
  import { randomUUID } from "node:crypto";
51
55
  import {
52
56
  appendFileSync,
@@ -61,7 +65,7 @@ import {
61
65
  } from "node:fs";
62
66
  import { createRequire } from "node:module";
63
67
  import { homedir, tmpdir } from "node:os";
64
- import { basename, dirname, join, relative, resolve } from "node:path";
68
+ import { basename, dirname, join, posix, relative, resolve } from "node:path";
65
69
  import { pathToFileURL } from "node:url";
66
70
  import {
67
71
  DEFAULT_MODEL,
@@ -78,14 +82,15 @@ import {
78
82
  log,
79
83
  sha256,
80
84
  takeSlot,
85
+ warnUnsandboxed,
81
86
  } from "./lib.mjs";
87
+ import { GROUP, OS_SANDBOX, WINDOWS, gitBash, inside, pathKey, slash, spawnCommand, which } from "./platform.mjs";
82
88
 
83
89
  export const CODEX_VERSION = "0.164.0";
84
90
  export const SDK_VERSION = "0.137.0";
85
91
  /** Per session: CODEX_HOME, HOME, TMPDIR, the API backend's messages. */
86
92
  export const SESSIONS = join(WORK_ROOT, "sessions");
87
93
 
88
- const inside = (path, folder) => path === folder || path.startsWith(`${folder}/`);
89
94
  /** `path` made real as far as it exists (the sandboxes match real paths). */
90
95
  export function real(path) {
91
96
  let at = resolve(path);
@@ -110,7 +115,7 @@ export function installTool(name, spec, version) {
110
115
  mkdirSync(partial, { recursive: true });
111
116
  writeFileSync(join(partial, "package.json"), `${JSON.stringify({ private: true })}\n`);
112
117
  log(`installing ${spec}@${version} into ${dest} (npm@10.9.2, once)`);
113
- const out = spawnSync("npx", ["-y", "npm@10.9.2", "install", "--no-audit", "--no-fund", "--save-exact", `${spec}@${version}`], {
118
+ const out = spawnCommand("npx", ["-y", "npm@10.9.2", "install", "--no-audit", "--no-fund", "--save-exact", `${spec}@${version}`], {
114
119
  cwd: partial,
115
120
  encoding: "utf8",
116
121
  maxBuffer: 64 * 1024 * 1024,
@@ -131,8 +136,8 @@ export function installTool(name, spec, version) {
131
136
 
132
137
  /** The install prefix of a binary (two folders above its real path), for the sandbox's read list. */
133
138
  function prefixOf(bin) {
134
- const found = bin.includes("/") ? bin : spawnSync("sh", ["-c", `command -v ${bin}`], { encoding: "utf8" }).stdout.trim();
135
- if (found === "" || !existsSync(found)) return null;
139
+ const found = which(bin);
140
+ if (found === null) return null;
136
141
  return dirname(dirname(realpathSync(found)));
137
142
  }
138
143
 
@@ -203,16 +208,28 @@ export function bwrapArgs({ deny, readable, writable }, network, cwd) {
203
208
  return out;
204
209
  }
205
210
 
206
- /** `bin argv` wrapped in the platform's sandbox: [bin, argv]. */
211
+ /**
212
+ * The shell `placement` runs a command line in: /bin/sh and /bin/bash on
213
+ * macOS and Linux, Git for Windows' bash on Windows (what Claude Code's
214
+ * Bash tool runs there).
215
+ */
216
+ export function shellBin(name) {
217
+ if (!WINDOWS) return `/bin/${name}`;
218
+ const bash = gitBash();
219
+ if (bash === null) throw new Error("No bash: install Git for Windows, or set CLAUDE_CODE_GIT_BASH_PATH to its bash.exe.");
220
+ return bash;
221
+ }
222
+ /** `bin argv` wrapped in the platform's sandbox: [bin, argv]. Windows has none: [bin, argv] as given. */
207
223
  export function placement(bin, argv, { cwd, state, read = [], network = true }) {
224
+ if (!OS_SANDBOX) {
225
+ warnUnsandboxed();
226
+ return [bin, argv];
227
+ }
208
228
  const paths = placementPaths({ cwd, state, read });
209
229
  if (process.platform === "darwin") return ["/usr/bin/sandbox-exec", ["-p", seatbeltProfile(paths, network), bin, ...argv]];
210
- if (process.platform === "linux") {
211
- const found = spawnSync("sh", ["-c", "command -v bwrap"], { encoding: "utf8" }).stdout.trim();
212
- if (found === "") throw new Error("No bwrap on PATH: a non-Claude backend on Linux needs bubblewrap for its sandbox.");
213
- return [found, [...bwrapArgs(paths, network, cwd), "--", bin, ...argv]];
214
- }
215
- throw new Error(`No sandbox for ${process.platform}: non-Claude backends run on macOS or Linux.`);
230
+ const found = which("bwrap");
231
+ if (found === null) throw new Error("No bwrap on PATH: a non-Claude backend on Linux needs bubblewrap for its sandbox.");
232
+ return [found, [...bwrapArgs(paths, network, cwd), "--", bin, ...argv]];
216
233
  }
217
234
 
218
235
  /** A session's state folder, kept across a resume of the same transcript and cleared on a fresh start. */
@@ -223,13 +240,25 @@ function stateOf(transcript, resume) {
223
240
  return dir;
224
241
  }
225
242
 
226
- /** The environment a placed process starts with: nothing of the caller's but the basics. */
243
+ /**
244
+ * The environment a placed process starts with: nothing of the caller's
245
+ * but the basics, plus on Windows what any process there needs to start
246
+ * (SystemRoot, ComSpec, PATHEXT) and the state folder as its profile and
247
+ * temp folder too.
248
+ */
227
249
  function childEnv(state, extra = {}) {
228
- const keep = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "USER", "LOGNAME", "SHELL"];
250
+ const keep = ["LANG", "LC_ALL", "LC_CTYPE", "TERM", "USER", "LOGNAME", "SHELL"];
251
+ const windows = ["SystemRoot", "SystemDrive", "windir", "ComSpec", "PATHEXT", "USERNAME", "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE"];
252
+ const home = join(state, "home");
253
+ const temp = join(state, "tmp");
229
254
  return {
230
- ...Object.fromEntries(keep.filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]])),
231
- HOME: join(state, "home"),
232
- TMPDIR: join(state, "tmp"),
255
+ [pathKey(process.env)]: process.env[pathKey(process.env)],
256
+ ...Object.fromEntries(
257
+ [...keep, ...(WINDOWS ? windows : [])].filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]]),
258
+ ),
259
+ HOME: home,
260
+ TMPDIR: temp,
261
+ ...(WINDOWS ? { USERPROFILE: home, TEMP: temp, TMP: temp, APPDATA: join(home, "AppData", "Roaming"), LOCALAPPDATA: join(home, "AppData", "Local") } : {}),
233
262
  ...extra,
234
263
  };
235
264
  }
@@ -249,7 +278,7 @@ const LIMITED = /usage limit|rate.?limit|quota|too many requests|429|out of cred
249
278
  function runChild(bin, argv, { cwd, env, input, minutes, onLine }) {
250
279
  return new Promise((done) => {
251
280
  // A group leader, its group killed when it closes: nothing it started outlives the session.
252
- const child = spawn(bin, argv, { cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: true });
281
+ const child = spawn(bin, argv, { cwd, env, stdio: ["pipe", "pipe", "pipe"], ...GROUP });
253
282
  const reap = () => killGroup(child.pid);
254
283
  process.once("exit", reap);
255
284
  child.on("exit", reap);
@@ -437,7 +466,7 @@ async function execBackend({ cwd, text, transcript, kind, resume, chain, command
437
466
  return { ...base, ok: false, exitCode: null, timedOut: false, durationMs: 0, subtype: "error_during_execution", finalText, stderr: "" };
438
467
  }
439
468
  const state = stateOf(transcript, resume);
440
- const [bin, argv] = placement("/bin/sh", ["-c", line], { cwd, state });
469
+ const [bin, argv] = placement(shellBin("sh"), ["-c", line], { cwd, state });
441
470
  const passed = (process.env.INDEPENDENT_TESTS_EXEC_ENV ?? "").split(",").filter((key) => key && process.env[key] !== undefined);
442
471
  const env = childEnv(state, Object.fromEntries(passed.map((key) => [key, process.env[key]])));
443
472
  write({ type: "system", subtype: "init", session_id: sessionId, model: null, cwd, backend: "exec", command: line });
@@ -559,7 +588,7 @@ function fileTools(root, runBash) {
559
588
  if (!inside(real(full), realRoot)) throw new Error(`${path} is outside the working directory ${root}`);
560
589
  return full;
561
590
  };
562
- const shown = (full) => relative(root, full) || ".";
591
+ const shown = (full) => slash(relative(root, full)) || ".";
563
592
  return {
564
593
  Bash: (input) => runBash(String(input.command ?? ""), Math.min(Number(input.timeout ?? 120_000), 600_000)),
565
594
  Read: (input) => {
@@ -592,7 +621,7 @@ function fileTools(root, runBash) {
592
621
  const base = confined(input.path);
593
622
  const matches = globRegExp(String(input.pattern));
594
623
  const hits = listFiles(base).filter((path) => matches.test(path) || (!String(input.pattern).includes("/") && matches.test(basename(path))));
595
- return hits.slice(0, 500).map((path) => join(shown(base) === "." ? "" : shown(base), path)).join("\n") || "no files";
624
+ return hits.slice(0, 500).map((path) => posix.join(shown(base) === "." ? "" : shown(base), path)).join("\n") || "no files";
596
625
  },
597
626
  Grep: (input) => {
598
627
  const base = confined(input.path);
@@ -601,7 +630,7 @@ function fileTools(root, runBash) {
601
630
  const out = [];
602
631
  const files = statSync(base).isDirectory() ? listFiles(base).map((path) => join(base, path)) : [base];
603
632
  for (const full of files) {
604
- if (only !== null && !only.test(relative(base, full)) && !only.test(basename(full))) continue;
633
+ if (only !== null && !only.test(slash(relative(base, full))) && !only.test(basename(full))) continue;
605
634
  if (statSync(full).size > 1_000_000) continue;
606
635
  const lines = readFileSync(full, "utf8").split("\n");
607
636
  lines.forEach((line, at) => {
@@ -629,10 +658,11 @@ async function anthropicApi({ cwd, text, transcript, kind, resume, model, maxTur
629
658
  const started = Date.now();
630
659
  const deadline = started + minutes * 60_000;
631
660
  const runBash = (command, timeout) => {
632
- const [bin, argv] = placement("/bin/bash", ["-c", command], { cwd, state, network: false });
661
+ const [bin, argv] = placement(shellBin("bash"), ["-c", command], { cwd, state, network: false });
633
662
  return new Promise((done) => {
634
663
  let output = "";
635
- const child = spawn(bin, argv, { cwd, env: childEnv(state), stdio: ["ignore", "pipe", "pipe"] });
664
+ const child = spawn(bin, argv, { cwd, env: childEnv(state), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
665
+
636
666
  const take = (chunk) => (output = (output + chunk).slice(-30_000));
637
667
  child.stdout.on("data", take);
638
668
  child.stderr.on("data", take);
package/bin/generate.mjs CHANGED
@@ -29,7 +29,7 @@
29
29
  // uses the same model as every other session.
30
30
 
31
31
  import { existsSync, readFileSync, rmSync } from "node:fs";
32
- import { basename, dirname, join, relative } from "node:path";
32
+ import { join, posix } from "node:path";
33
33
  import {
34
34
  RUNS,
35
35
  args,
@@ -74,7 +74,7 @@ function suitesOf(chain) {
74
74
  dir: "tests/public",
75
75
  folder: publicFolder,
76
76
  own: repo
77
- ? (path) => dirname(path) === publicFolder && chain.isPublicName(basename(path)) && test(path)
77
+ ? (path) => posix.dirname(path) === publicFolder && chain.isPublicName(posix.basename(path)) && test(path)
78
78
  : (path) => path.startsWith(`${publicFolder}/`) && test(path),
79
79
  required: repo ? `${publicFolder}/${chain.publicName} (${chain.testPattern})` : `${publicFolder}/${chain.testPattern}`,
80
80
  prompt: "public-tests",
@@ -255,7 +255,7 @@ async function generate(slug, force, only = null) {
255
255
  if (record[suite.name].problem === null) {
256
256
  rmSync(join(out, target), { recursive: true, force: true });
257
257
  const files = Object.keys(manifestOf(dir, chain)).filter(inSuite);
258
- for (const path of files) copy(join(dir, path), join(out, target, relative(suite.folder, path)));
258
+ for (const path of files) copy(join(dir, path), join(out, target, posix.relative(suite.folder, path)));
259
259
  record[suite.name].files = files;
260
260
  // A hidden suite written against an older public suite is stale.
261
261
  if (suite.name === "public")
package/bin/handout.mjs CHANGED
@@ -26,7 +26,7 @@
26
26
  import { spawnSync } from "node:child_process";
27
27
  import { commitment, recordCommitment } from "./commitment.mjs";
28
28
  import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
29
- import { join, resolve } from "node:path";
29
+ import { join, posix, resolve, sep } from "node:path";
30
30
  import {
31
31
  AGENT,
32
32
  PRIVATE,
@@ -58,7 +58,7 @@ if (options.repo !== undefined) {
58
58
  process.exit(0);
59
59
  }
60
60
  const dir = resolve(target);
61
- const beside = PRIVATE.find((folder) => dir === folder || dir.startsWith(`${folder}/`));
61
+ const beside = PRIVATE.find((folder) => dir === folder || dir.startsWith(`${folder}${sep}`));
62
62
  if (beside !== undefined) {
63
63
  console.error(`${dir} is inside ${beside}, beside the hidden suites; pick a folder outside it.`);
64
64
  process.exit(1);
@@ -112,7 +112,7 @@ function repoHandout(checkoutPath, remove) {
112
112
  const files = [
113
113
  ...chain.seam.map((path) => ({ path, from: join(spec, "skeleton", path), seam: true })),
114
114
  ...listFiles(join(spec, "tests", "public")).map((name) => ({
115
- path: join(chain.publicDir, name),
115
+ path: posix.join(chain.publicDir, name),
116
116
  from: join(spec, "tests", "public", name),
117
117
  seam: false,
118
118
  })),