vigiles 33.1.0 → 33.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/README.md +3 -2
- package/dist/adapter-conformance.js +14 -10
- package/dist/adapter.d.ts +3 -1
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/output-style.d.ts +3 -0
- package/dist/adapters/claude-code/output-style.js +81 -0
- package/dist/adapters/claude-code/replies.d.ts +14 -0
- package/dist/adapters/claude-code/replies.js +78 -0
- package/dist/adapters/claude-code/runtime.js +70 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +42 -21
- package/dist/adapters/claude-code/skill-reachability.js +91 -28
- package/dist/audit-report.d.ts +5 -0
- package/dist/audit-report.js +29 -20
- package/dist/audit-report.template.html +25 -25
- package/dist/audit-score.js +2 -2
- package/dist/check.d.ts +20 -0
- package/dist/check.js +87 -2
- package/dist/cli-main.js +249 -30
- package/dist/core/adapter.d.ts +7 -0
- package/dist/core/config-schema.d.ts +9 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/eval-driver.d.ts +10 -0
- package/dist/core/frontmatter-read.d.ts +31 -0
- package/dist/core/frontmatter-read.js +4 -2
- package/dist/core/harness-driver.d.ts +2 -0
- package/dist/core/install-reader.js +15 -0
- package/dist/core/layout.d.ts +17 -0
- package/dist/core/layout.js +17 -0
- package/dist/core/output-style.d.ts +84 -0
- package/dist/core/output-style.js +90 -0
- package/dist/core/rule-meta.d.ts +1 -1
- package/dist/core/rule-meta.js +8 -0
- package/dist/core/run-env.d.ts +50 -0
- package/dist/core/run-env.js +51 -0
- package/dist/core/runtime.d.ts +44 -0
- package/dist/core/skill-link-target.d.ts +20 -0
- package/dist/core/skill-link-target.js +33 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/coverage-evidence.d.ts +42 -0
- package/dist/coverage-evidence.js +39 -0
- package/dist/eval.d.ts +65 -42
- package/dist/eval.js +116 -120
- package/dist/harness-test.d.ts +65 -1
- package/dist/harness-test.js +114 -22
- package/dist/optimize.js +1 -5
- package/dist/output-style-arms.d.ts +27 -0
- package/dist/output-style-arms.js +51 -0
- package/dist/plugin-declaration.d.ts +6 -6
- package/dist/plugin-declaration.js +6 -6
- package/dist/scan-files.js +1 -1
- package/dist/scan.d.ts +6 -0
- package/dist/scan.js +14 -4
- package/dist/score-core.d.ts +3 -0
- package/dist/score-core.js +5 -1
- package/dist/setup-plan.d.ts +50 -10
- package/dist/setup-plan.js +103 -68
- package/dist/skill-links.d.ts +183 -0
- package/dist/skill-links.js +255 -0
- package/dist/test-coverage-files.d.ts +3 -1
- package/dist/test-coverage-files.js +20 -6
- package/dist/test-coverage.d.ts +18 -1
- package/dist/test-coverage.js +101 -15
- package/dist/test.d.ts +3 -1
- package/dist/test.js +8 -2
- package/dist/vigilesrc.schema.json +134 -0
- package/package.json +1 -1
package/dist/eval.d.ts
CHANGED
|
@@ -119,7 +119,11 @@ export interface EvalSpec<M extends Metrics> {
|
|
|
119
119
|
* the cache, and never read from an env var. Omit for the harness default.
|
|
120
120
|
*/
|
|
121
121
|
readonly effort?: string | number;
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Tools pre-approved for the run (`--allowedTools`, a permission allowlist);
|
|
124
|
+
* the default tools stay available either way. `[]` approves none.
|
|
125
|
+
* Default: Read Edit Write Bash.
|
|
126
|
+
*/
|
|
123
127
|
readonly allowedTools?: readonly string[];
|
|
124
128
|
/** Per-run timeout ms. Default 240000. */
|
|
125
129
|
readonly timeoutMs?: number;
|
|
@@ -164,8 +168,9 @@ export interface EvalSpec<M extends Metrics> {
|
|
|
164
168
|
* **Ships default-OFF** because a too-narrow auth allowlist would silently break
|
|
165
169
|
* the real `claude` CLI's authentication; leaving it off keeps every existing
|
|
166
170
|
* eval (including one running right now) authenticating exactly as before. When
|
|
167
|
-
* absent / `false`, the per-trial env is
|
|
168
|
-
*
|
|
171
|
+
* absent / `false`, the per-trial env is the caller's (`{ ...process.env,
|
|
172
|
+
* ...arm.env }`) minus the harness's declared session identity, which no child
|
|
173
|
+
* run inherits in either mode. See `docs/safety.md` (ephemerality) and
|
|
169
174
|
* `research/cross-platform-sandboxing.md`.
|
|
170
175
|
*/
|
|
171
176
|
readonly ephemeralEnv?: boolean;
|
|
@@ -245,11 +250,13 @@ export interface EvalReport {
|
|
|
245
250
|
* Resolve the environment a trial's subprocess actually runs with — the
|
|
246
251
|
* SECURITY-CRITICAL decision behind `ephemeralEnv`. When `replaceEnv` is set, the
|
|
247
252
|
* scrubbed `env` is the COMPLETE environment, so the real `$HOME` and inherited
|
|
248
|
-
* secrets are DROPPED; otherwise `env` is an overlay on `base
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
+
* secrets are DROPPED; otherwise `env` is an overlay on `base`. In BOTH, the
|
|
254
|
+
* harness's declared session identity (`runEnv.sessionIdentity`) is removed: a
|
|
255
|
+
* run started from inside a live session must not run as that session, scrubbed
|
|
256
|
+
* or not. Extracted from the `v8 ignore`d `spawnAgent` so the one line that
|
|
257
|
+
* enforces the scrub is both unit- and behaviourally-tested — a regression to an
|
|
258
|
+
* always-merge would otherwise silently defeat ephemerality and leak the host
|
|
259
|
+
* environment into an untrusted, model-driven run.
|
|
253
260
|
*/
|
|
254
261
|
export declare function resolveSpawnEnv(a: Pick<AgentRunArgs, "env" | "replaceEnv" | "effort">, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
255
262
|
/**
|
|
@@ -264,10 +271,10 @@ export declare const EFFORT_ENV_VAR = "CLAUDE_CODE_EFFORT_LEVEL";
|
|
|
264
271
|
*
|
|
265
272
|
* WHY THIS EXISTS AND WHY IT IS NOT OPTIONAL. Effort has THREE inputs — the
|
|
266
273
|
* `--effort` flag, the `effortLevel` settings key, and `CLAUDE_CODE_EFFORT_LEVEL`
|
|
267
|
-
* — and the env var wins over the flag.
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
274
|
+
* — and the env var wins over the flag. The default (inherited) spawn env
|
|
275
|
+
* carries the caller's whole environment, so an ambient
|
|
276
|
+
* `CLAUDE_CODE_EFFORT_LEVEL=max` in the author's shell reaches the run unless
|
|
277
|
+
* something removes it. Without this pin,
|
|
271
278
|
* hashing effort into the lock would make the lock CONFIDENTLY WRONG: it would
|
|
272
279
|
* record `low` over a run that executed at `max` — the exact defect the feature
|
|
273
280
|
* exists to prevent, reintroduced by the fix for it.
|
|
@@ -309,6 +316,22 @@ export declare function withEffortGuard(runner: AgentRunner): AgentRunner;
|
|
|
309
316
|
* inline there could not be asserted at all. Mirrors `buildCodexArgs`.
|
|
310
317
|
*/
|
|
311
318
|
export declare function buildAgentArgs(a: AgentRunArgs): string[];
|
|
319
|
+
/**
|
|
320
|
+
* Why a run never reached the model, or null when it did. A run that exited
|
|
321
|
+
* non-zero with no `assistant` event and no `result` event asked the model
|
|
322
|
+
* nothing — a usage error in the command line, a missing binary — and scored as
|
|
323
|
+
* a trial it reads as a miss on every check that needs output and a pass on
|
|
324
|
+
* every bound it cannot break. A run that reached the model and then broke is a
|
|
325
|
+
* scored outcome, not this. Rate-limited runs are left to the retry loop.
|
|
326
|
+
*/
|
|
327
|
+
export declare function startFailure(out: Readonly<RunOut>): string | null;
|
|
328
|
+
/**
|
|
329
|
+
* Wrap a runner so a run that never reached the model THROWS instead of being
|
|
330
|
+
* scored — the same choice, for the same reason, as {@link withEffortGuard}:
|
|
331
|
+
* the cause is usually deterministic, so every trial would fail it. Not `async`,
|
|
332
|
+
* for the reason that guard gives.
|
|
333
|
+
*/
|
|
334
|
+
export declare function withStartGuard(runner: AgentRunner): AgentRunner;
|
|
312
335
|
/**
|
|
313
336
|
* The real `claude`-spawning runner (composition root). Exported so other
|
|
314
337
|
* real-model entries (e.g. the `audit` trigger tier) bind the same runner.
|
|
@@ -391,7 +414,7 @@ export interface MeasureSpec {
|
|
|
391
414
|
* the cache, and never read from an env var. Omit for the harness default.
|
|
392
415
|
*/
|
|
393
416
|
readonly effort?: string | number;
|
|
394
|
-
/** Tools the
|
|
417
|
+
/** Tools pre-approved for the run (`--allowedTools`); `[]` approves none. */
|
|
395
418
|
readonly allowedTools?: readonly string[];
|
|
396
419
|
/** Per-run timeout ms. */
|
|
397
420
|
readonly timeoutMs?: number;
|
|
@@ -581,51 +604,48 @@ export declare function modelTier(id: string): number | null;
|
|
|
581
604
|
export declare function belowModelFloor(model: string, floor: string): boolean;
|
|
582
605
|
/**
|
|
583
606
|
* Build an **ephemeral run environment** for a model-driven run: a NEW env object
|
|
584
|
-
* with a *fresh* `HOME` (and `TMPDIR`) pointed at the throwaway `opts.home`,
|
|
585
|
-
*
|
|
586
|
-
* else
|
|
607
|
+
* with a *fresh* `HOME` (and `TMPDIR`) pointed at the throwaway `opts.home`, the
|
|
608
|
+
* OS essentials (`PATH`, `LANG`, `LC_*`, `TERM`), the harness's OWN auth, and
|
|
609
|
+
* nothing else. Pure — no fs, no spawn.
|
|
587
610
|
*
|
|
588
611
|
* The rationale is "fresh HOME + only the harness credential injected, **not** a
|
|
589
612
|
* blanket wipe": running a model-driven skill/agent is itself a side effect (the
|
|
590
613
|
* *model*, not the author, chose the actions), so it should not be able to read
|
|
591
614
|
* the real `~/.gitconfig` / `~/.ssh` / `~/.aws` or write to the real `~`. But the
|
|
592
|
-
* real
|
|
593
|
-
* (
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
615
|
+
* real CLI must still AUTHENTICATE, so the auth names the harness declares
|
|
616
|
+
* (`runtime.runEnv.keep`) are re-injected, and the parent session's identity
|
|
617
|
+
* (`runtime.runEnv.sessionIdentity`) never is. Which names those are is the
|
|
618
|
+
* adapter's fact, not this file's: see `RunEnvPolicy` in `core/runtime.ts`.
|
|
619
|
+
* Because this needs no kernel features, it is the cross-platform
|
|
620
|
+
* STATE-protection floor (lands on macOS immediately), orthogonal to the Linux
|
|
621
|
+
* bubblewrap HOST confinement in `src/sandbox.ts`.
|
|
622
|
+
*
|
|
623
|
+
* Notably it does NOT pass through `GIT_*`, `GH_TOKEN`, `SSH_*`, `AWS_*` access
|
|
624
|
+
* keys, a config-dir override, or any other undeclared variable — those are
|
|
625
|
+
* exactly what an ephemeral run must not see. A too-narrow `keep` breaks auth
|
|
626
|
+
* loudly (the run fails to start); a too-broad one leaks silently, which is why
|
|
627
|
+
* it is an allowlist and not a prefix.
|
|
598
628
|
*
|
|
599
629
|
* @param base the source environment to filter (usually `process.env`).
|
|
600
630
|
* @param opts.home the throwaway dir to set as `HOME`/`TMPDIR`.
|
|
601
631
|
* @param opts.allow extra var NAMES to pass through (e.g. the `VIGILES_*` keys
|
|
602
|
-
* the eval already injects). Layered ON TOP of the
|
|
632
|
+
* the eval already injects). Layered ON TOP of the harness's `keep`; a
|
|
633
|
+
* session-identity name is dropped even when listed here.
|
|
603
634
|
*/
|
|
604
635
|
export declare function ephemeralRunEnv(base: NodeJS.ProcessEnv | Record<string, string | undefined>, opts: {
|
|
605
636
|
home: string;
|
|
606
637
|
allow?: readonly string[];
|
|
607
638
|
}): Record<string, string>;
|
|
608
|
-
/**
|
|
609
|
-
* Home-relative AUTH files to carry from the real HOME into the throwaway one.
|
|
610
|
-
*
|
|
611
|
-
* A local subscription credential (OAuth token) often lives in a FILE under HOME,
|
|
612
|
-
* not an env var — so scrubbing HOME would lose it and break a local-authed run.
|
|
613
|
-
* This is the file half of the auth allowlist; {@link EPHEMERAL_ALLOW} covers the
|
|
614
|
-
* env-var / host-brokered half. Kept a named constant so it's easy to extend, and
|
|
615
|
-
* deliberately NARROW — only the explicit auth files, never `.gitconfig` / `.ssh`
|
|
616
|
-
* / `.aws`, which are exactly what an ephemeral run must not see.
|
|
617
|
-
*/
|
|
618
|
-
export declare const EPHEMERAL_HOME_KEEP: readonly string[];
|
|
619
639
|
/**
|
|
620
640
|
* Seed the throwaway HOME with the harness's own auth FILE(s) — best-effort;
|
|
621
|
-
* covers local file-based OAuth; the env-var/host-brokered path is covered by
|
|
622
|
-
*
|
|
641
|
+
* covers local file-based OAuth; the env-var/host-brokered path is covered by
|
|
642
|
+
* `ephemeralRunEnv`. The files are the runtime's declared `keepHomeFiles`.
|
|
623
643
|
*
|
|
624
|
-
* COPIES (never symlinks) each
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
644
|
+
* COPIES (never symlinks) each path from `realHome` into `throwawayHome`,
|
|
645
|
+
* creating parent dirs as needed; a symlink would let the model-driven run write
|
|
646
|
+
* back to the real credential file, defeating ephemerality. A path that doesn't
|
|
647
|
+
* exist in the real HOME is skipped silently (that user auths via env-var / host
|
|
648
|
+
* broker instead). Pure fs — no env, no spawn.
|
|
629
649
|
*/
|
|
630
650
|
export declare function seedEphemeralHome(throwawayHome: string, realHome: string, keep?: readonly string[]): void;
|
|
631
651
|
/** Whether a run's captured output looks like a rate-limit / overload. Pure. */
|
|
@@ -744,7 +764,10 @@ export interface TriggerRateSpec {
|
|
|
744
764
|
* false-negative recall. Default `"sonnet"`. Lower it deliberately for a cheap run.
|
|
745
765
|
*/
|
|
746
766
|
readonly minModel?: string;
|
|
747
|
-
/**
|
|
767
|
+
/**
|
|
768
|
+
* Tools pre-approved for the run (`--allowedTools`); `[]` approves none.
|
|
769
|
+
* Default: Read Edit Write Bash Skill.
|
|
770
|
+
*/
|
|
748
771
|
readonly allowedTools?: readonly string[];
|
|
749
772
|
/** Per-run timeout ms. Default 240000. */
|
|
750
773
|
readonly timeoutMs?: number;
|
package/dist/eval.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.claudeCodeLiveDriver = exports.claudeEvalDriver = exports.
|
|
3
|
+
exports.claudeCodeLiveDriver = exports.claudeEvalDriver = exports.spawnAgent = exports.EFFORT_ENV_VAR = void 0;
|
|
4
4
|
exports.resolveSpawnEnv = resolveSpawnEnv;
|
|
5
5
|
exports.pinEffortEnv = pinEffortEnv;
|
|
6
6
|
exports.effortRejection = effortRejection;
|
|
7
7
|
exports.withEffortGuard = withEffortGuard;
|
|
8
8
|
exports.buildAgentArgs = buildAgentArgs;
|
|
9
|
+
exports.startFailure = startFailure;
|
|
10
|
+
exports.withStartGuard = withStartGuard;
|
|
9
11
|
exports.unregisteredSkillFiles = unregisteredSkillFiles;
|
|
10
12
|
exports.runEval = runEval;
|
|
11
13
|
exports.measureWith = measureWith;
|
|
@@ -74,6 +76,7 @@ const node_path_1 = require("node:path");
|
|
|
74
76
|
const observe_js_1 = require("./observe.js");
|
|
75
77
|
const plugin_loader_js_1 = require("./adapters/claude-code/plugin-loader.js");
|
|
76
78
|
const runtime_js_1 = require("./adapters/claude-code/runtime.js");
|
|
79
|
+
const run_env_js_1 = require("./core/run-env.js");
|
|
77
80
|
const eval_cost_js_1 = require("./eval-cost.js");
|
|
78
81
|
const proofs_js_1 = require("./core/proofs.js");
|
|
79
82
|
const model_access_js_1 = require("./adapters/claude-code/model-access.js");
|
|
@@ -88,6 +91,14 @@ const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
|
88
91
|
const tool_stub_js_1 = require("./tool-stub.js");
|
|
89
92
|
const tmp_root_js_1 = require("./core/tmp-root.js");
|
|
90
93
|
const edit_distance_js_1 = require("./core/edit-distance.js");
|
|
94
|
+
/**
|
|
95
|
+
* The harness the eval tier spawns. ONE binding on purpose: the paid tier is
|
|
96
|
+
* still wired to the Claude Code runner rather than chosen through the adapter's
|
|
97
|
+
* eval driver (#325), and every fact this file needs about that harness — its
|
|
98
|
+
* binary, its version key, which env a child may inherit — is read off this
|
|
99
|
+
* runtime's port fields, never spelled here.
|
|
100
|
+
*/
|
|
101
|
+
const EVAL_RUNTIME = runtime_js_1.claudeCodeRuntime;
|
|
91
102
|
function writeFiles(cwd, files) {
|
|
92
103
|
for (const [p, content] of Object.entries(files)) {
|
|
93
104
|
const full = (0, node_path_1.resolve)(cwd, p);
|
|
@@ -99,15 +110,17 @@ function writeFiles(cwd, files) {
|
|
|
99
110
|
* Resolve the environment a trial's subprocess actually runs with — the
|
|
100
111
|
* SECURITY-CRITICAL decision behind `ephemeralEnv`. When `replaceEnv` is set, the
|
|
101
112
|
* scrubbed `env` is the COMPLETE environment, so the real `$HOME` and inherited
|
|
102
|
-
* secrets are DROPPED; otherwise `env` is an overlay on `base
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
113
|
+
* secrets are DROPPED; otherwise `env` is an overlay on `base`. In BOTH, the
|
|
114
|
+
* harness's declared session identity (`runEnv.sessionIdentity`) is removed: a
|
|
115
|
+
* run started from inside a live session must not run as that session, scrubbed
|
|
116
|
+
* or not. Extracted from the `v8 ignore`d `spawnAgent` so the one line that
|
|
117
|
+
* enforces the scrub is both unit- and behaviourally-tested — a regression to an
|
|
118
|
+
* always-merge would otherwise silently defeat ephemerality and leak the host
|
|
119
|
+
* environment into an untrusted, model-driven run.
|
|
107
120
|
*/
|
|
108
121
|
function resolveSpawnEnv(a, base = process.env) {
|
|
109
122
|
const resolved = a.replaceEnv ? (a.env ?? {}) : { ...base, ...a.env };
|
|
110
|
-
return pinEffortEnv(resolved, a.effort);
|
|
123
|
+
return pinEffortEnv((0, run_env_js_1.withoutSessionIdentity)(resolved, EVAL_RUNTIME.runEnv), a.effort);
|
|
111
124
|
}
|
|
112
125
|
/**
|
|
113
126
|
* The env var name the harness reads for the reasoning budget. It sits ABOVE the
|
|
@@ -121,10 +134,10 @@ exports.EFFORT_ENV_VAR = "CLAUDE_CODE_EFFORT_LEVEL";
|
|
|
121
134
|
*
|
|
122
135
|
* WHY THIS EXISTS AND WHY IT IS NOT OPTIONAL. Effort has THREE inputs — the
|
|
123
136
|
* `--effort` flag, the `effortLevel` settings key, and `CLAUDE_CODE_EFFORT_LEVEL`
|
|
124
|
-
* — and the env var wins over the flag.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
137
|
+
* — and the env var wins over the flag. The default (inherited) spawn env
|
|
138
|
+
* carries the caller's whole environment, so an ambient
|
|
139
|
+
* `CLAUDE_CODE_EFFORT_LEVEL=max` in the author's shell reaches the run unless
|
|
140
|
+
* something removes it. Without this pin,
|
|
128
141
|
* hashing effort into the lock would make the lock CONFIDENTLY WRONG: it would
|
|
129
142
|
* record `low` over a run that executed at `max` — the exact defect the feature
|
|
130
143
|
* exists to prevent, reintroduced by the fix for it.
|
|
@@ -215,10 +228,41 @@ function buildAgentArgs(a) {
|
|
|
215
228
|
? ["--plugin-dir", (0, node_path_1.resolve)(a.pluginDir)]
|
|
216
229
|
: []),
|
|
217
230
|
...(a.hasSettings ? ["--settings", "settings.json"] : []),
|
|
218
|
-
|
|
219
|
-
|
|
231
|
+
// A permission allowlist: an empty one approves nothing, so the flag is
|
|
232
|
+
// left out (a bare `--allowedTools` exits before any model turn).
|
|
233
|
+
...(a.tools.length === 0 ? [] : ["--allowedTools", ...a.tools]),
|
|
220
234
|
];
|
|
221
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Why a run never reached the model, or null when it did. A run that exited
|
|
238
|
+
* non-zero with no `assistant` event and no `result` event asked the model
|
|
239
|
+
* nothing — a usage error in the command line, a missing binary — and scored as
|
|
240
|
+
* a trial it reads as a miss on every check that needs output and a pass on
|
|
241
|
+
* every bound it cannot break. A run that reached the model and then broke is a
|
|
242
|
+
* scored outcome, not this. Rate-limited runs are left to the retry loop.
|
|
243
|
+
*/
|
|
244
|
+
function startFailure(out) {
|
|
245
|
+
if (out.code === 0 || (0, harness_test_js_1.parseResultEvent)(out.stdout) !== null)
|
|
246
|
+
return null;
|
|
247
|
+
if ((0, harness_test_js_1.reachedModel)(out.stdout) || isRateLimited(out))
|
|
248
|
+
return null;
|
|
249
|
+
const said = (out.stderr ?? "").trim().split("\n").slice(-5).join("\n");
|
|
250
|
+
return `the harness exited ${String(out.code)} before calling the model: ${said || "(no stderr)"}`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Wrap a runner so a run that never reached the model THROWS instead of being
|
|
254
|
+
* scored — the same choice, for the same reason, as {@link withEffortGuard}:
|
|
255
|
+
* the cause is usually deterministic, so every trial would fail it. Not `async`,
|
|
256
|
+
* for the reason that guard gives.
|
|
257
|
+
*/
|
|
258
|
+
function withStartGuard(runner) {
|
|
259
|
+
return (a) => runner(a).then((out) => {
|
|
260
|
+
const failure = startFailure(out);
|
|
261
|
+
if (failure !== null)
|
|
262
|
+
throw new Error(failure);
|
|
263
|
+
return out;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
222
266
|
/**
|
|
223
267
|
* The real `claude`-spawning runner (composition root). Exported so other
|
|
224
268
|
* real-model entries (e.g. the `audit` trigger tier) bind the same runner.
|
|
@@ -228,7 +272,7 @@ function buildAgentArgs(a) {
|
|
|
228
272
|
* not yet written, is covered by construction. Guarding each call site instead is
|
|
229
273
|
* the shape that left four of five compilers unprotected in #173.
|
|
230
274
|
*/
|
|
231
|
-
exports.spawnAgent = withEffortGuard(spawnAgentRaw);
|
|
275
|
+
exports.spawnAgent = withEffortGuard(withStartGuard(spawnAgentRaw));
|
|
232
276
|
/* v8 ignore start -- real claude subprocess; exercised by bench/, not the unit gate */
|
|
233
277
|
/** The unguarded spawn itself; wrapped by {@link spawnAgent}, never bound raw. */
|
|
234
278
|
function spawnAgentRaw(a) {
|
|
@@ -236,7 +280,7 @@ function spawnAgentRaw(a) {
|
|
|
236
280
|
(0, foreign_runner_js_1.refuseUnderForeignRunner)("spawning `claude`");
|
|
237
281
|
return new Promise((resolvePromise) => {
|
|
238
282
|
const args = buildAgentArgs(a);
|
|
239
|
-
const child = (0, node_child_process_1.spawn)(
|
|
283
|
+
const child = (0, node_child_process_1.spawn)(EVAL_RUNTIME.agentBinary, args, {
|
|
240
284
|
cwd: a.cwd,
|
|
241
285
|
// The security-critical env resolution (overlay vs. scrubbed replacement)
|
|
242
286
|
// lives in the tested `resolveSpawnEnv` seam above, not inline here.
|
|
@@ -572,6 +616,7 @@ function parseClaudeRun(out) {
|
|
|
572
616
|
toolCalls: (0, harness_test_js_1.parseToolCalls)(out.stdout),
|
|
573
617
|
hooks: (0, harness_test_js_1.parseHooks)(out.stdout),
|
|
574
618
|
subagents: (0, harness_test_js_1.parseSubagents)(out.stdout),
|
|
619
|
+
replies: (0, harness_test_js_1.parseReplies)(out.stdout),
|
|
575
620
|
usage: usageFrom(result),
|
|
576
621
|
};
|
|
577
622
|
}
|
|
@@ -592,6 +637,7 @@ function makeContext(cwd, out, parse = parseClaudeRun) {
|
|
|
592
637
|
hooks: p.hooks,
|
|
593
638
|
output: p.output,
|
|
594
639
|
subagents: p.subagents,
|
|
640
|
+
...(p.replies === undefined ? {} : { replies: p.replies }),
|
|
595
641
|
usage: p.usage,
|
|
596
642
|
// The eval tier drives the real API (no mock between the agent and the model),
|
|
597
643
|
// so the requests can't be captured here — modelRequests is harness-tier only.
|
|
@@ -755,7 +801,10 @@ async function runWithCache(runArgs, keyParts, runner, cfg) {
|
|
|
755
801
|
trialIndex: keyParts.trialIndex,
|
|
756
802
|
});
|
|
757
803
|
const hit = (0, eval_cache_js_1.readCache)(cfg.cacheDir, key);
|
|
758
|
-
|
|
804
|
+
// A run that never reached the model is not replayed: the real runner now
|
|
805
|
+
// throws on one, and an entry cached before that guard existed would
|
|
806
|
+
// otherwise be scored as a trial on every later run.
|
|
807
|
+
if (hit && startFailure(hit.out) === null) {
|
|
759
808
|
(0, eval_cache_js_1.restoreDir)(runArgs.cwd, hit.files);
|
|
760
809
|
return hit.out;
|
|
761
810
|
}
|
|
@@ -828,7 +877,7 @@ let cachedHarnessVersion;
|
|
|
828
877
|
function harnessVersion() {
|
|
829
878
|
if (cachedHarnessVersion === undefined) {
|
|
830
879
|
try {
|
|
831
|
-
cachedHarnessVersion =
|
|
880
|
+
cachedHarnessVersion = EVAL_RUNTIME.versionKey((0, node_child_process_1.execSync)(`${EVAL_RUNTIME.agentBinary} --version`, {
|
|
832
881
|
encoding: "utf-8",
|
|
833
882
|
stdio: ["ignore", "pipe", "ignore"],
|
|
834
883
|
}));
|
|
@@ -875,132 +924,68 @@ function withInterceptToolHook(settings, intercepts) {
|
|
|
875
924
|
},
|
|
876
925
|
};
|
|
877
926
|
}
|
|
878
|
-
/**
|
|
879
|
-
* The default allowlist `ephemeralRunEnv` passes through from the real
|
|
880
|
-
* environment. Two groups, both load-bearing for a real-model `claude` run:
|
|
881
|
-
*
|
|
882
|
-
* - **Auth** — the harness's OWN credentials. The eval drives the real `claude`
|
|
883
|
-
* CLI, which authenticates via the user's subscription (`~/.claude`, reached
|
|
884
|
-
* through the fresh HOME's allowed config — see below) OR via these env vars.
|
|
885
|
-
* We mirror the auth surface the runtime port already names
|
|
886
|
-
* (`ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`, see
|
|
887
|
-
* `adapters/claude-code/runtime.ts`) plus the OAuth/token + region variants the
|
|
888
|
-
* CLI accepts, so a token-authed user is not broken by a too-narrow list.
|
|
889
|
-
* - **Runtime** — what any spawned process needs to *function*: `PATH` (resolve
|
|
890
|
-
* `node` / `claude`), the locale/terminal vars (`LANG` / `LC_*` / `TERM`), and
|
|
891
|
-
* `TMPDIR` (which we override to the fresh HOME). Mirrors what `bwrapArgs` /
|
|
892
|
-
* `setenvArgs` in `src/sandbox.ts` set back after `--clearenv`.
|
|
893
|
-
*
|
|
894
|
-
* Notably it does NOT pass through `GIT_*`, `GH_TOKEN`, `SSH_*`, `AWS_*`, or any
|
|
895
|
-
* other non-allowlisted secret-shaped var — those are exactly what an ephemeral
|
|
896
|
-
* run must not see. `CLAUDE_*` is allowlisted by prefix because the CLI reads
|
|
897
|
-
* several `CLAUDE_*` knobs (config dir, etc.) and omitting one is the failure
|
|
898
|
-
* mode this whole guard is conservative against.
|
|
899
|
-
*
|
|
900
|
-
* Conservative by design: a too-broad allowlist is safe (it just leaks a benign
|
|
901
|
-
* var); a too-narrow one silently breaks auth — which is why the feature ships
|
|
902
|
-
* default-OFF until validated against a real run.
|
|
903
|
-
*/
|
|
904
|
-
const EPHEMERAL_ALLOW = [
|
|
905
|
-
// Runtime essentials (mirror sandbox.ts setenv-after-clearenv).
|
|
906
|
-
"PATH",
|
|
907
|
-
"LANG",
|
|
908
|
-
"TERM",
|
|
909
|
-
// Anthropic / Claude Code auth + endpoint (mirror runtime.ts + CLI auth vars).
|
|
910
|
-
runtime_js_1.claudeCodeRuntime.modelApiKeyEnv, // ANTHROPIC_API_KEY
|
|
911
|
-
runtime_js_1.claudeCodeRuntime.modelBaseUrlEnv, // ANTHROPIC_BASE_URL
|
|
912
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
913
|
-
"ANTHROPIC_API_URL",
|
|
914
|
-
"ANTHROPIC_MODEL",
|
|
915
|
-
"ANTHROPIC_DEFAULT_HEADERS",
|
|
916
|
-
"CLAUDE_CODE_USE_BEDROCK",
|
|
917
|
-
"CLAUDE_CODE_USE_VERTEX",
|
|
918
|
-
// Cloud-provider auth the CLI uses for Bedrock/Vertex backends (region/profile
|
|
919
|
-
// only — NOT the secret-shaped AWS_* access keys, which stay dropped).
|
|
920
|
-
"AWS_REGION",
|
|
921
|
-
"AWS_DEFAULT_REGION",
|
|
922
|
-
"AWS_PROFILE",
|
|
923
|
-
"CLOUD_ML_REGION",
|
|
924
|
-
"GOOGLE_CLOUD_PROJECT",
|
|
925
|
-
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
926
|
-
];
|
|
927
|
-
/** Prefixes passed through wholesale — the CLI reads several `CLAUDE_*` knobs and
|
|
928
|
-
* a `LC_*` locale family; allowlist by prefix so omitting one isn't the silent
|
|
929
|
-
* auth/locale break this guard exists to avoid. */
|
|
930
|
-
const EPHEMERAL_ALLOW_PREFIXES = ["CLAUDE_", "LC_"];
|
|
931
927
|
/**
|
|
932
928
|
* Build an **ephemeral run environment** for a model-driven run: a NEW env object
|
|
933
|
-
* with a *fresh* `HOME` (and `TMPDIR`) pointed at the throwaway `opts.home`,
|
|
934
|
-
*
|
|
935
|
-
* else
|
|
929
|
+
* with a *fresh* `HOME` (and `TMPDIR`) pointed at the throwaway `opts.home`, the
|
|
930
|
+
* OS essentials (`PATH`, `LANG`, `LC_*`, `TERM`), the harness's OWN auth, and
|
|
931
|
+
* nothing else. Pure — no fs, no spawn.
|
|
936
932
|
*
|
|
937
933
|
* The rationale is "fresh HOME + only the harness credential injected, **not** a
|
|
938
934
|
* blanket wipe": running a model-driven skill/agent is itself a side effect (the
|
|
939
935
|
* *model*, not the author, chose the actions), so it should not be able to read
|
|
940
936
|
* the real `~/.gitconfig` / `~/.ssh` / `~/.aws` or write to the real `~`. But the
|
|
941
|
-
* real
|
|
942
|
-
* (
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
937
|
+
* real CLI must still AUTHENTICATE, so the auth names the harness declares
|
|
938
|
+
* (`runtime.runEnv.keep`) are re-injected, and the parent session's identity
|
|
939
|
+
* (`runtime.runEnv.sessionIdentity`) never is. Which names those are is the
|
|
940
|
+
* adapter's fact, not this file's: see `RunEnvPolicy` in `core/runtime.ts`.
|
|
941
|
+
* Because this needs no kernel features, it is the cross-platform
|
|
942
|
+
* STATE-protection floor (lands on macOS immediately), orthogonal to the Linux
|
|
943
|
+
* bubblewrap HOST confinement in `src/sandbox.ts`.
|
|
944
|
+
*
|
|
945
|
+
* Notably it does NOT pass through `GIT_*`, `GH_TOKEN`, `SSH_*`, `AWS_*` access
|
|
946
|
+
* keys, a config-dir override, or any other undeclared variable — those are
|
|
947
|
+
* exactly what an ephemeral run must not see. A too-narrow `keep` breaks auth
|
|
948
|
+
* loudly (the run fails to start); a too-broad one leaks silently, which is why
|
|
949
|
+
* it is an allowlist and not a prefix.
|
|
947
950
|
*
|
|
948
951
|
* @param base the source environment to filter (usually `process.env`).
|
|
949
952
|
* @param opts.home the throwaway dir to set as `HOME`/`TMPDIR`.
|
|
950
953
|
* @param opts.allow extra var NAMES to pass through (e.g. the `VIGILES_*` keys
|
|
951
|
-
* the eval already injects). Layered ON TOP of the
|
|
954
|
+
* the eval already injects). Layered ON TOP of the harness's `keep`; a
|
|
955
|
+
* session-identity name is dropped even when listed here.
|
|
952
956
|
*/
|
|
953
957
|
function ephemeralRunEnv(base, opts) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
...
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const allowed = allowExact.has(k) ||
|
|
963
|
-
EPHEMERAL_ALLOW_PREFIXES.some((p) => k.startsWith(p));
|
|
964
|
-
if (allowed)
|
|
965
|
-
out[k] = v;
|
|
966
|
-
}
|
|
967
|
-
// Fresh HOME + TMPDIR last so they always win over anything passed through.
|
|
968
|
-
out.HOME = opts.home;
|
|
969
|
-
out.TMPDIR = opts.home;
|
|
970
|
-
return out;
|
|
958
|
+
// A fresh object: the trial layers its overlay and stub PATH onto it.
|
|
959
|
+
return {
|
|
960
|
+
...(0, run_env_js_1.scrubbedRunEnv)(base, {
|
|
961
|
+
home: opts.home,
|
|
962
|
+
policy: EVAL_RUNTIME.runEnv,
|
|
963
|
+
allow: opts.allow,
|
|
964
|
+
}),
|
|
965
|
+
};
|
|
971
966
|
}
|
|
972
|
-
/**
|
|
973
|
-
* Home-relative AUTH files to carry from the real HOME into the throwaway one.
|
|
974
|
-
*
|
|
975
|
-
* A local subscription credential (OAuth token) often lives in a FILE under HOME,
|
|
976
|
-
* not an env var — so scrubbing HOME would lose it and break a local-authed run.
|
|
977
|
-
* This is the file half of the auth allowlist; {@link EPHEMERAL_ALLOW} covers the
|
|
978
|
-
* env-var / host-brokered half. Kept a named constant so it's easy to extend, and
|
|
979
|
-
* deliberately NARROW — only the explicit auth files, never `.gitconfig` / `.ssh`
|
|
980
|
-
* / `.aws`, which are exactly what an ephemeral run must not see.
|
|
981
|
-
*/
|
|
982
|
-
exports.EPHEMERAL_HOME_KEEP = [
|
|
983
|
-
".claude/.credentials.json", // the Claude Code OAuth token
|
|
984
|
-
];
|
|
985
967
|
/**
|
|
986
968
|
* Seed the throwaway HOME with the harness's own auth FILE(s) — best-effort;
|
|
987
|
-
* covers local file-based OAuth; the env-var/host-brokered path is covered by
|
|
988
|
-
*
|
|
969
|
+
* covers local file-based OAuth; the env-var/host-brokered path is covered by
|
|
970
|
+
* `ephemeralRunEnv`. The files are the runtime's declared `keepHomeFiles`.
|
|
989
971
|
*
|
|
990
|
-
* COPIES (never symlinks) each
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
972
|
+
* COPIES (never symlinks) each path from `realHome` into `throwawayHome`,
|
|
973
|
+
* creating parent dirs as needed; a symlink would let the model-driven run write
|
|
974
|
+
* back to the real credential file, defeating ephemerality. A path that doesn't
|
|
975
|
+
* exist in the real HOME is skipped silently (that user auths via env-var / host
|
|
976
|
+
* broker instead). Pure fs — no env, no spawn.
|
|
995
977
|
*/
|
|
996
|
-
function seedEphemeralHome(throwawayHome, realHome, keep =
|
|
978
|
+
function seedEphemeralHome(throwawayHome, realHome, keep = EVAL_RUNTIME.runEnv?.keepHomeFiles ?? []) {
|
|
997
979
|
for (const rel of keep) {
|
|
998
980
|
const src = (0, node_path_1.join)(realHome, rel);
|
|
999
981
|
if (!(0, node_fs_1.existsSync)(src))
|
|
1000
982
|
continue; // env-var / host-brokered auth covers this.
|
|
1001
983
|
const dest = (0, node_path_1.join)(throwawayHome, rel);
|
|
1002
984
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
|
|
1003
|
-
|
|
985
|
+
// The DATA, never a link: `cpSync` recreates a symlinked credential
|
|
986
|
+
// (dotfiles, a secret manager) as a link, and the run would write through
|
|
987
|
+
// it into the real file. `copyFileSync` always copies the contents.
|
|
988
|
+
(0, node_fs_1.copyFileSync)(src, dest);
|
|
1004
989
|
}
|
|
1005
990
|
}
|
|
1006
991
|
/** Execute one trial in a fresh sandbox; returns its metric row + usage. */
|
|
@@ -1122,14 +1107,25 @@ async function runWithRetry(runArgs, runner, retries, baseMs) {
|
|
|
1122
1107
|
/** Map `worker` over `items` with at most `concurrency` in flight, order preserved. */
|
|
1123
1108
|
async function runPool(items, concurrency, worker) {
|
|
1124
1109
|
const results = new Array(items.length);
|
|
1110
|
+
// A worker that throws rejects the whole pool; the others must stop taking
|
|
1111
|
+
// items then, or they keep starting (and paying for) work nobody will read.
|
|
1112
|
+
const stop = new AbortController();
|
|
1125
1113
|
let next = 0;
|
|
1126
1114
|
const drain = async () => {
|
|
1127
1115
|
for (;;) {
|
|
1116
|
+
if (stop.signal.aborted)
|
|
1117
|
+
return;
|
|
1128
1118
|
const i = next++;
|
|
1129
1119
|
const item = items[i];
|
|
1130
1120
|
if (i >= items.length || item === undefined)
|
|
1131
1121
|
return;
|
|
1132
|
-
|
|
1122
|
+
try {
|
|
1123
|
+
results[i] = await worker(item);
|
|
1124
|
+
}
|
|
1125
|
+
catch (e) {
|
|
1126
|
+
stop.abort();
|
|
1127
|
+
throw e;
|
|
1128
|
+
}
|
|
1133
1129
|
}
|
|
1134
1130
|
};
|
|
1135
1131
|
const workers = Math.max(1, Math.min(concurrency, items.length || 1));
|