@vibe-agent-toolkit/agent-skills 0.2.0-rc.1 → 0.2.0-rc.3
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/dist/files-config.d.ts +34 -1
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +280 -36
- package/dist/files-config.js.map +1 -1
- package/dist/fs-attribution.d.ts +45 -0
- package/dist/fs-attribution.d.ts.map +1 -0
- package/dist/fs-attribution.js +57 -0
- package/dist/fs-attribution.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
- package/dist/inventory/index.d.ts +1 -1
- package/dist/inventory/index.d.ts.map +1 -1
- package/dist/inventory/index.js +1 -1
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/serialize.d.ts +4 -4
- package/dist/inventory/serialize.d.ts.map +1 -1
- package/dist/inventory/serialize.js +6 -6
- package/dist/inventory/serialize.js.map +1 -1
- package/dist/post-build-checks.d.ts +1 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js.map +1 -1
- package/dist/projection/skill-extent.d.ts +247 -0
- package/dist/projection/skill-extent.d.ts.map +1 -0
- package/dist/projection/skill-extent.js +558 -0
- package/dist/projection/skill-extent.js.map +1 -0
- package/dist/schemas/agent-skill-frontmatter.js +1 -1
- package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
- package/dist/skill-packager.d.ts +34 -4
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +113 -23
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +9 -8
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-test/baseline-delta.d.ts +1575 -0
- package/dist/skill-test/baseline-delta.d.ts.map +1 -0
- package/dist/skill-test/baseline-delta.js +272 -0
- package/dist/skill-test/baseline-delta.js.map +1 -0
- package/dist/skill-test/baseline-integrity.d.ts +944 -0
- package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
- package/dist/skill-test/baseline-integrity.js +3083 -0
- package/dist/skill-test/baseline-integrity.js.map +1 -0
- package/dist/skill-test/eval-executor.d.ts +19 -3
- package/dist/skill-test/eval-executor.d.ts.map +1 -1
- package/dist/skill-test/eval-executor.js +3 -3
- package/dist/skill-test/eval-executor.js.map +1 -1
- package/dist/skill-test/eval-fragment.d.ts +80 -6
- package/dist/skill-test/eval-fragment.d.ts.map +1 -1
- package/dist/skill-test/eval-fragment.js +61 -7
- package/dist/skill-test/eval-fragment.js.map +1 -1
- package/dist/skill-test/eval-grader.d.ts.map +1 -1
- package/dist/skill-test/eval-grader.js +143 -7
- package/dist/skill-test/eval-grader.js.map +1 -1
- package/dist/skill-test/eval-inputs.d.ts +107 -6
- package/dist/skill-test/eval-inputs.d.ts.map +1 -1
- package/dist/skill-test/eval-inputs.js +173 -16
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.d.ts +13 -0
- package/dist/skill-test/eval-lint.d.ts.map +1 -1
- package/dist/skill-test/eval-lint.js +17 -4
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/executor-prompt.d.ts +13 -5
- package/dist/skill-test/executor-prompt.d.ts.map +1 -1
- package/dist/skill-test/executor-prompt.js +8 -5
- package/dist/skill-test/executor-prompt.js.map +1 -1
- package/dist/skill-test/exit-codes.d.ts +53 -16
- package/dist/skill-test/exit-codes.d.ts.map +1 -1
- package/dist/skill-test/exit-codes.js +123 -40
- package/dist/skill-test/exit-codes.js.map +1 -1
- package/dist/skill-test/fragment-merge.d.ts +1 -18
- package/dist/skill-test/fragment-merge.d.ts.map +1 -1
- package/dist/skill-test/fragment-merge.js +63 -4
- package/dist/skill-test/fragment-merge.js.map +1 -1
- package/dist/skill-test/grader-prompt.d.ts +27 -7
- package/dist/skill-test/grader-prompt.d.ts.map +1 -1
- package/dist/skill-test/grader-prompt.js +253 -61
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/skill-test/grader-text.d.ts +145 -0
- package/dist/skill-test/grader-text.d.ts.map +1 -0
- package/dist/skill-test/grader-text.js +410 -0
- package/dist/skill-test/grader-text.js.map +1 -0
- package/dist/skill-test/grading-adapter.d.ts +32 -0
- package/dist/skill-test/grading-adapter.d.ts.map +1 -1
- package/dist/skill-test/grading-adapter.js +50 -3
- package/dist/skill-test/grading-adapter.js.map +1 -1
- package/dist/skill-test/grading-schema.d.ts +160 -5
- package/dist/skill-test/grading-schema.d.ts.map +1 -1
- package/dist/skill-test/grading-schema.js +66 -7
- package/dist/skill-test/grading-schema.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/lock.d.ts +56 -0
- package/dist/skill-test/lock.d.ts.map +1 -1
- package/dist/skill-test/lock.js +75 -2
- package/dist/skill-test/lock.js.map +1 -1
- package/dist/skill-test/pipeline.d.ts +16 -0
- package/dist/skill-test/pipeline.d.ts.map +1 -1
- package/dist/skill-test/pipeline.js +16 -9
- package/dist/skill-test/pipeline.js.map +1 -1
- package/dist/skill-test/preflight.d.ts.map +1 -1
- package/dist/skill-test/preflight.js +58 -14
- package/dist/skill-test/preflight.js.map +1 -1
- package/dist/skill-test/run-harness.d.ts +578 -35
- package/dist/skill-test/run-harness.d.ts.map +1 -1
- package/dist/skill-test/run-harness.js +1278 -147
- package/dist/skill-test/run-harness.js.map +1 -1
- package/dist/skill-test/staging.d.ts +8 -2
- package/dist/skill-test/staging.d.ts.map +1 -1
- package/dist/skill-test/staging.js +8 -4
- package/dist/skill-test/staging.js.map +1 -1
- package/dist/skill-test/tool-eval-schema.d.ts +6 -6
- package/dist/test-input.d.ts +1 -1
- package/dist/test-input.d.ts.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +1 -1
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +1 -1
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +1 -1
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +1 -1
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.js +1 -1
- package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
- package/dist/validators/description-style-detection.d.ts +1 -1
- package/dist/validators/description-style-detection.d.ts.map +1 -1
- package/dist/validators/description-style-detection.js +1 -1
- package/dist/validators/description-style-detection.js.map +1 -1
- package/dist/validators/frontmatter-validation.d.ts +1 -1
- package/dist/validators/frontmatter-validation.d.ts.map +1 -1
- package/dist/validators/frontmatter-validation.js +1 -1
- package/dist/validators/frontmatter-validation.js.map +1 -1
- package/dist/validators/imperative-body-detection.d.ts +1 -1
- package/dist/validators/imperative-body-detection.d.ts.map +1 -1
- package/dist/validators/imperative-body-detection.js +1 -1
- package/dist/validators/imperative-body-detection.js.map +1 -1
- package/dist/validators/index.d.ts +1 -1
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.d.ts +1 -1
- package/dist/validators/kebab-case-detection.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.js +1 -1
- package/dist/validators/kebab-case-detection.js.map +1 -1
- package/dist/validators/marketplace-validator.js +1 -1
- package/dist/validators/marketplace-validator.js.map +1 -1
- package/dist/validators/packaging-validator.d.ts +38 -5
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +87 -10
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
- package/dist/validators/plugin-hosted-shape.js +1 -1
- package/dist/validators/plugin-hosted-shape.js.map +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
- package/dist/validators/plugin-recommended-fields.js +1 -1
- package/dist/validators/plugin-recommended-fields.js.map +1 -1
- package/dist/validators/registry-validator.js +1 -1
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +20 -9
- package/dist/validators/rule-engine/rule-engine.js.map +1 -1
- package/dist/validators/skill-validator.d.ts.map +1 -1
- package/dist/validators/skill-validator.js +21 -3
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/types.d.ts +1 -1
- package/dist/validators/types.d.ts.map +1 -1
- package/dist/validators/unified-validator.js +1 -1
- package/dist/validators/unified-validator.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +1 -1
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.d.ts +7 -3
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +37 -8
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +36 -2
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +7 -7
|
@@ -3,6 +3,7 @@ import { mkdirSyncReal, parseStreamJsonTranscript, safePath, spawnHeadlessClaude
|
|
|
3
3
|
import { EvalFragmentError, parseEvalFragment } from './eval-fragment.js';
|
|
4
4
|
import { InternalHarnessError } from './exit-codes.js';
|
|
5
5
|
import { assertGraderPromptInvariants, buildGraderPrompt } from './grader-prompt.js';
|
|
6
|
+
import { sanitizeGraderText } from './grader-text.js';
|
|
6
7
|
import { GradingNonceError } from './grading-adapter.js';
|
|
7
8
|
import { computeToolPassed } from './tool-eval-schema.js';
|
|
8
9
|
/**
|
|
@@ -40,7 +41,16 @@ function fragmentWarnRouter(onProgress) {
|
|
|
40
41
|
return (message) => onProgress(`[skill-test] ${message}\n`);
|
|
41
42
|
}
|
|
42
43
|
export async function runGraderForEval(input) {
|
|
43
|
-
|
|
44
|
+
// 0700, like every other vat-only directory this codebase creates (the grader
|
|
45
|
+
// root in `resolveGraderOutDir`, the workspaces root and every eval workspace in
|
|
46
|
+
// `stageEvalWorkspaces`). This is the per-ARM subdirectory of an already-0700
|
|
47
|
+
// parent, so nothing is exposed today — traversal into it is blocked one level
|
|
48
|
+
// up. It is made explicit because the rule is only worth anything while it is
|
|
49
|
+
// uniform: an unmoded `mkdir` here is the one place a reader would have to prove
|
|
50
|
+
// the parent's mode to know the fragment (which echoes the run's integrity nonce)
|
|
51
|
+
// is unreadable, and the day this dir is created somewhere else that proof is
|
|
52
|
+
// gone. Fragments are consumed-on-read anyway; this is the resting-state half.
|
|
53
|
+
mkdirSyncReal(input.graderOutDir, { recursive: true, mode: 0o700 });
|
|
44
54
|
// evalId is validated `[A-Za-z0-9_-]+` upstream (eval-inputs.ts) so a plain
|
|
45
55
|
// join is safe, but `joinUnderRoot` is used defensively — it costs nothing
|
|
46
56
|
// and closes off any future path where an unvalidated id reaches here.
|
|
@@ -58,10 +68,14 @@ export async function runGraderForEval(input) {
|
|
|
58
68
|
});
|
|
59
69
|
// Defense-in-depth (parity with eval-executor.ts's build→assert→use pattern):
|
|
60
70
|
// re-verify the built grader prompt still carries its required invariants
|
|
61
|
-
// (STOP, fragment path, browser/iteration forbids, nonce directive
|
|
62
|
-
// spawning, so a regressed buildGraderPrompt is
|
|
63
|
-
// that silently misbehaves.
|
|
64
|
-
|
|
71
|
+
// (STOP, fragment path, browser/iteration forbids, nonce directive, and the
|
|
72
|
+
// untrusted-data fences) before spawning, so a regressed buildGraderPrompt is
|
|
73
|
+
// caught here, not by a grader that silently misbehaves. The nonce is handed
|
|
74
|
+
// over so the assertion can cut out every fenced region — transcript, subject
|
|
75
|
+
// manifest, and the suite's own expectations/expected_output — and none of
|
|
76
|
+
// that attacker-controlled text can satisfy an invariant on the builder's
|
|
77
|
+
// behalf.
|
|
78
|
+
assertGraderPromptInvariants(prompt, input.nonce);
|
|
65
79
|
const spawn = input.spawn ?? spawnHeadlessClaude;
|
|
66
80
|
// Accumulate the grader's stream-json stdout so we can read its terminal
|
|
67
81
|
// `total_cost_usd` for run-wide spend aggregation (adopter follow-up). Held in
|
|
@@ -117,8 +131,16 @@ export async function runGraderForEval(input) {
|
|
|
117
131
|
if (fragment.runNonce !== input.nonce) {
|
|
118
132
|
throw new GradingNonceError(`eval "${input.evalId}" fragment \`runNonce\` does not match this run`);
|
|
119
133
|
}
|
|
134
|
+
assertExpectationCountDeclared(input.evalId, input.expectations, fragment.expectations);
|
|
120
135
|
assertToolVerdictConsistent(input.evalId, input.toolExpectations, fragment.tool);
|
|
121
|
-
|
|
136
|
+
// The grader's only input is the executor transcript, which untrusted skill code
|
|
137
|
+
// controls, and `evalId` is schema-typed as any non-empty string. It is echoed
|
|
138
|
+
// verbatim into the run summary and into `baseline.json` — including the
|
|
139
|
+
// CONTAMINATED banner — so a grader talked into emitting an id containing
|
|
140
|
+
// newlines and ANSI escapes can paint a reassuring "known false positive, the
|
|
141
|
+
// delta is valid" line directly beneath vat's own warning. VAT knows which eval
|
|
142
|
+
// it asked about; take the id from the request, never from the answer.
|
|
143
|
+
return { ...fragment, evalId: input.evalId };
|
|
122
144
|
}
|
|
123
145
|
/**
|
|
124
146
|
* Read the grader's fragment JSON into memory and unlink it immediately —
|
|
@@ -146,7 +168,16 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
|
|
|
146
168
|
raw = JSON.parse(readFileSync(fragmentOut, 'utf-8'));
|
|
147
169
|
}
|
|
148
170
|
catch (err) {
|
|
149
|
-
|
|
171
|
+
// V8 embeds a VERBATIM slice of the offending bytes in its SyntaxError
|
|
172
|
+
// message, unescaped — and this is one of the few grader-controlled strings
|
|
173
|
+
// that reaches an operator WITHOUT passing `parseEvalFragment`, the
|
|
174
|
+
// documented text boundary, because the parse it would have gone through is
|
|
175
|
+
// the one that just failed. The CLI writes it as `Error: ${err.message}`, so
|
|
176
|
+
// an unsanitized fragment of `ESC[2K CR ESC[32m…` wipes the line vat just
|
|
177
|
+
// printed and continues in vat's own colour (and a <=16-byte file gets its
|
|
178
|
+
// whole `ESC[2J ESC[H` echoed, clearing the screen).
|
|
179
|
+
throw new EvalFragmentError(`grader fragment for eval "${evalId}" at ${fragmentOut} is not valid JSON: ` +
|
|
180
|
+
sanitizeGraderText(err instanceof Error ? err.message : String(err)));
|
|
150
181
|
}
|
|
151
182
|
try {
|
|
152
183
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- fragmentOut is our own derived path (joinUnderRoot-guarded)
|
|
@@ -157,6 +188,57 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
|
|
|
157
188
|
}
|
|
158
189
|
return raw;
|
|
159
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The OUTPUT channel's declared-vs-reported check — the exact thing
|
|
193
|
+
* {@link assertToolCheckNamesDeclared} does for the TOOL channel, applied to the
|
|
194
|
+
* channel every reported number is computed from.
|
|
195
|
+
*
|
|
196
|
+
* Each arm's `summary`, the pass/fail verdict `reconcileGrading` produces, and
|
|
197
|
+
* the `--baseline` delta between the arms are all computed over
|
|
198
|
+
* `fragment.expectations` — i.e. over however many entries the grader chose to
|
|
199
|
+
* write. The prompt ASKS for "one per expectation above" and, until this check,
|
|
200
|
+
* nothing verified it:
|
|
201
|
+
*
|
|
202
|
+
* - **Omission laundered a pass, on EVERY run.** A grader that skips a FAILING
|
|
203
|
+
* expectation leaves a fragment where the survivors all passed, so
|
|
204
|
+
* `reconcileGrading` reports `allPassed: true` and the run exits 0.
|
|
205
|
+
* - **Invention inflated the delta.** A suite declaring 1 expectation whose two
|
|
206
|
+
* graders each emit 3 of their own yields `Baseline delta: +3` where the
|
|
207
|
+
* maximum legal lift is +1.
|
|
208
|
+
*
|
|
209
|
+
* The existing `armExpectationSkew` cannot see either: it compares the two ARMS
|
|
210
|
+
* to each other, and both graders receive the same declared list from the same
|
|
211
|
+
* template, so they drift together and parity holds while a confident wrong
|
|
212
|
+
* number prints.
|
|
213
|
+
*
|
|
214
|
+
* COUNT, not text. The grader is asked to echo each expectation's `text`, but it
|
|
215
|
+
* routinely reworks the wording, and a reworded text is not a wrong verdict —
|
|
216
|
+
* whereas a missing or extra ENTRY always is, because the entry is the unit
|
|
217
|
+
* every score counts.
|
|
218
|
+
*
|
|
219
|
+
* Fail-closed like its siblings below: this is vat's own grading infrastructure
|
|
220
|
+
* failing its one job, not adopter data being audited.
|
|
221
|
+
*
|
|
222
|
+
* FAIL-CLOSED IS NOT FAIL-THE-RUN, and the distinction is the caller's to make, not
|
|
223
|
+
* this function's. Throwing here is unconditional and stays that way: a fragment
|
|
224
|
+
* whose entry count does not match the declaration is not a verdict, and returning
|
|
225
|
+
* it "leniently" for the control arm would put an unchecked number straight into the
|
|
226
|
+
* delta — which is the hole this check was added to close. What changed is where the
|
|
227
|
+
* throw lands. `runEvalWorker` (run-harness.ts) catches a CONTROL-arm throw, records
|
|
228
|
+
* it as a control-arm failure, and lets the run finish; a TREATMENT-arm throw still
|
|
229
|
+
* propagates and fails the run. So a control-arm grader miscount now withholds the
|
|
230
|
+
* delta as `null` with the reason attached, instead of destroying fully-billed
|
|
231
|
+
* treatment work that was never in doubt. Do not soften this assert to get that
|
|
232
|
+
* behaviour — it is already there, one level up, where the arm is known.
|
|
233
|
+
*/
|
|
234
|
+
function assertExpectationCountDeclared(evalId, declared, reported) {
|
|
235
|
+
if (reported.length === declared.length)
|
|
236
|
+
return;
|
|
237
|
+
throw new InternalHarnessError(`Grader for eval "${evalId}" returned ${reported.length} expectation entr${reported.length === 1 ? 'y' : 'ies'} ` +
|
|
238
|
+
`for an eval that declares ${declared.length}. Every score vat reports — each arm's summary and the ` +
|
|
239
|
+
`--baseline delta between them — is computed over the entries the grader emitted, so an invented or ` +
|
|
240
|
+
`omitted entry silently changes it.`);
|
|
241
|
+
}
|
|
160
242
|
/**
|
|
161
243
|
* Fail-open guards on the grader's tool verdict, split out of
|
|
162
244
|
* {@link runGraderForEval} to keep its cognitive complexity within budget:
|
|
@@ -171,6 +253,8 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
|
|
|
171
253
|
* that disagrees with its own sub-checks is grader malfunction (a false green
|
|
172
254
|
* or a false red), surfaced loudly rather than merged. Mirrors
|
|
173
255
|
* `reconcileGrading`'s summary/expectations reconciliation (grading-adapter.ts).
|
|
256
|
+
* - Fix #4: the check NAMES must be exactly the eval's declared ones. See
|
|
257
|
+
* {@link assertToolCheckNamesDeclared}.
|
|
174
258
|
*/
|
|
175
259
|
function assertToolVerdictConsistent(evalId, toolExpectations, tool) {
|
|
176
260
|
if (toolExpectations !== undefined && tool === undefined) {
|
|
@@ -180,5 +264,57 @@ function assertToolVerdictConsistent(evalId, toolExpectations, tool) {
|
|
|
180
264
|
throw new InternalHarnessError(`Grader for eval "${evalId}" emitted a tool verdict whose \`passed\` (${tool.passed}) ` +
|
|
181
265
|
`disagrees with its own sub-checks (recomputed ${computeToolPassed(tool)}).`);
|
|
182
266
|
}
|
|
267
|
+
assertToolCheckNamesDeclared(evalId, toolExpectations, tool);
|
|
268
|
+
}
|
|
269
|
+
/** Render grader-supplied names for an error message, already sanitized by the fragment parse. */
|
|
270
|
+
function quoteNames(names) {
|
|
271
|
+
return names.map((name) => `"${name}"`).join(', ');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* One channel's check names must be EXACTLY the declared ones — no invention,
|
|
275
|
+
* no omission.
|
|
276
|
+
*
|
|
277
|
+
* `computeToolPassed` iterates the checks the GRADER emitted, not the ones the
|
|
278
|
+
* eval declared, so both directions are load-bearing and neither is cosmetic:
|
|
279
|
+
*
|
|
280
|
+
* - **Omission laundered a pass.** Drop the `mustRun` entry for an executable
|
|
281
|
+
* that never ran and the channel is vacuously true — the verdict reads
|
|
282
|
+
* `passed: true` for an expectation nobody checked.
|
|
283
|
+
* - **Invention gates on a name vat never asked about.** A grader talked into
|
|
284
|
+
* emitting `{name: "totally-fine", ran: true}` adds a check the eval does not
|
|
285
|
+
* declare, and it counts toward the verdict exactly like a real one.
|
|
286
|
+
*
|
|
287
|
+
* Names are compared AFTER {@link sanitizeGraderText} on both sides. The
|
|
288
|
+
* reported side is already sanitized (parseEvalFragment); normalizing the
|
|
289
|
+
* declared side too means the comparison happens in one space, so an adopter
|
|
290
|
+
* who declared a name containing a tab does not get an unexplainable mismatch.
|
|
291
|
+
*
|
|
292
|
+
* Fail-closed like its siblings above: this is vat's own grading infrastructure
|
|
293
|
+
* failing to do its one job, not adopter data being audited.
|
|
294
|
+
*/
|
|
295
|
+
function assertToolCheckNamesDeclared(evalId, toolExpectations, tool) {
|
|
296
|
+
if (tool === undefined)
|
|
297
|
+
return;
|
|
298
|
+
const declared = toolExpectations ?? {};
|
|
299
|
+
assertChannelNames(evalId, 'mustRun', declared.mustRun, (tool.mustRun ?? []).map((c) => c.name));
|
|
300
|
+
assertChannelNames(evalId, 'mustNotRun', declared.mustNotRun, (tool.mustNotRun ?? []).map((c) => c.name));
|
|
301
|
+
assertChannelNames(evalId, 'mustSucceed', declared.mustSucceed, (tool.mustSucceed ?? []).map((c) => c.name));
|
|
302
|
+
assertChannelNames(evalId, 'sequence', declared.sequence, (tool.sequence ?? []).flatMap((c) => c.steps));
|
|
303
|
+
}
|
|
304
|
+
function assertChannelNames(evalId, channel, declared, reported) {
|
|
305
|
+
const declaredSet = new Set((declared ?? []).map(sanitizeGraderText));
|
|
306
|
+
const reportedSet = new Set(reported);
|
|
307
|
+
const invented = [...reportedSet].filter((name) => !declaredSet.has(name));
|
|
308
|
+
const missing = [...declaredSet].filter((name) => !reportedSet.has(name));
|
|
309
|
+
if (invented.length === 0 && missing.length === 0)
|
|
310
|
+
return;
|
|
311
|
+
const problems = [];
|
|
312
|
+
if (invented.length > 0)
|
|
313
|
+
problems.push(`invented ${quoteNames(invented)}`);
|
|
314
|
+
if (missing.length > 0)
|
|
315
|
+
problems.push(`omitted ${quoteNames(missing)}`);
|
|
316
|
+
throw new InternalHarnessError(`Grader for eval "${evalId}" emitted \`tool.${channel}\` checks that do not match the eval's ` +
|
|
317
|
+
`declared toolExpectations: ${problems.join('; ')}. The verdict is computed from the checks the ` +
|
|
318
|
+
`grader emitted, so an invented or omitted name silently changes it.`);
|
|
183
319
|
}
|
|
184
320
|
//# sourceMappingURL=eval-grader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-grader.js","sourceRoot":"","sources":["../../src/skill-test/eval-grader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAEpH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAqB,MAAM,oBAAoB,CAAC;AAE7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,4BAA4B,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAgD1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH;;;;;GAKG;AACH,SAAS,kBAAkB,CACzB,UAAwC;IAExC,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,gBAAgB,OAAO,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAqB;IAC1D,aAAa,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"eval-grader.js","sourceRoot":"","sources":["../../src/skill-test/eval-grader.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Raised for any eval-input problem (bad JSON, schema failure, missing input file).
|
|
4
|
+
* Maps to exit 2.
|
|
5
|
+
*
|
|
6
|
+
* The FIELD is what makes that sentence true, and until it was added only one of this
|
|
7
|
+
* class's two routes delivered it. `attemptStageWorkspaces` catches the staging
|
|
8
|
+
* instance and maps it explicitly; the instance thrown from {@link armDirSegment}
|
|
9
|
+
* fires deep in the eval loop, outside that handler, and reached
|
|
10
|
+
* `mapErrorToExitCode`'s Internal default — reporting 1 for a preflight-class
|
|
11
|
+
* problem while the docblock above claimed 2. Exit 2 for the internal-invariant case
|
|
12
|
+
* too is consistent with every sibling in this feature.
|
|
13
|
+
*/
|
|
3
14
|
export declare class EvalInputError extends Error {
|
|
15
|
+
readonly exitCode: 2;
|
|
4
16
|
constructor(message: string);
|
|
5
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Neutralize a SUITE-AUTHORED string before it is quoted into an
|
|
20
|
+
* {@link EvalInputError}, whose message reaches `process.stdout` on the
|
|
21
|
+
* `Summary:` line — the one channel deliberately kept machine-readable — and
|
|
22
|
+
* renders twice on the way there.
|
|
23
|
+
*
|
|
24
|
+
* The suite is nominally adopter-authored, but `resolveEvalSuitePath` will
|
|
25
|
+
* harvest one out of a FETCHED npm/url artifact, i.e. out of the skill under
|
|
26
|
+
* test, so treat it as untrusted. `files[]` is the sharp edge: it is
|
|
27
|
+
* `z.array(z.string().min(1))` with NO charset constraint, in deliberate
|
|
28
|
+
* contrast to `id`, which is regex-pinned precisely because it names a
|
|
29
|
+
* directory. A derived path or an OS error text built from such an entry
|
|
30
|
+
* carries the same bytes, so those are sanitized here too, not just the entry.
|
|
31
|
+
*
|
|
32
|
+
* Reuses {@link sanitizeGraderText} rather than growing a second neutralizer:
|
|
33
|
+
* the threat and the remedy (drop escape sequences whole, fold every remaining
|
|
34
|
+
* control code to a space, collapse, cap) are identical — only the source of
|
|
35
|
+
* the untrusted text differs.
|
|
36
|
+
*
|
|
37
|
+
* EXPORTED because the suite reaches an operator surface from more than this
|
|
38
|
+
* module: `eval-lint.ts` quotes `toolExpectations.*` entries — the same
|
|
39
|
+
* unconstrained `z.array(z.string().min(1))` shape as `files[]` — into an advisory
|
|
40
|
+
* that `run-harness.ts` writes to stderr verbatim, and does it at Step 5.5, ahead
|
|
41
|
+
* of both the security-ack gate and the `--dry-run` short-circuit. Every new
|
|
42
|
+
* suite-authored string bound for a terminal goes through HERE, so the trust model
|
|
43
|
+
* above stays stated in one place.
|
|
44
|
+
*/
|
|
45
|
+
export declare function quoteSuiteText(value: string): string;
|
|
6
46
|
/**
|
|
7
47
|
* True when `key` is exactly one edit (insert/delete/substitute one char) from
|
|
8
48
|
* `target`. Used to catch typos of recognized fields (here) and of declared
|
|
@@ -496,19 +536,80 @@ export type EvalSuite = z.infer<typeof EvalSuiteSchema>;
|
|
|
496
536
|
export type ToolExpectations = NonNullable<EvalEntry['toolExpectations']>;
|
|
497
537
|
/** Parse + validate a skill-test eval suite. Throws {@link EvalInputError} on any problem. */
|
|
498
538
|
export declare function parseEvalSuite(jsonText: string): EvalSuite;
|
|
539
|
+
/**
|
|
540
|
+
* One side of a `--baseline` A/B: `'with'` runs the skill staged and declared,
|
|
541
|
+
* `'without'` is the control. Declared here rather than in the run orchestrator
|
|
542
|
+
* because the staging layer needs it to give each arm its own workspace tree.
|
|
543
|
+
*/
|
|
544
|
+
export type EvalArm = 'with' | 'without';
|
|
545
|
+
/**
|
|
546
|
+
* The opaque directory segment each arm's workspaces live under, minted fresh per
|
|
547
|
+
* run. `with` is required and `without` present only under `--baseline`, so the
|
|
548
|
+
* set of arms is the keys — a shape that cannot express "stage nothing", unlike
|
|
549
|
+
* the `readonly EvalArm[]` this replaced (an empty array was a legal input that
|
|
550
|
+
* silently left every executor with a non-existent cwd).
|
|
551
|
+
*
|
|
552
|
+
* ⚠️ The VALUES must not be the literal arm names, and must not be derivable from
|
|
553
|
+
* each other. See {@link stageEvalWorkspaces}.
|
|
554
|
+
*/
|
|
555
|
+
export interface ArmWorkspaceDirs {
|
|
556
|
+
with: string;
|
|
557
|
+
without?: string;
|
|
558
|
+
}
|
|
559
|
+
/** The arms an {@link ArmWorkspaceDirs} covers, in dispatch order. */
|
|
560
|
+
export declare function armsOf(dirs: ArmWorkspaceDirs): readonly EvalArm[];
|
|
561
|
+
/**
|
|
562
|
+
* The directory segment for one arm. Throws rather than falling back to the arm
|
|
563
|
+
* NAME: a silent fallback would reinstate the unblinding this indirection exists
|
|
564
|
+
* to remove, and would do it invisibly.
|
|
565
|
+
*/
|
|
566
|
+
export declare function armDirSegment(dirs: ArmWorkspaceDirs, arm: EvalArm): string;
|
|
499
567
|
export interface StageEvalWorkspacesInput {
|
|
500
568
|
/** Parsed suite (Task 1). */
|
|
501
569
|
suite: EvalSuite;
|
|
502
570
|
/** Directory containing evals.json — the base for each eval's relative `files`. */
|
|
503
571
|
evalsDir: string;
|
|
504
|
-
/**
|
|
572
|
+
/** Per-run workspaces root — per-arm, per-eval dirs are created beneath it. */
|
|
505
573
|
workspacesRoot: string;
|
|
574
|
+
/**
|
|
575
|
+
* Opaque per-arm directory segments. Each arm gets its OWN copy of every
|
|
576
|
+
* workspace; see the note on the layout below for why that is not an
|
|
577
|
+
* optimization to undo.
|
|
578
|
+
*/
|
|
579
|
+
armDirs: ArmWorkspaceDirs;
|
|
506
580
|
}
|
|
507
581
|
/**
|
|
508
|
-
* Materialize each eval's declared input `files` into
|
|
509
|
-
* preserving relative structure.
|
|
510
|
-
* {@link EvalInputError} if a listed file does not exist (the eval cannot run
|
|
511
|
-
* Returns `workspacesRoot`.
|
|
582
|
+
* Materialize each eval's declared input `files` into
|
|
583
|
+
* `<workspacesRoot>/<armDir>/<id>/<relpath>`, preserving relative structure. Throws
|
|
584
|
+
* {@link EvalInputError} if a listed file does not exist (the eval cannot run
|
|
585
|
+
* without it). Returns `workspacesRoot`.
|
|
586
|
+
*
|
|
587
|
+
* EVERY eval gets a directory, including one that declares no `files` — it is
|
|
588
|
+
* simply left empty. An eval without a workspace used to leave the executor with
|
|
589
|
+
* no cwd of its own, so it ran inside the staged subject dir instead; for the
|
|
590
|
+
* skill-absent arm of a `--baseline` run that placed the control's cwd inside the
|
|
591
|
+
* very skill the arm exists to withhold. An empty dir costs nothing and keeps cwd
|
|
592
|
+
* shaped identically across both arms, which is what makes the A/B a comparison.
|
|
593
|
+
*
|
|
594
|
+
* WHY THE `<arm>` SEGMENT. The two arms of one eval are dispatched ADJACENTLY into
|
|
595
|
+
* a bounded-parallel pool, so they run at the same time. When they shared one
|
|
596
|
+
* directory, the control arm could simply `ls` and read whatever the treatment arm
|
|
597
|
+
* had just written — recovering the treatment's answer without ever touching the
|
|
598
|
+
* skill, and without producing a single detectable harness path. That is a channel
|
|
599
|
+
* BETWEEN the arms rather than into one of them, which is why auditing the four
|
|
600
|
+
* channels into a process (prompt, argv, cwd, env) did not surface it: each arm's
|
|
601
|
+
* own cwd was correct in isolation, and wrong as a pair. The arms must be
|
|
602
|
+
* byte-identical at start and unable to observe each other after it.
|
|
603
|
+
*
|
|
604
|
+
* WHY THE SEGMENT IS AN OPAQUE TOKEN and not the arm's name. It used to be the
|
|
605
|
+
* literal `with`/`without`, which made the executor's own working directory the
|
|
606
|
+
* loudest possible unblinding: the prompt tells each arm `Your working directory
|
|
607
|
+
* is …/vat-skill-test-ws-<token>/without/e1`, so the skill-absent arm was told, in
|
|
608
|
+
* the one string it cannot avoid reading, which side of the experiment it was on —
|
|
609
|
+
* while `buildExecutorPrompt`'s docblock promises it "must NEVER be told (directly
|
|
610
|
+
* or by implication) that it is being tested, evaluated, or graded". It also named
|
|
611
|
+
* the sibling: `../with/<id>/` was a guessable path to the treatment arm's live
|
|
612
|
+
* output. Independent random tokens fix both, and cost nothing.
|
|
512
613
|
*/
|
|
513
614
|
export declare function stageEvalWorkspaces(input: StageEvalWorkspacesInput): string;
|
|
514
615
|
//# sourceMappingURL=eval-inputs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-inputs.d.ts","sourceRoot":"","sources":["../../src/skill-test/eval-inputs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"eval-inputs.d.ts","sourceRoot":"","sources":["../../src/skill-test/eval-inputs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAG,CAAC,CAAU;gBACnB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AA2CD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAUrE;AAiDD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAyDxB,CAAC;AAEL,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAMZ,CAAC;AAEjB,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AACxD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE1E,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAsB1D;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,OAAO,EAAE,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAU1E;AAED,MAAM,WAAW,wBAAwB;IACvC,6BAA6B;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAK3E"}
|
|
@@ -1,13 +1,73 @@
|
|
|
1
1
|
import { cpSync, existsSync } from 'node:fs';
|
|
2
2
|
import { mkdirSyncReal, safePath } from '@vibe-agent-toolkit/utils';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
|
|
4
|
+
import { sanitizeGraderText, sanitizeTextPreservingLines } from './grader-text.js';
|
|
5
|
+
/**
|
|
6
|
+
* Raised for any eval-input problem (bad JSON, schema failure, missing input file).
|
|
7
|
+
* Maps to exit 2.
|
|
8
|
+
*
|
|
9
|
+
* The FIELD is what makes that sentence true, and until it was added only one of this
|
|
10
|
+
* class's two routes delivered it. `attemptStageWorkspaces` catches the staging
|
|
11
|
+
* instance and maps it explicitly; the instance thrown from {@link armDirSegment}
|
|
12
|
+
* fires deep in the eval loop, outside that handler, and reached
|
|
13
|
+
* `mapErrorToExitCode`'s Internal default — reporting 1 for a preflight-class
|
|
14
|
+
* problem while the docblock above claimed 2. Exit 2 for the internal-invariant case
|
|
15
|
+
* too is consistent with every sibling in this feature.
|
|
16
|
+
*/
|
|
5
17
|
export class EvalInputError extends Error {
|
|
18
|
+
exitCode = 2;
|
|
6
19
|
constructor(message) {
|
|
7
20
|
super(message);
|
|
8
21
|
this.name = 'EvalInputError';
|
|
9
22
|
}
|
|
10
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Neutralize a SUITE-AUTHORED string before it is quoted into an
|
|
26
|
+
* {@link EvalInputError}, whose message reaches `process.stdout` on the
|
|
27
|
+
* `Summary:` line — the one channel deliberately kept machine-readable — and
|
|
28
|
+
* renders twice on the way there.
|
|
29
|
+
*
|
|
30
|
+
* The suite is nominally adopter-authored, but `resolveEvalSuitePath` will
|
|
31
|
+
* harvest one out of a FETCHED npm/url artifact, i.e. out of the skill under
|
|
32
|
+
* test, so treat it as untrusted. `files[]` is the sharp edge: it is
|
|
33
|
+
* `z.array(z.string().min(1))` with NO charset constraint, in deliberate
|
|
34
|
+
* contrast to `id`, which is regex-pinned precisely because it names a
|
|
35
|
+
* directory. A derived path or an OS error text built from such an entry
|
|
36
|
+
* carries the same bytes, so those are sanitized here too, not just the entry.
|
|
37
|
+
*
|
|
38
|
+
* Reuses {@link sanitizeGraderText} rather than growing a second neutralizer:
|
|
39
|
+
* the threat and the remedy (drop escape sequences whole, fold every remaining
|
|
40
|
+
* control code to a space, collapse, cap) are identical — only the source of
|
|
41
|
+
* the untrusted text differs.
|
|
42
|
+
*
|
|
43
|
+
* EXPORTED because the suite reaches an operator surface from more than this
|
|
44
|
+
* module: `eval-lint.ts` quotes `toolExpectations.*` entries — the same
|
|
45
|
+
* unconstrained `z.array(z.string().min(1))` shape as `files[]` — into an advisory
|
|
46
|
+
* that `run-harness.ts` writes to stderr verbatim, and does it at Step 5.5, ahead
|
|
47
|
+
* of both the security-ack gate and the `--dry-run` short-circuit. Every new
|
|
48
|
+
* suite-authored string bound for a terminal goes through HERE, so the trust model
|
|
49
|
+
* above stays stated in one place.
|
|
50
|
+
*/
|
|
51
|
+
export function quoteSuiteText(value) {
|
|
52
|
+
return sanitizeGraderText(value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The same neutralization for a suite-derived string that is ALREADY multi-line
|
|
56
|
+
* and whose lines carry the meaning — today, exactly one: zod's schema-failure
|
|
57
|
+
* text.
|
|
58
|
+
*
|
|
59
|
+
* That message is a list of issues, one per offending path, and every one of the
|
|
60
|
+
* paths in it is a SUITE-AUTHORED KEY (`evals[17].toolExpecations`) reaching
|
|
61
|
+
* stderr unsanitized. It cannot go through {@link quoteSuiteText}: that collapses
|
|
62
|
+
* the list onto one capped line, which is the difference between "your suite has
|
|
63
|
+
* a typo, here is where" and "your suite has a typo". Degrading the common case to
|
|
64
|
+
* neutralize the rare one is a bad trade, so the sanitizer that keeps lines is
|
|
65
|
+
* used instead. See {@link sanitizeTextPreservingLines} for why it is not the
|
|
66
|
+
* default anywhere else.
|
|
67
|
+
*/
|
|
68
|
+
function quoteSuiteBlock(value) {
|
|
69
|
+
return sanitizeTextPreservingLines(value);
|
|
70
|
+
}
|
|
11
71
|
/**
|
|
12
72
|
* String eval ids name a per-eval working directory ({@link stageEvalWorkspaces}),
|
|
13
73
|
* so they must be safe path segments on every platform. Letters, digits, hyphen,
|
|
@@ -52,6 +112,42 @@ export function isSingleEditAway(key, target) {
|
|
|
52
112
|
return key.slice(i + 1) === target.slice(i); // deletion from key
|
|
53
113
|
return key.slice(i) === target.slice(i + 1); // insertion into key
|
|
54
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Reject an expectation string an eval declares TWICE.
|
|
117
|
+
*
|
|
118
|
+
* The entry, not the text, is the unit every reported score counts:
|
|
119
|
+
* `runGraderForEval`'s declared-vs-graded check compares `fragment.expectations`
|
|
120
|
+
* against `expectations.length`, and each arm's summary and the `--baseline` delta
|
|
121
|
+
* are computed over those entries. A suite that declares the same string twice
|
|
122
|
+
* hands the grader "1. x / 2. x" and gets ONE entry back for it — a perfectly
|
|
123
|
+
* reasonable grader response, and one that fails the count check mid-run with an
|
|
124
|
+
* `InternalHarnessError`, destroying a fully-billed treatment run over an input
|
|
125
|
+
* defect nothing looked at.
|
|
126
|
+
*
|
|
127
|
+
* So it is caught at PARSE, exactly like the duplicate-id rule in
|
|
128
|
+
* {@link parseEvalSuite}: exit 2, user-correctable, before anything is spawned.
|
|
129
|
+
* Scoped to one eval — two different evals may legitimately share expectation
|
|
130
|
+
* text, since each is graded from its own transcript.
|
|
131
|
+
*
|
|
132
|
+
* `expectations` is `z.array(z.string().min(1))` with no charset constraint and
|
|
133
|
+
* the message reaches stdout, so the offending string is sanitized on the way in.
|
|
134
|
+
*/
|
|
135
|
+
function addDuplicateExpectationIssues(expectations, ctx) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (const [index, expectation] of expectations.entries()) {
|
|
138
|
+
if (!seen.has(expectation)) {
|
|
139
|
+
seen.add(expectation);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
ctx.addIssue({
|
|
143
|
+
code: z.ZodIssueCode.custom,
|
|
144
|
+
message: `duplicate expectation "${quoteSuiteText(expectation)}" — an eval must declare each expectation ` +
|
|
145
|
+
'once. Every reported score counts fragment ENTRIES, and a grader handed the same text twice ' +
|
|
146
|
+
'returns one entry for it, which fails the declared-count check mid-run.',
|
|
147
|
+
path: ['expectations', index],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
55
151
|
// evals.json is adopter-authored input that VAT *reads* — so per the project's
|
|
56
152
|
// Postel's Law (read the outside world liberally), we validate only the fields
|
|
57
153
|
// VAT actually consumes and pass everything else through untouched. `id` accepts
|
|
@@ -101,6 +197,7 @@ export const EvalEntrySchema = z
|
|
|
101
197
|
})
|
|
102
198
|
.passthrough()
|
|
103
199
|
.superRefine((entry, ctx) => {
|
|
200
|
+
addDuplicateExpectationIssues(entry.expectations, ctx);
|
|
104
201
|
for (const key of Object.keys(entry)) {
|
|
105
202
|
if (RECOGNIZED_EVAL_FIELDS.includes(key))
|
|
106
203
|
continue;
|
|
@@ -108,7 +205,9 @@ export const EvalEntrySchema = z
|
|
|
108
205
|
if (near !== undefined) {
|
|
109
206
|
ctx.addIssue({
|
|
110
207
|
code: z.ZodIssueCode.custom,
|
|
111
|
-
|
|
208
|
+
// `key` is a suite-authored object key with no charset constraint, and
|
|
209
|
+
// this message is surfaced through `result.error.message` below.
|
|
210
|
+
message: `unknown eval field "${quoteSuiteText(key)}" — did you mean "${near}"? (other custom fields are allowed and ignored)`,
|
|
112
211
|
path: [key],
|
|
113
212
|
});
|
|
114
213
|
}
|
|
@@ -128,11 +227,14 @@ export function parseEvalSuite(jsonText) {
|
|
|
128
227
|
raw = JSON.parse(jsonText);
|
|
129
228
|
}
|
|
130
229
|
catch (e) {
|
|
131
|
-
|
|
230
|
+
// V8 quotes a verbatim, unescaped slice of the offending bytes into its
|
|
231
|
+
// SyntaxError message — a suite file that is nothing but ANSI escapes would
|
|
232
|
+
// otherwise repaint the operator's terminal from vat's own error line.
|
|
233
|
+
throw new EvalInputError(`evals.json is not valid JSON: ${quoteSuiteText(e.message)}`);
|
|
132
234
|
}
|
|
133
235
|
const result = EvalSuiteSchema.safeParse(raw);
|
|
134
236
|
if (!result.success) {
|
|
135
|
-
throw new EvalInputError(`evals.json failed schema validation: ${result.error.message}`);
|
|
237
|
+
throw new EvalInputError(`evals.json failed schema validation: ${quoteSuiteBlock(result.error.message)}`);
|
|
136
238
|
}
|
|
137
239
|
// Compare ids as strings: each id names a working directory via String(id),
|
|
138
240
|
// so a numeric `1` and a string `"1"` would collide on disk even though they
|
|
@@ -143,18 +245,72 @@ export function parseEvalSuite(jsonText) {
|
|
|
143
245
|
}
|
|
144
246
|
return result.data;
|
|
145
247
|
}
|
|
248
|
+
/** The arms an {@link ArmWorkspaceDirs} covers, in dispatch order. */
|
|
249
|
+
export function armsOf(dirs) {
|
|
250
|
+
return dirs.without === undefined ? ['with'] : ['with', 'without'];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The directory segment for one arm. Throws rather than falling back to the arm
|
|
254
|
+
* NAME: a silent fallback would reinstate the unblinding this indirection exists
|
|
255
|
+
* to remove, and would do it invisibly.
|
|
256
|
+
*/
|
|
257
|
+
export function armDirSegment(dirs, arm) {
|
|
258
|
+
const segment = arm === 'with' ? dirs.with : dirs.without;
|
|
259
|
+
if (segment === undefined || segment === '') {
|
|
260
|
+
throw new EvalInputError(`Internal: no workspace directory was minted for the "${arm}" arm. ` +
|
|
261
|
+
'Every arm this run dispatches must have one; falling back to the arm name would ' +
|
|
262
|
+
'tell the executor which side of the A/B it is on.');
|
|
263
|
+
}
|
|
264
|
+
return segment;
|
|
265
|
+
}
|
|
146
266
|
/**
|
|
147
|
-
* Materialize each eval's declared input `files` into
|
|
148
|
-
* preserving relative structure.
|
|
149
|
-
* {@link EvalInputError} if a listed file does not exist (the eval cannot run
|
|
150
|
-
* Returns `workspacesRoot`.
|
|
267
|
+
* Materialize each eval's declared input `files` into
|
|
268
|
+
* `<workspacesRoot>/<armDir>/<id>/<relpath>`, preserving relative structure. Throws
|
|
269
|
+
* {@link EvalInputError} if a listed file does not exist (the eval cannot run
|
|
270
|
+
* without it). Returns `workspacesRoot`.
|
|
271
|
+
*
|
|
272
|
+
* EVERY eval gets a directory, including one that declares no `files` — it is
|
|
273
|
+
* simply left empty. An eval without a workspace used to leave the executor with
|
|
274
|
+
* no cwd of its own, so it ran inside the staged subject dir instead; for the
|
|
275
|
+
* skill-absent arm of a `--baseline` run that placed the control's cwd inside the
|
|
276
|
+
* very skill the arm exists to withhold. An empty dir costs nothing and keeps cwd
|
|
277
|
+
* shaped identically across both arms, which is what makes the A/B a comparison.
|
|
278
|
+
*
|
|
279
|
+
* WHY THE `<arm>` SEGMENT. The two arms of one eval are dispatched ADJACENTLY into
|
|
280
|
+
* a bounded-parallel pool, so they run at the same time. When they shared one
|
|
281
|
+
* directory, the control arm could simply `ls` and read whatever the treatment arm
|
|
282
|
+
* had just written — recovering the treatment's answer without ever touching the
|
|
283
|
+
* skill, and without producing a single detectable harness path. That is a channel
|
|
284
|
+
* BETWEEN the arms rather than into one of them, which is why auditing the four
|
|
285
|
+
* channels into a process (prompt, argv, cwd, env) did not surface it: each arm's
|
|
286
|
+
* own cwd was correct in isolation, and wrong as a pair. The arms must be
|
|
287
|
+
* byte-identical at start and unable to observe each other after it.
|
|
288
|
+
*
|
|
289
|
+
* WHY THE SEGMENT IS AN OPAQUE TOKEN and not the arm's name. It used to be the
|
|
290
|
+
* literal `with`/`without`, which made the executor's own working directory the
|
|
291
|
+
* loudest possible unblinding: the prompt tells each arm `Your working directory
|
|
292
|
+
* is …/vat-skill-test-ws-<token>/without/e1`, so the skill-absent arm was told, in
|
|
293
|
+
* the one string it cannot avoid reading, which side of the experiment it was on —
|
|
294
|
+
* while `buildExecutorPrompt`'s docblock promises it "must NEVER be told (directly
|
|
295
|
+
* or by implication) that it is being tested, evaluated, or graded". It also named
|
|
296
|
+
* the sibling: `../with/<id>/` was a guessable path to the treatment arm's live
|
|
297
|
+
* output. Independent random tokens fix both, and cost nothing.
|
|
151
298
|
*/
|
|
152
299
|
export function stageEvalWorkspaces(input) {
|
|
300
|
+
for (const arm of armsOf(input.armDirs)) {
|
|
301
|
+
stageEvalWorkspacesForArm(input, arm);
|
|
302
|
+
}
|
|
303
|
+
return input.workspacesRoot;
|
|
304
|
+
}
|
|
305
|
+
function stageEvalWorkspacesForArm(input, arm) {
|
|
306
|
+
const armRoot = safePath.joinUnderRoot(input.workspacesRoot, armDirSegment(input.armDirs, arm));
|
|
307
|
+
mkdirSyncReal(armRoot, { recursive: true, mode: 0o700 });
|
|
153
308
|
for (const entry of input.suite.evals) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
309
|
+
const evalWorkspace = safePath.joinUnderRoot(armRoot, String(entry.id));
|
|
310
|
+
// 0700 like the root above it — created for every eval, populated only by
|
|
311
|
+
// those declaring `files`.
|
|
312
|
+
mkdirSyncReal(evalWorkspace, { recursive: true, mode: 0o700 });
|
|
313
|
+
for (const rel of entry.files ?? []) {
|
|
158
314
|
// Containment first: a `rel` that escapes evalsDir or the workspace is a
|
|
159
315
|
// genuine "escapes the eval directory" problem and is reported as such.
|
|
160
316
|
let src;
|
|
@@ -164,11 +320,12 @@ export function stageEvalWorkspaces(input) {
|
|
|
164
320
|
dest = safePath.joinUnderRoot(evalWorkspace, rel);
|
|
165
321
|
}
|
|
166
322
|
catch (err) {
|
|
167
|
-
throw new EvalInputError(`eval ${entry.id} declares input file "${rel}" that escapes the eval directory:
|
|
323
|
+
throw new EvalInputError(`eval ${entry.id} declares input file "${quoteSuiteText(rel)}" that escapes the eval directory: ` +
|
|
324
|
+
quoteSuiteText(err.message));
|
|
168
325
|
}
|
|
169
326
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- src is contained under evalsDir via joinUnderRoot; suite is developer-authored
|
|
170
327
|
if (!existsSync(src)) {
|
|
171
|
-
throw new EvalInputError(`eval ${entry.id} declares input file "${rel}" but it is absent at ${src}`);
|
|
328
|
+
throw new EvalInputError(`eval ${entry.id} declares input file "${quoteSuiteText(rel)}" but it is absent at ${quoteSuiteText(src)}`);
|
|
172
329
|
}
|
|
173
330
|
// Copy failures (permissions, illegal filename on the host, disk) are
|
|
174
331
|
// reported accurately rather than mislabeled as a containment escape.
|
|
@@ -179,10 +336,10 @@ export function stageEvalWorkspaces(input) {
|
|
|
179
336
|
cpSync(src, dest, { recursive: true });
|
|
180
337
|
}
|
|
181
338
|
catch (err) {
|
|
182
|
-
throw new EvalInputError(`eval ${entry.id} failed to stage input file "${rel}" into the workspace:
|
|
339
|
+
throw new EvalInputError(`eval ${entry.id} failed to stage input file "${quoteSuiteText(rel)}" into the workspace: ` +
|
|
340
|
+
quoteSuiteText(err.message));
|
|
183
341
|
}
|
|
184
342
|
}
|
|
185
343
|
}
|
|
186
|
-
return input.workspacesRoot;
|
|
187
344
|
}
|
|
188
345
|
//# sourceMappingURL=eval-inputs.js.map
|