vigiles 35.0.0 → 36.0.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.
|
@@ -7,6 +7,9 @@ import type { HarnessTestDriver, HarnessDriverContext, ParsedRun } from "../../c
|
|
|
7
7
|
* `--dangerously-bypass-approvals-and-sandbox` make it run unattended;
|
|
8
8
|
* `--skip-git-repo-check` lets it run in a bare temp dir; `--ignore-user-config`
|
|
9
9
|
* keeps the host's `~/.codex` out. The prompt is the trailing positional.
|
|
10
|
+
* `ctx.allowedTools` is ignored: `codex exec` runs with approvals bypassed, so
|
|
11
|
+
* everything is already approved. `ctx.tools` is refused (throws): Codex has no
|
|
12
|
+
* per-run tool availability.
|
|
10
13
|
* Pure, so the arg shape is unit-tested.
|
|
11
14
|
*/
|
|
12
15
|
export declare function buildCodexArgs(ctx: HarnessDriverContext): string[];
|
|
@@ -26,9 +26,15 @@ const mock_model_js_1 = require("./mock-model.js");
|
|
|
26
26
|
* `--dangerously-bypass-approvals-and-sandbox` make it run unattended;
|
|
27
27
|
* `--skip-git-repo-check` lets it run in a bare temp dir; `--ignore-user-config`
|
|
28
28
|
* keeps the host's `~/.codex` out. The prompt is the trailing positional.
|
|
29
|
+
* `ctx.allowedTools` is ignored: `codex exec` runs with approvals bypassed, so
|
|
30
|
+
* everything is already approved. `ctx.tools` is refused (throws): Codex has no
|
|
31
|
+
* per-run tool availability.
|
|
29
32
|
* Pure, so the arg shape is unit-tested.
|
|
30
33
|
*/
|
|
31
34
|
function buildCodexArgs(ctx) {
|
|
35
|
+
if (ctx.tools !== undefined) {
|
|
36
|
+
throw new Error("tools is not supported for codex: it has no per-run tool availability, and a list that withheld nothing would let a test pass over a run where nothing was withheld. Drop tools, or assert on what the agent did.");
|
|
37
|
+
}
|
|
32
38
|
return [
|
|
33
39
|
"exec",
|
|
34
40
|
"--ignore-user-config",
|
|
@@ -108,10 +108,17 @@ export interface HarnessDriverContext {
|
|
|
108
108
|
/** Whether a settings file was written (CC: pass `--settings`). */
|
|
109
109
|
readonly hasSettings: boolean;
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
111
|
+
* Which tools EXIST in the session, when the spec lists them (CC: `--tools`);
|
|
112
|
+
* undefined = every tool exists. A driver whose harness has no such concept
|
|
113
|
+
* (Codex) throws on one rather than ignore it.
|
|
113
114
|
*/
|
|
114
115
|
readonly tools?: readonly string[];
|
|
116
|
+
/**
|
|
117
|
+
* Which tools are PRE-APPROVED (CC: `--allowedTools`); undefined = the
|
|
118
|
+
* default four. It never removes a tool. A driver with no such concept
|
|
119
|
+
* ignores it.
|
|
120
|
+
*/
|
|
121
|
+
readonly allowedTools?: readonly string[];
|
|
115
122
|
/** Capture the full event transcript instead of just the final result. */
|
|
116
123
|
readonly transcript: boolean;
|
|
117
124
|
/** Path to a plugin dir to install natively, if any. */
|
package/dist/harness-test.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HarnessAdapter } from "./core/adapter.js";
|
|
2
|
-
import type { HarnessTestDriver, ToolCall, HookFire, ParsedRun, ModelTurn } from "./core/harness-driver.js";
|
|
2
|
+
import type { HarnessTestDriver, ToolCall, HookFire, ParsedRun, ModelTurn, ModelRequest } from "./core/harness-driver.js";
|
|
3
3
|
import type { Trace, SubagentTrace } from "./core/eval-driver.js";
|
|
4
4
|
export type { Trace, SubagentTrace } from "./core/eval-driver.js";
|
|
5
5
|
import { parseReplies, reachedModel } from "./adapters/claude-code/replies.js";
|
|
@@ -53,16 +53,36 @@ export interface HarnessTestSpec {
|
|
|
53
53
|
/** The user prompt. Default: "go". */
|
|
54
54
|
readonly prompt?: string;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
56
|
+
* Which tools EXIST in the session (`claude --tools`, Claude Code 2.0.31+). A
|
|
57
|
+
* tool left out is not offered at all, so a call to it is "No such tool
|
|
58
|
+
* available". Omitted, every tool exists. This is the field that withholds a
|
|
59
|
+
* tool. Names only: a permission rule such as `Bash(git *)` is refused (put the
|
|
60
|
+
* rule in `allowedTools`). `[]` is refused (an agent with no tools is never
|
|
61
|
+
* served a scripted turn), and a scripted call to a tool the list leaves out
|
|
62
|
+
* throws before the run starts. An MCP tool is not a built-in, so `--tools`
|
|
63
|
+
* does not withhold it. Codex has no equivalent, so a run there refuses it.
|
|
64
|
+
*
|
|
65
|
+
* | field | Claude Code flag | meaning |
|
|
66
|
+
* | -------------- | ----------------- | ---------------------------- |
|
|
67
|
+
* | `tools` | `--tools` | which tools exist |
|
|
68
|
+
* | `allowedTools` | `--allowedTools` | which are pre-approved |
|
|
69
|
+
*/
|
|
70
|
+
readonly tools?: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* Which tools are PRE-APPROVED (`claude --allowedTools`), so they do not stop
|
|
73
|
+
* on a permission prompt. Takes permission rules (`Bash(git *)`). Omitted:
|
|
74
|
+
* Read, Edit, Write and Bash are approved. An explicit list REPLACES that
|
|
75
|
+
* default, so `tools: ["Read", "Write"]` with `allowedTools: ["Skill"]` leaves
|
|
76
|
+
* Write unapproved. `[]`: nothing is approved.
|
|
77
|
+
*
|
|
78
|
+
* It never removes a tool, and leaving a tool out is not a fence: in headless
|
|
79
|
+
* mode Claude Code refuses only the calls it would have asked about. For Bash
|
|
80
|
+
* that is a command that creates, changes or removes a file (`touch`, `>`),
|
|
81
|
+
* inside or outside the working directory; a command with no file effect
|
|
82
|
+
* (`ls`, `printf`, `id`) runs unapproved. So a refused `touch` proves the
|
|
83
|
+
* file-change guard is live, not that Bash is withheld. To withhold a tool use
|
|
84
|
+
* `tools`; to forbid its use put a deny rule in the fixture's
|
|
85
|
+
* `settings.permissions.deny`.
|
|
66
86
|
*/
|
|
67
87
|
readonly allowedTools?: readonly string[];
|
|
68
88
|
/**
|
|
@@ -131,20 +151,34 @@ export declare function parseResultEvent(stdout: string): Record<string, unknown
|
|
|
131
151
|
export declare function parseOutput(stdout: string): string;
|
|
132
152
|
export declare function parseHooks(stdout: string): HookFire[];
|
|
133
153
|
/**
|
|
134
|
-
* The names `--tools`
|
|
135
|
-
* takes permission rules (`Bash(git *)`); `--tools` takes tool
|
|
136
|
-
*
|
|
137
|
-
*
|
|
154
|
+
* The names `--tools` takes for a `tools` list: trimmed and deduplicated.
|
|
155
|
+
* `--allowedTools` takes permission rules (`Bash(git *)`); `--tools` takes tool
|
|
156
|
+
* NAMES, and a rule there makes the CLI offer no tools at all. Reducing it to
|
|
157
|
+
* `Bash` would offer more than the author wrote, so a rule is refused: it
|
|
158
|
+
* belongs in `allowedTools`. Pure.
|
|
138
159
|
*/
|
|
139
|
-
export declare function toolAvailabilityList(
|
|
160
|
+
export declare function toolAvailabilityList(tools: readonly string[]): readonly string[];
|
|
140
161
|
/**
|
|
141
|
-
* A scripted call to a tool an EXPLICIT `
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
162
|
+
* A scripted call to a tool an EXPLICIT `tools` list leaves out. The CLI answers
|
|
163
|
+
* it with "No such tool available" and the run goes on, so a test that scripts
|
|
164
|
+
* the call and then asserts on something else passes over a step that never
|
|
165
|
+
* happened. A tool that exists but is not in `allowedTools` is NOT this: the CLI
|
|
166
|
+
* refuses it for permission, which is a result a test can assert on. Returns the
|
|
167
|
+
* message to throw, or undefined. MCP tools are not built-ins, so `--tools` does
|
|
168
|
+
* not withhold them and they are skipped. Pure.
|
|
146
169
|
*/
|
|
147
|
-
export declare function unofferedScriptedTool(model: readonly ModelTurn[],
|
|
170
|
+
export declare function unofferedScriptedTool(model: readonly ModelTurn[], tools: readonly string[] | undefined): string | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* The agent's own tool results, read off the requests it sent back to the model,
|
|
173
|
+
* that say a scripted tool does not exist. The CLI answers an unknown name
|
|
174
|
+
* (`Bsh` for `Bash`, an `mcp__` server that is not configured) with an error
|
|
175
|
+
* RESULT and carries on, exit 0, so a test that asserts "the call errored and
|
|
176
|
+
* left no side effect" passes on a typo. `tools` cannot catch it: the pre-flight
|
|
177
|
+
* skips `mcp__` names and only runs when `tools` is given. Side-channel requests
|
|
178
|
+
* are the CLI's own bookkeeping and are not read. Returns the message to throw,
|
|
179
|
+
* or undefined. Pure.
|
|
180
|
+
*/
|
|
181
|
+
export declare function noSuchToolMessage(model: readonly ModelTurn[], requests: readonly ModelRequest[]): string | undefined;
|
|
148
182
|
/**
|
|
149
183
|
* A run on a `claude` that predates `--tools` exits at argument parsing, and
|
|
150
184
|
* commander's "unknown option" says nothing about WHY vigiles passed it. Returns
|
|
@@ -204,6 +238,10 @@ export interface RunHarnessTestOptions {
|
|
|
204
238
|
}
|
|
205
239
|
/** Say so on stderr when side-channel calls arrived and no script turn was served. */
|
|
206
240
|
export declare function warnUnconsumed(count: number, sideChannelCount: number): void;
|
|
241
|
+
/**
|
|
242
|
+
* What makes a finished run unsound, most specific first, or undefined. Pure.
|
|
243
|
+
*/
|
|
244
|
+
export declare function runProblem(stderr: string, model: readonly ModelTurn[], modelRequests: readonly ModelRequest[], tools?: readonly string[]): string | undefined;
|
|
207
245
|
/**
|
|
208
246
|
* Run the real agent CLI against a scripted mock model, with the given fixture
|
|
209
247
|
* and settings (hooks). Deterministic — same script, same result. Adapter-driven
|
package/dist/harness-test.js
CHANGED
|
@@ -8,12 +8,14 @@ exports.parseOutput = parseOutput;
|
|
|
8
8
|
exports.parseHooks = parseHooks;
|
|
9
9
|
exports.toolAvailabilityList = toolAvailabilityList;
|
|
10
10
|
exports.unofferedScriptedTool = unofferedScriptedTool;
|
|
11
|
+
exports.noSuchToolMessage = noSuchToolMessage;
|
|
11
12
|
exports.unsupportedToolsFlag = unsupportedToolsFlag;
|
|
12
13
|
exports.buildClaudeArgs = buildClaudeArgs;
|
|
13
14
|
exports.parseClaudeRun = parseClaudeRun;
|
|
14
15
|
exports.harnessSpawnEnv = harnessSpawnEnv;
|
|
15
16
|
exports.claudeAvailable = claudeAvailable;
|
|
16
17
|
exports.warnUnconsumed = warnUnconsumed;
|
|
18
|
+
exports.runProblem = runProblem;
|
|
17
19
|
exports.runHarnessTest = runHarnessTest;
|
|
18
20
|
exports.runHarnessTestIn = runHarnessTestIn;
|
|
19
21
|
exports.runHarness = runHarness;
|
|
@@ -287,32 +289,66 @@ function parseHooks(stdout) {
|
|
|
287
289
|
*/
|
|
288
290
|
const TOOLS_FLAG_MIN_CLAUDE = "2.0.31";
|
|
289
291
|
/**
|
|
290
|
-
* The names `--tools`
|
|
291
|
-
* takes permission rules (`Bash(git *)`); `--tools` takes tool
|
|
292
|
-
*
|
|
293
|
-
*
|
|
292
|
+
* The names `--tools` takes for a `tools` list: trimmed and deduplicated.
|
|
293
|
+
* `--allowedTools` takes permission rules (`Bash(git *)`); `--tools` takes tool
|
|
294
|
+
* NAMES, and a rule there makes the CLI offer no tools at all. Reducing it to
|
|
295
|
+
* `Bash` would offer more than the author wrote, so a rule is refused: it
|
|
296
|
+
* belongs in `allowedTools`. Pure.
|
|
294
297
|
*/
|
|
295
|
-
function toolAvailabilityList(
|
|
296
|
-
const names =
|
|
297
|
-
|
|
298
|
+
function toolAvailabilityList(tools) {
|
|
299
|
+
const names = tools.map((entry) => entry.trim()).filter((n) => n !== "");
|
|
300
|
+
const rule = names.find((name) => name.includes("("));
|
|
301
|
+
if (rule !== undefined) {
|
|
302
|
+
throw new Error(`tools takes bare tool names, and "${rule}" is a permission rule — put the rule in allowedTools and the name ("${rule.replace(/\(.*$/, "")}") in tools`);
|
|
303
|
+
}
|
|
304
|
+
return [...new Set(names)];
|
|
298
305
|
}
|
|
299
306
|
/**
|
|
300
|
-
* A scripted call to a tool an EXPLICIT `
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
307
|
+
* A scripted call to a tool an EXPLICIT `tools` list leaves out. The CLI answers
|
|
308
|
+
* it with "No such tool available" and the run goes on, so a test that scripts
|
|
309
|
+
* the call and then asserts on something else passes over a step that never
|
|
310
|
+
* happened. A tool that exists but is not in `allowedTools` is NOT this: the CLI
|
|
311
|
+
* refuses it for permission, which is a result a test can assert on. Returns the
|
|
312
|
+
* message to throw, or undefined. MCP tools are not built-ins, so `--tools` does
|
|
313
|
+
* not withhold them and they are skipped. Pure.
|
|
305
314
|
*/
|
|
306
|
-
function unofferedScriptedTool(model,
|
|
307
|
-
if (
|
|
315
|
+
function unofferedScriptedTool(model, tools) {
|
|
316
|
+
if (tools === undefined)
|
|
308
317
|
return undefined;
|
|
309
|
-
const
|
|
318
|
+
const exist = toolAvailabilityList(tools);
|
|
310
319
|
const at = model.findIndex((t) => t.tool !== undefined &&
|
|
311
320
|
!t.tool.startsWith("mcp__") &&
|
|
312
|
-
!
|
|
321
|
+
!exist.includes(t.tool));
|
|
313
322
|
return at < 0
|
|
314
323
|
? undefined
|
|
315
|
-
: `scripted call to "${model[at]?.tool ?? ""}" on turn ${String(at + 1)}, but this run
|
|
324
|
+
: `scripted call to "${model[at]?.tool ?? ""}" on turn ${String(at + 1)}, but this run's tools are only: ${exist.join(", ")} — add it to tools`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The agent's own tool results, read off the requests it sent back to the model,
|
|
328
|
+
* that say a scripted tool does not exist. The CLI answers an unknown name
|
|
329
|
+
* (`Bsh` for `Bash`, an `mcp__` server that is not configured) with an error
|
|
330
|
+
* RESULT and carries on, exit 0, so a test that asserts "the call errored and
|
|
331
|
+
* left no side effect" passes on a typo. `tools` cannot catch it: the pre-flight
|
|
332
|
+
* skips `mcp__` names and only runs when `tools` is given. Side-channel requests
|
|
333
|
+
* are the CLI's own bookkeeping and are not read. Returns the message to throw,
|
|
334
|
+
* or undefined. Pure.
|
|
335
|
+
*/
|
|
336
|
+
function noSuchToolMessage(model, requests) {
|
|
337
|
+
// The CLI's own wording, inside its error wrapper, for a name the script
|
|
338
|
+
// calls: the agent calls tools only from the script, so the same words in the
|
|
339
|
+
// prompt or in a hook's injected text are not a result of this run.
|
|
340
|
+
const pattern = /<tool_use_error>Error: No such tool available: ([^\s<]+)<\/tool_use_error>/g;
|
|
341
|
+
const scripted = new Set(model.flatMap((t) => (t.tool === undefined ? [] : [t.tool])));
|
|
342
|
+
const hit = requests
|
|
343
|
+
.filter((r) => r.sideChannel !== true)
|
|
344
|
+
.flatMap((r) => r.messages)
|
|
345
|
+
.flatMap((m) => [...m.text.matchAll(pattern)].map((x) => x[1]))
|
|
346
|
+
.find((name) => name !== undefined && scripted.has(name));
|
|
347
|
+
if (hit === undefined)
|
|
348
|
+
return undefined;
|
|
349
|
+
const at = model.findIndex((t) => t.tool === hit);
|
|
350
|
+
const turn = at < 0 ? "" : ` on turn ${String(at + 1)}`;
|
|
351
|
+
return `scripted call to "${hit}"${turn}: the CLI answered "No such tool available: ${hit}", so the call never ran and the test would carry on over a step that did not happen. Check the name for a typo; an mcp__ tool also needs its server configured in the fixture.`;
|
|
316
352
|
}
|
|
317
353
|
/**
|
|
318
354
|
* A run on a `claude` that predates `--tools` exits at argument parsing, and
|
|
@@ -321,7 +357,7 @@ function unofferedScriptedTool(model, allowed) {
|
|
|
321
357
|
*/
|
|
322
358
|
function unsupportedToolsFlag(stderr) {
|
|
323
359
|
return /unknown option '--tools'/.test(stderr)
|
|
324
|
-
? `this \`claude\` does not know \`--tools\`, which runHarnessTest
|
|
360
|
+
? `this \`claude\` does not know \`--tools\`, which runHarnessTest passes for the \`tools\` field (which tools exist). Update Claude Code to ${TOOLS_FLAG_MIN_CLAUDE} or newer. Running without it would hand the agent every tool while the test reads as if it were restricted.`
|
|
325
361
|
: undefined;
|
|
326
362
|
}
|
|
327
363
|
/**
|
|
@@ -330,7 +366,7 @@ function unsupportedToolsFlag(stderr) {
|
|
|
330
366
|
* here. Pure, so the arg shape is unit-tested.
|
|
331
367
|
*/
|
|
332
368
|
function buildClaudeArgs(spec, hasSettings) {
|
|
333
|
-
const
|
|
369
|
+
const approved = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
|
|
334
370
|
return [
|
|
335
371
|
"-p",
|
|
336
372
|
spec.prompt ?? "go",
|
|
@@ -343,18 +379,15 @@ function buildClaudeArgs(spec, hasSettings) {
|
|
|
343
379
|
? ["--plugin-dir", (0, node_path_1.resolve)(spec.pluginDir)]
|
|
344
380
|
: []),
|
|
345
381
|
...(hasSettings ? ["--settings", "settings.json"] : []),
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
// empty list is `--tools ""` = no tools. With no list, nothing is withheld:
|
|
350
|
-
// the default four are pre-approved and every other tool stays available.
|
|
351
|
-
...(spec.allowedTools === undefined
|
|
382
|
+
// `tools` = which tools EXIST (`--tools`, the availability list). Omitted:
|
|
383
|
+
// nothing is withheld.
|
|
384
|
+
...(spec.tools === undefined
|
|
352
385
|
? []
|
|
353
|
-
: ["--tools", toolAvailabilityList(spec.
|
|
354
|
-
//
|
|
355
|
-
//
|
|
386
|
+
: ["--tools", toolAvailabilityList(spec.tools).join(",")]),
|
|
387
|
+
// `allowedTools` = which are PRE-APPROVED (`--allowedTools`); it never
|
|
388
|
+
// restricts. An empty list approves nothing, so the flag is left out (a bare
|
|
356
389
|
// `--allowedTools` exits before any model turn).
|
|
357
|
-
...(
|
|
390
|
+
...(approved.length === 0 ? [] : ["--allowedTools", ...approved]),
|
|
358
391
|
];
|
|
359
392
|
}
|
|
360
393
|
/** Build the `claude` argv from the driver context — wraps `buildClaudeArgs`. */
|
|
@@ -364,7 +397,8 @@ function buildClaudeArgsFromCtx(ctx) {
|
|
|
364
397
|
prompt: ctx.prompt,
|
|
365
398
|
transcript: ctx.transcript,
|
|
366
399
|
pluginDir: ctx.pluginDir,
|
|
367
|
-
|
|
400
|
+
tools: ctx.tools,
|
|
401
|
+
allowedTools: ctx.allowedTools,
|
|
368
402
|
}, ctx.hasSettings);
|
|
369
403
|
}
|
|
370
404
|
/** Parse the `claude` stdout/stream into the unified trace fields. */
|
|
@@ -480,15 +514,41 @@ function warnUnconsumed(count, sideChannelCount) {
|
|
|
480
514
|
if (unconsumed !== undefined)
|
|
481
515
|
console.error(unconsumed);
|
|
482
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* `tools` made only of names the CLI does not know leaves the agent with none:
|
|
519
|
+
* every request then declares no tools, the mock files it as side-channel
|
|
520
|
+
* traffic, and the script is never served, so the run decides nothing. Requests
|
|
521
|
+
* that all came in that way, under an explicit `tools`, mean exactly that. No
|
|
522
|
+
* request at all (a hook stopped the prompt) is a different run. Pure.
|
|
523
|
+
*/
|
|
524
|
+
function toolsResolvedToNothing(tools, requests) {
|
|
525
|
+
if (tools === undefined || requests.length === 0)
|
|
526
|
+
return undefined;
|
|
527
|
+
if (!requests.every((r) => r.sideChannel === true))
|
|
528
|
+
return undefined;
|
|
529
|
+
return `tools ${JSON.stringify(tools)} gave the agent no tools: no name in it is a tool this CLI knows, so no request offered a tool and the script was never served. Check the names for typos.`;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* What makes a finished run unsound, most specific first, or undefined. Pure.
|
|
533
|
+
*/
|
|
534
|
+
function runProblem(stderr, model, modelRequests, tools) {
|
|
535
|
+
return (unsupportedToolsFlag(stderr) ??
|
|
536
|
+
toolsResolvedToNothing(tools, modelRequests) ??
|
|
537
|
+
// A typo'd last turn makes the CLI ask once more, so the overrun follows
|
|
538
|
+
// from it: report the cause, not its effect.
|
|
539
|
+
noSuchToolMessage(model, modelRequests) ??
|
|
540
|
+
(0, script_overrun_js_1.overrunMessageFor)(model.length, modelRequests));
|
|
541
|
+
}
|
|
483
542
|
/**
|
|
484
543
|
* A run that proves nothing about the script fails the test instead of coming
|
|
485
|
-
* back as a result: the `claude` on PATH predates `--tools` (
|
|
544
|
+
* back as a result: the `claude` on PATH predates `--tools` (needed for `tools`), or the agent
|
|
486
545
|
* asked for a model turn the script did not have and the mock answered with an
|
|
487
|
-
* error rather than an invented turn (#340)
|
|
546
|
+
* error rather than an invented turn (#340), or a scripted call named a tool the
|
|
547
|
+
* CLI does not have. Removes the run's directory, since
|
|
488
548
|
* no result is handed back to clean it up.
|
|
489
549
|
*/
|
|
490
|
-
function assertRunSound(cwd, stderr,
|
|
491
|
-
const problem =
|
|
550
|
+
function assertRunSound(cwd, stderr, model, modelRequests, tools) {
|
|
551
|
+
const problem = runProblem(stderr, model, modelRequests, tools);
|
|
492
552
|
if (problem === undefined)
|
|
493
553
|
return;
|
|
494
554
|
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
@@ -539,16 +599,30 @@ async function runHarnessTest(spec, opts = {}) {
|
|
|
539
599
|
refuseUnrunnableSpec(spec);
|
|
540
600
|
return runHarnessTestIn(spec, opts, "inherit");
|
|
541
601
|
}
|
|
602
|
+
/** The driver's view of one run of `spec`. Pure. */
|
|
603
|
+
function driverContext(spec, cwd, hasSettings, mockArgs) {
|
|
604
|
+
return {
|
|
605
|
+
prompt: spec.prompt ?? "go",
|
|
606
|
+
cwd,
|
|
607
|
+
hasSettings,
|
|
608
|
+
tools: spec.tools,
|
|
609
|
+
allowedTools: spec.allowedTools,
|
|
610
|
+
transcript: spec.transcript ?? false,
|
|
611
|
+
pluginDir: spec.pluginDir,
|
|
612
|
+
mockArgs,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
542
615
|
/**
|
|
543
616
|
* The scripted model tells an agent turn from the CLI's own bookkeeping calls by
|
|
544
617
|
* the tools the request declares (`isMainLoopRequest`). An agent with none is
|
|
545
618
|
* never served a script turn, and the run would decide on nothing.
|
|
546
619
|
*/
|
|
547
620
|
function refuseUnrunnableSpec(spec) {
|
|
548
|
-
if (spec.
|
|
549
|
-
|
|
621
|
+
if (spec.tools !== undefined &&
|
|
622
|
+
toolAvailabilityList(spec.tools).length === 0) {
|
|
623
|
+
throw new Error("tools: [] leaves the agent with no tools (a list of blank names counts as empty), and the scripted model only serves a turn to a request that declares tools — no script turn would be consumed. Name the tools the agent has.");
|
|
550
624
|
}
|
|
551
|
-
const unoffered = unofferedScriptedTool(spec.model, spec.
|
|
625
|
+
const unoffered = unofferedScriptedTool(spec.model, spec.tools);
|
|
552
626
|
if (unoffered !== undefined)
|
|
553
627
|
throw new Error(unoffered);
|
|
554
628
|
}
|
|
@@ -585,15 +659,18 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
585
659
|
const cwd = (0, tmp_root_js_1.makeTmpDir)("harness");
|
|
586
660
|
writeFixture(cwd, files, settings);
|
|
587
661
|
const timeoutMs = spec.timeoutMs ?? 60000;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
662
|
+
// A driver refuses a spec it cannot run (Codex and `tools`) inside buildArgs.
|
|
663
|
+
// That refusal must not leave the run's temp dir behind; the mock, if started,
|
|
664
|
+
// is closed by the caller's finally.
|
|
665
|
+
const buildArgs = (mockArgs) => {
|
|
666
|
+
try {
|
|
667
|
+
return driver.buildArgs(driverContext(spec, cwd, settings !== undefined, mockArgs));
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
597
674
|
// Confined path (Claude Code only): the mock is co-launched in the netns, so
|
|
598
675
|
// the agent reaches it over the loopback URL the sandbox sets — Claude Code is
|
|
599
676
|
// env-only (empty mockArgs), so the argv needs no mock-wiring flags.
|
|
@@ -612,7 +689,7 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
612
689
|
// warning, which the sandbox path could not emit at all before.
|
|
613
690
|
const { count, sideChannelCount } = (0, mock_model_js_1.splitRequestCounts)(out.requests);
|
|
614
691
|
warnUnconsumed(count, sideChannelCount);
|
|
615
|
-
assertRunSound(cwd, "", spec.model
|
|
692
|
+
assertRunSound(cwd, "", spec.model, out.requests, spec.tools);
|
|
616
693
|
return check(makeResult(cwd, out, parseClaudeRun(out.stdout), count, out.requests));
|
|
617
694
|
}
|
|
618
695
|
// Direct path: mock runs in this process; the agent reaches it over localhost.
|
|
@@ -631,7 +708,7 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
631
708
|
// it (every request arriving without tool declarations, so nothing looked
|
|
632
709
|
// like an agent turn) instead of leaving it to be rediscovered.
|
|
633
710
|
warnUnconsumed(mock.count, mock.sideChannelCount ?? 0);
|
|
634
|
-
assertRunSound(cwd, out.stderr, spec.model
|
|
711
|
+
assertRunSound(cwd, out.stderr, spec.model, mock.requests, spec.tools);
|
|
635
712
|
return check(makeResult(cwd, out, driver.parseRun(out.stdout), mock.count, [
|
|
636
713
|
...mock.requests,
|
|
637
714
|
]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "36.0.0",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|