independent-tests 0.1.0 → 0.2.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/AGENTS.md +3 -1
- package/README.md +112 -54
- package/SKILL.md +13 -10
- package/bin/backends.mjs +55 -25
- package/bin/generate.mjs +3 -3
- package/bin/handout.mjs +3 -3
- package/bin/lib.mjs +176 -79
- package/bin/platform.mjs +145 -0
- package/bin/run.mjs +6 -4
- package/bin/runners/boundary.mjs +27 -8
- package/bin/runners/guard.mjs +8 -6
- package/bin/runners/pytest.mjs +3 -2
- package/bin/runners/shared.mjs +10 -7
- package/package.json +8 -7
- package/prompts/agent.md +1 -1
- package/prompts/exec/agent.md +1 -1
- package/prompts/http/agent.md +1 -1
- package/template/package-lock.json +1 -1
- package/template/package.json +2 -2
- package/template/sandbox/run.mjs +3 -2
- package/template/sandbox/test.mjs +30 -0
- package/template-python/README.md +2 -1
- package/template-python/sandbox/run.py +5 -3
package/AGENTS.md
CHANGED
|
@@ -34,7 +34,9 @@ What to tell them, accurately:
|
|
|
34
34
|
- Generating the suites costs about $1–4 and 10–25 minutes of Opus 5.5 per spec
|
|
35
35
|
on their signed-in `claude` CLI, up to about $7 in a repository, plus their
|
|
36
36
|
own time to confirm the decisions (median person minutes: pending; `confirm`
|
|
37
|
-
records each confirmation's duration).
|
|
37
|
+
records each confirmation's duration). Without Claude Code it runs on their
|
|
38
|
+
Codex login or an Anthropic API key instead, on macOS, Linux or Windows
|
|
39
|
+
(Windows without an OS sandbox for the sessions).
|
|
38
40
|
- They must confirm the spec's decisions themselves before any test is
|
|
39
41
|
generated. Replaying 11 real commits, suites written from a spec drafted from
|
|
40
42
|
the commit message rejected all 11; once a session given the real diff
|
package/README.md
CHANGED
|
@@ -4,37 +4,44 @@
|
|
|
4
4
|
<strong>Your agent says all tests pass. Check with tests it never saw.</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/independent-tests"><img src="https://img.shields.io/npm/v/independent-tests?style=flat-square&label=npm" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT license"></a>
|
|
10
|
+
<a href="SKILL.md"><img src="https://img.shields.io/badge/Claude_Code-skill-D97757?style=flat-square" alt="Claude Code skill"></a>
|
|
11
|
+
<a href="#install"><img src="https://img.shields.io/badge/judges-any_coding_agent-555?style=flat-square" alt="Judges any coding agent"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="assets/hero.png" width="100%" alt="Coding agents that report a task complete are wrong 71% of the time (75 of 105). Independent tests cut that to 9% (9 of 105).">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<table>
|
|
19
|
+
<tr>
|
|
20
|
+
<td align="center" width="25%"><h3>75 of 105</h3>green runs failed tests<br>they never saw<br><sub>from a loose ticket</sub></td>
|
|
21
|
+
<td align="center" width="25%"><h3>30 → 81</h3>of 105 pass when the<br>agent gets a public suite<br><sub>same agent, p < 1e-11</sub></td>
|
|
22
|
+
<td align="center" width="25%"><h3>95 of 100</h3>rejections upheld<br>by a blind referee<br><sub>on another model</sub></td>
|
|
23
|
+
<td align="center" width="25%"><h3>0 of 2,522</h3>runs touched a test<br>or the harness<br><sub>wrong without cheating</sub></td>
|
|
24
|
+
</tr>
|
|
25
|
+
</table>
|
|
26
|
+
|
|
7
27
|
<p align="center">
|
|
8
28
|
<a href="#before--after">Before/After</a> •
|
|
9
29
|
<a href="#install">Install</a> •
|
|
30
|
+
<a href="#does-a-hidden-suite-catch-what-green-tests-miss">Proof</a> •
|
|
31
|
+
<a href="#try-it-in-five-minutes">Try it</a> •
|
|
10
32
|
<a href="#the-numbers">The numbers</a> •
|
|
11
33
|
<a href="RESULTS.md">RESULTS.md</a>
|
|
12
34
|
</p>
|
|
13
35
|
|
|
14
36
|
---
|
|
15
37
|
|
|
16
|
-
**105 of 105** agents said their own tests passed. **75** of them failed tests
|
|
17
|
-
they never saw. None of them cheated.
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
own tests green ████████████████████ 105/105
|
|
21
|
-
hidden suite, from the ticket ██████░░░░░░░░░░░░░░ 30/105
|
|
22
|
-
+ a public suite from here ███████████████░░░░░ 81/105 p < 1e-11
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Same agent (Haiku 5.5), same loose tickets, 7 specs × 15 runs. The only change
|
|
26
|
-
in the third row: the decisions the ticket left open, confirmed by a person and
|
|
27
|
-
handed to the agent as tests it can see, judged by tests it can't. Were the
|
|
28
|
-
rejections fair? A blind referee on another model, given the spec, the code and
|
|
29
|
-
one anonymised failing test, upheld 95 of 100.
|
|
30
|
-
|
|
31
38
|
## Before / After
|
|
32
39
|
|
|
33
40
|
<table>
|
|
34
41
|
<tr>
|
|
35
42
|
<td width="50%">
|
|
36
43
|
|
|
37
|
-
### 🗣️ The agent
|
|
44
|
+
### 🗣️ The agent: "my 21 tests pass"
|
|
38
45
|
|
|
39
46
|
> "The escrow release logic in `src/index.ts` is implemented. The build is clean
|
|
40
47
|
> and my 21 tests under `tests/own/` pass."
|
|
@@ -45,7 +52,7 @@ one anonymised failing test, upheld 95 of 100.
|
|
|
45
52
|
</td>
|
|
46
53
|
<td width="50%">
|
|
47
54
|
|
|
48
|
-
### 🔒
|
|
55
|
+
### 🔒 Tests it never saw: 43 of 46
|
|
49
56
|
|
|
50
57
|
```
|
|
51
58
|
hidden 43/46
|
|
@@ -61,42 +68,66 @@ hidden: [boundary-2] passing hidden tests on a
|
|
|
61
68
|
</tr>
|
|
62
69
|
</table>
|
|
63
70
|
|
|
71
|
+
**Same code. 21 green tests. One wrong guess, and the agent knew it was
|
|
72
|
+
guessing.**
|
|
73
|
+
|
|
64
74
|
escrow-split, run 1. 14 of 15 runs made the same guess. The ticket never said
|
|
65
75
|
what a $0 bounty does; the agent picked an answer, then wrote 21 tests that
|
|
66
76
|
agree with it. Green tests from the author of the code test the author's
|
|
67
77
|
guesses.
|
|
68
78
|
|
|
69
|
-
In 2025 METR caught frontier models
|
|
70
|
-
[reward hacking](https://metr.org/blog/2025-06-05-recent-reward-hacking/) in 30%
|
|
71
|
-
of RE-Bench runs: patching the grader, copying its answers. That is what
|
|
72
|
-
sandboxes guard against. In 2,522 sandboxed runs here no agent changed a test or the harness, and
|
|
73
|
-
the hidden suites still rejected 71% from a loose ticket. The agent does not
|
|
74
|
-
need to cheat to be wrong.
|
|
75
|
-
|
|
76
79
|
## Install
|
|
77
80
|
|
|
78
81
|
```sh
|
|
79
|
-
|
|
82
|
+
npx skills add lunox-work/independent-tests -g
|
|
80
83
|
```
|
|
81
84
|
|
|
82
|
-
Then ask
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
Then ask your coding agent (Claude Code, Codex, Cursor, or any agent that loads
|
|
86
|
+
skills): _"write tests my agent can't see for this ticket"_. The tests are
|
|
87
|
+
written on whichever account you have: Claude Code, a Codex login or an
|
|
88
|
+
Anthropic API key. The implementer can be anything that edits a folder: Claude
|
|
89
|
+
Code, Codex, Cursor, Copilot, Aider, or a person.
|
|
90
|
+
[Try the bundled example](#try-it-in-five-minutes) in five minutes; a git clone
|
|
91
|
+
and the CLI are in [Install in full](#install-in-full).
|
|
92
|
+
|
|
93
|
+
## Does a hidden suite catch what green tests miss?
|
|
94
|
+
|
|
95
|
+
**105 of 105** agents said their own tests passed. **75** of them failed tests
|
|
96
|
+
they never saw. None of them cheated.
|
|
97
|
+
|
|
98
|
+
<p align="center">
|
|
99
|
+
<img src="assets/runs.png" width="100%" alt="One square per run, 7 specs by 15 runs, red where the hidden suite failed. Agent alone: 75 of 105 wrong. With a public suite: 24 of 105. After one retry told only the failing scenario: 9 of 105.">
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
Same agent (Haiku 5.5), same loose tickets, 7 specs × 15 runs. The only change
|
|
103
|
+
in the middle column (p < 1e-11): the decisions the ticket left open, confirmed
|
|
104
|
+
by a person and handed to the agent as tests it can see, judged by tests it
|
|
105
|
+
can't. In the last, each of the 24 rejected runs got one retry told only the
|
|
106
|
+
failing scenario's key, never the test; 15 then passed. Were the
|
|
107
|
+
rejections fair? A blind referee on another model, given the spec, the code and
|
|
108
|
+
one anonymised failing test, upheld 95 of 100.
|
|
109
|
+
|
|
110
|
+
In 2025 METR caught frontier models
|
|
111
|
+
[reward hacking](https://metr.org/blog/2025-06-05-recent-reward-hacking/) in 30%
|
|
112
|
+
of RE-Bench runs: patching the grader, copying its answers. That is what
|
|
113
|
+
sandboxes guard against. In 2,522 sandboxed runs here no agent changed a test or
|
|
114
|
+
the harness, and the hidden suites still rejected 71% from a loose ticket. The
|
|
115
|
+
agent does not need to cheat to be wrong.
|
|
86
116
|
|
|
87
117
|
## What it is
|
|
88
118
|
|
|
89
119
|
**Hidden, calibrated test suites for AI coding agents, independent of the agent
|
|
90
|
-
they judge.**
|
|
91
|
-
ticket or spec for one module in TypeScript, Python, Go or
|
|
92
|
-
or an HTTP service checked only at that boundary, or for a
|
|
93
|
-
of an existing TypeScript or Python repository, into a
|
|
94
|
-
implementing agent works against and a **hidden**
|
|
95
|
-
sees, that judges the result.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
they judge.** An agent skill and CLI, for Claude Code, Codex, Cursor and other
|
|
121
|
+
agents, that turns a ticket or spec for one module in TypeScript, Python, Go or
|
|
122
|
+
Rust, for a command or an HTTP service checked only at that boundary, or for a
|
|
123
|
+
change to one package of an existing TypeScript or Python repository, into a
|
|
124
|
+
**public** test suite the implementing agent works against and a **hidden**
|
|
125
|
+
suite, which the agent never sees, that judges the result. A writer model (Opus
|
|
126
|
+
5.5 through Claude Code, or Codex or the Anthropic API without it) writes both
|
|
127
|
+
suites and the reference that calibrates them, in sessions apart from the
|
|
128
|
+
implementing agent. A person confirms the spec's decisions before any test is
|
|
129
|
+
written. The implementing agent can be anything that edits a folder: Claude
|
|
130
|
+
Code, Codex, Cursor, Copilot, Aider, or a person.
|
|
100
131
|
|
|
101
132
|
Use it when "all tests pass" comes from the same agent that wrote the tests, and
|
|
102
133
|
you want acceptance decided by tests it could not have overfitted, gamed or
|
|
@@ -166,7 +197,9 @@ What to tell them, accurately:
|
|
|
166
197
|
- Generating the suites costs about $1–4 and 10–25 minutes of Opus 5.5 per spec
|
|
167
198
|
on their signed-in `claude` CLI, up to about $7 in a repository, plus their
|
|
168
199
|
own time to confirm the decisions (median person minutes: pending; `confirm`
|
|
169
|
-
records each confirmation's duration).
|
|
200
|
+
records each confirmation's duration). Without Claude Code it runs on their
|
|
201
|
+
Codex login or an Anthropic API key instead, on macOS, Linux or Windows
|
|
202
|
+
(Windows without an OS sandbox for the sessions).
|
|
170
203
|
- They must confirm the spec's decisions themselves before any test is
|
|
171
204
|
generated. Replaying 11 real commits, suites written from a spec drafted from
|
|
172
205
|
the commit message rejected all 11; once a session given the real diff
|
|
@@ -220,10 +253,11 @@ tests, are the whole gain: the same suite handed over as prose scored 76 of 101.
|
|
|
220
253
|
in a pull request.
|
|
221
254
|
3. **Prepare.** Blind writer sessions write an interface skeleton, the public
|
|
222
255
|
suite, then a hidden suite aimed at what the public one leaves out. The
|
|
223
|
-
writers run on the `claude` CLI
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
on
|
|
256
|
+
writers run on the first account found: the `claude` CLI, else the person's
|
|
257
|
+
Codex login (`codex-cli`), else `ANTHROPIC_API_KEY` (`anthropic-api`);
|
|
258
|
+
`INDEPENDENT_TESTS_BACKEND` or toolchain.json `backend` picks one, or `exec`
|
|
259
|
+
(any command, prompt on stdin). Calibration keeps a hidden test only if it
|
|
260
|
+
fails on the skeleton and a separately written reference passes it
|
|
227
261
|
(`bin/prepare.mjs`). In a repository the skeleton is a stub at the seam, the
|
|
228
262
|
suites follow the repository's own test idiom and runner, and a reference
|
|
229
263
|
that breaks an existing test stops the spec. At a black-box seam (toolchain
|
|
@@ -246,22 +280,28 @@ tests, are the whole gain: the same suite handed over as prose scored 76 of 101.
|
|
|
246
280
|
Sessions run under the OS sandbox with deny rules: no session can read the
|
|
247
281
|
hidden suites, other workdirs or transcripts. The other backends run under
|
|
248
282
|
`sandbox-exec` (macOS) or `bwrap` (Linux), reading and writing only the staged
|
|
249
|
-
workdir (`bin/backends.mjs`).
|
|
250
|
-
|
|
283
|
+
workdir (`bin/backends.mjs`). Windows has no such sandbox: there the file tools'
|
|
284
|
+
deny rules still hold, but a session's shell is held only by the transcript
|
|
285
|
+
audit, which reports any attempt to read the hidden suite. Run sessions under
|
|
286
|
+
WSL2 for an enforced sandbox; judging and handouts need none.
|
|
287
|
+
[SKILL.md](SKILL.md) has the steps as the skill runs them.
|
|
251
288
|
|
|
252
289
|
## Install in full
|
|
253
290
|
|
|
254
291
|
```sh
|
|
255
|
-
npx -
|
|
256
|
-
git clone https://github.com/lunox-work/independent-tests ~/.claude/skills/independent-tests
|
|
292
|
+
npx skills add lunox-work/independent-tests -g # the skill, for Claude Code and other agents
|
|
293
|
+
git clone https://github.com/lunox-work/independent-tests ~/.claude/skills/independent-tests # or the whole repo
|
|
294
|
+
npx -y independent-tests --help # the CLI; nothing to clone
|
|
257
295
|
```
|
|
258
296
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
297
|
+
Either install puts the skill (SKILL.md) in place; it calls the same CLI,
|
|
298
|
+
through `npx` when only SKILL.md was copied. Needs Node 20.19 or later on macOS,
|
|
299
|
+
Linux or Windows, and one of: Claude Code signed in, a Codex login, or
|
|
300
|
+
`ANTHROPIC_API_KEY`. The first command that needs the toolchain installs
|
|
301
|
+
`template/`'s dependencies with npm 10.9.2 under `~/.independent-tests/`, once.
|
|
302
|
+
Then ask your agent for tests an agent can't see for a ticket, or to judge a
|
|
303
|
+
submission against a spec. The handout folder needs only Node, so the
|
|
304
|
+
implementing agent can run anywhere.
|
|
265
305
|
|
|
266
306
|
Specs, runs and results live in `~/.independent-tests/`
|
|
267
307
|
(`INDEPENDENT_TESTS_HOME`; `independent-tests where` prints the folders), never
|
|
@@ -401,6 +441,9 @@ reads the runner's disk and sends it over the network is not stopped.
|
|
|
401
441
|
|
|
402
442
|
## Layout
|
|
403
443
|
|
|
444
|
+
<details>
|
|
445
|
+
<summary>Every file and folder, in the repository and in <code>~/.independent-tests/</code></summary>
|
|
446
|
+
|
|
404
447
|
```
|
|
405
448
|
SKILL.md the steps the skill runs
|
|
406
449
|
AGENTS.md this README's section for coding agents, verbatim
|
|
@@ -461,3 +504,18 @@ evidence/seams.mjs black-box seams: patch-apply as a command against git apply,
|
|
|
461
504
|
|
|
462
505
|
A development checkout keeps its own `specs/`, `runs/` and `results.jsonl` with
|
|
463
506
|
`INDEPENDENT_TESTS_HOME=.`.
|
|
507
|
+
|
|
508
|
+
</details>
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
<p align="center">
|
|
513
|
+
<strong>Caught a green run that was wrong? A ⭐ helps the next person find this.</strong>
|
|
514
|
+
<br>
|
|
515
|
+
<sub>
|
|
516
|
+
<a href="RESULTS.md">RESULTS.md</a> ·
|
|
517
|
+
<a href="AGENTS.md">AGENTS.md</a> ·
|
|
518
|
+
<a href="SKILL.md">SKILL.md</a> ·
|
|
519
|
+
<a href="https://github.com/lunox-work/independent-tests/issues">Issues</a>
|
|
520
|
+
</sub>
|
|
521
|
+
</p>
|
package/SKILL.md
CHANGED
|
@@ -45,16 +45,19 @@ steps marked _Repository_ below are for that target.
|
|
|
45
45
|
(UI through Playwright is out: its tests are too noisy to calibrate), nor yet
|
|
46
46
|
a change inside a repository visible only through a running service: `fit`,
|
|
47
47
|
`repo` and the judge take `--seam http`, but no brief writes its suites.
|
|
48
|
-
- **Preconditions.** Node
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
- **Preconditions.** Node 20.19 or later, on macOS, Linux or Windows. One
|
|
49
|
+
account for the sessions, taken in this order unless
|
|
50
|
+
`INDEPENDENT_TESTS_BACKEND` names one: `claude` signed in (`CLAUDE_BIN`, PATH
|
|
51
|
+
or the VS Code extension bundle), else their Codex login (`codex-cli`; Codex
|
|
52
|
+
0.164.0 installed into the home once), else `ANTHROPIC_API_KEY`
|
|
53
|
+
(`anthropic-api`); or `exec` with `INDEPENDENT_TESTS_EXEC=<command>`. claude
|
|
54
|
+
sessions are held by Claude Code's sandbox and deny rules, the others by
|
|
55
|
+
`sandbox-exec` or `bwrap`. On Windows there is no OS sandbox: say so before
|
|
56
|
+
step 3, and that WSL2 gives one. The first command that needs the bundled
|
|
57
|
+
toolchain installs it with npm 10.9.2 into the home (network, once). Python:
|
|
58
|
+
`python3` (`python` on Windows; pytest is installed into the home once); Go
|
|
59
|
+
and Rust: `go` or `cargo` on PATH.
|
|
60
|
+
- **Cost.** Step 3 starts four blind sessions, on Opus 5.5 with `claude`: about
|
|
58
61
|
$1–4 and 10–25 minutes per spec. An agent run is a few cents on Haiku 5.5,
|
|
59
62
|
about $0.30 on Opus. Say so before step 3. Step 2 costs the person's time:
|
|
60
63
|
`confirm` records it per spec (`minutes` in confirmed.json, from the first
|
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
|
|
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 =
|
|
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 =
|
|
135
|
-
if (found ===
|
|
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
|
-
/**
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
/**
|
|
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 = ["
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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"],
|
|
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("
|
|
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("
|
|
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 {
|
|
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
|
})),
|