@vibe-agent-toolkit/agent-skills 0.2.0-rc.2 → 0.2.0-rc.4

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.
Files changed (213) hide show
  1. package/dist/files-config.d.ts +34 -1
  2. package/dist/files-config.d.ts.map +1 -1
  3. package/dist/files-config.js +280 -36
  4. package/dist/files-config.js.map +1 -1
  5. package/dist/fs-attribution.d.ts +45 -0
  6. package/dist/fs-attribution.d.ts.map +1 -0
  7. package/dist/fs-attribution.js +57 -0
  8. package/dist/fs-attribution.js.map +1 -0
  9. package/dist/index.d.ts +6 -5
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +9 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
  14. package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
  15. package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
  16. package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
  17. package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
  18. package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
  19. package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
  20. package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
  21. package/dist/inventory/index.d.ts +1 -1
  22. package/dist/inventory/index.d.ts.map +1 -1
  23. package/dist/inventory/index.js +1 -1
  24. package/dist/inventory/index.js.map +1 -1
  25. package/dist/inventory/serialize.d.ts +4 -4
  26. package/dist/inventory/serialize.d.ts.map +1 -1
  27. package/dist/inventory/serialize.js +6 -6
  28. package/dist/inventory/serialize.js.map +1 -1
  29. package/dist/plugin-distribution-layout.d.ts.map +1 -1
  30. package/dist/plugin-distribution-layout.js +3 -1
  31. package/dist/plugin-distribution-layout.js.map +1 -1
  32. package/dist/post-build-checks.d.ts +1 -1
  33. package/dist/post-build-checks.d.ts.map +1 -1
  34. package/dist/post-build-checks.js.map +1 -1
  35. package/dist/projection/skill-extent.d.ts +247 -0
  36. package/dist/projection/skill-extent.d.ts.map +1 -0
  37. package/dist/projection/skill-extent.js +558 -0
  38. package/dist/projection/skill-extent.js.map +1 -0
  39. package/dist/schemas/agent-skill-frontmatter.js +1 -1
  40. package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
  41. package/dist/skill-packager.d.ts +45 -7
  42. package/dist/skill-packager.d.ts.map +1 -1
  43. package/dist/skill-packager.js +123 -25
  44. package/dist/skill-packager.js.map +1 -1
  45. package/dist/skill-source/git-clone.d.ts +1 -1
  46. package/dist/skill-source/git-clone.d.ts.map +1 -1
  47. package/dist/skill-source/git-clone.js +10 -8
  48. package/dist/skill-source/git-clone.js.map +1 -1
  49. package/dist/skill-source/sources/url-source.d.ts.map +1 -1
  50. package/dist/skill-source/sources/url-source.js +2 -1
  51. package/dist/skill-source/sources/url-source.js.map +1 -1
  52. package/dist/skill-test/baseline-delta.d.ts +1575 -0
  53. package/dist/skill-test/baseline-delta.d.ts.map +1 -0
  54. package/dist/skill-test/baseline-delta.js +272 -0
  55. package/dist/skill-test/baseline-delta.js.map +1 -0
  56. package/dist/skill-test/baseline-integrity.d.ts +944 -0
  57. package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
  58. package/dist/skill-test/baseline-integrity.js +3083 -0
  59. package/dist/skill-test/baseline-integrity.js.map +1 -0
  60. package/dist/skill-test/configure-writer.js +1 -1
  61. package/dist/skill-test/configure-writer.js.map +1 -1
  62. package/dist/skill-test/declared-env.d.ts.map +1 -1
  63. package/dist/skill-test/declared-env.js +2 -1
  64. package/dist/skill-test/declared-env.js.map +1 -1
  65. package/dist/skill-test/eval-executor.d.ts +20 -4
  66. package/dist/skill-test/eval-executor.d.ts.map +1 -1
  67. package/dist/skill-test/eval-executor.js +4 -4
  68. package/dist/skill-test/eval-executor.js.map +1 -1
  69. package/dist/skill-test/eval-fragment.d.ts +80 -6
  70. package/dist/skill-test/eval-fragment.d.ts.map +1 -1
  71. package/dist/skill-test/eval-fragment.js +61 -7
  72. package/dist/skill-test/eval-fragment.js.map +1 -1
  73. package/dist/skill-test/eval-grader.d.ts +1 -1
  74. package/dist/skill-test/eval-grader.d.ts.map +1 -1
  75. package/dist/skill-test/eval-grader.js +145 -8
  76. package/dist/skill-test/eval-grader.js.map +1 -1
  77. package/dist/skill-test/eval-inputs.d.ts +107 -6
  78. package/dist/skill-test/eval-inputs.d.ts.map +1 -1
  79. package/dist/skill-test/eval-inputs.js +173 -16
  80. package/dist/skill-test/eval-inputs.js.map +1 -1
  81. package/dist/skill-test/eval-lint.d.ts +13 -0
  82. package/dist/skill-test/eval-lint.d.ts.map +1 -1
  83. package/dist/skill-test/eval-lint.js +17 -4
  84. package/dist/skill-test/eval-lint.js.map +1 -1
  85. package/dist/skill-test/executor-prompt.d.ts +13 -5
  86. package/dist/skill-test/executor-prompt.d.ts.map +1 -1
  87. package/dist/skill-test/executor-prompt.js +8 -5
  88. package/dist/skill-test/executor-prompt.js.map +1 -1
  89. package/dist/skill-test/exit-codes.d.ts +53 -16
  90. package/dist/skill-test/exit-codes.d.ts.map +1 -1
  91. package/dist/skill-test/exit-codes.js +123 -40
  92. package/dist/skill-test/exit-codes.js.map +1 -1
  93. package/dist/skill-test/fragment-merge.d.ts +1 -18
  94. package/dist/skill-test/fragment-merge.d.ts.map +1 -1
  95. package/dist/skill-test/fragment-merge.js +63 -4
  96. package/dist/skill-test/fragment-merge.js.map +1 -1
  97. package/dist/skill-test/grader-prompt.d.ts +27 -7
  98. package/dist/skill-test/grader-prompt.d.ts.map +1 -1
  99. package/dist/skill-test/grader-prompt.js +253 -61
  100. package/dist/skill-test/grader-prompt.js.map +1 -1
  101. package/dist/skill-test/grader-text.d.ts +145 -0
  102. package/dist/skill-test/grader-text.d.ts.map +1 -0
  103. package/dist/skill-test/grader-text.js +410 -0
  104. package/dist/skill-test/grader-text.js.map +1 -0
  105. package/dist/skill-test/grading-adapter.d.ts +32 -0
  106. package/dist/skill-test/grading-adapter.d.ts.map +1 -1
  107. package/dist/skill-test/grading-adapter.js +50 -3
  108. package/dist/skill-test/grading-adapter.js.map +1 -1
  109. package/dist/skill-test/grading-schema.d.ts +160 -5
  110. package/dist/skill-test/grading-schema.d.ts.map +1 -1
  111. package/dist/skill-test/grading-schema.js +66 -7
  112. package/dist/skill-test/grading-schema.js.map +1 -1
  113. package/dist/skill-test/index.d.ts +1 -1
  114. package/dist/skill-test/index.d.ts.map +1 -1
  115. package/dist/skill-test/index.js +1 -1
  116. package/dist/skill-test/index.js.map +1 -1
  117. package/dist/skill-test/lock.d.ts +56 -0
  118. package/dist/skill-test/lock.d.ts.map +1 -1
  119. package/dist/skill-test/lock.js +75 -2
  120. package/dist/skill-test/lock.js.map +1 -1
  121. package/dist/skill-test/pipeline.d.ts +16 -0
  122. package/dist/skill-test/pipeline.d.ts.map +1 -1
  123. package/dist/skill-test/pipeline.js +16 -9
  124. package/dist/skill-test/pipeline.js.map +1 -1
  125. package/dist/skill-test/preflight.d.ts +1 -1
  126. package/dist/skill-test/preflight.d.ts.map +1 -1
  127. package/dist/skill-test/preflight.js +59 -15
  128. package/dist/skill-test/preflight.js.map +1 -1
  129. package/dist/skill-test/run-harness.d.ts +578 -35
  130. package/dist/skill-test/run-harness.d.ts.map +1 -1
  131. package/dist/skill-test/run-harness.js +1281 -148
  132. package/dist/skill-test/run-harness.js.map +1 -1
  133. package/dist/skill-test/staging.d.ts +8 -2
  134. package/dist/skill-test/staging.d.ts.map +1 -1
  135. package/dist/skill-test/staging.js +8 -4
  136. package/dist/skill-test/staging.js.map +1 -1
  137. package/dist/skill-test/tool-eval-schema.d.ts +6 -6
  138. package/dist/test-input.d.ts +57 -6
  139. package/dist/test-input.d.ts.map +1 -1
  140. package/dist/test-input.js +35 -12
  141. package/dist/test-input.js.map +1 -1
  142. package/dist/validators/agent-instruction-presence.d.ts +1 -1
  143. package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
  144. package/dist/validators/agent-instruction-presence.js +2 -1
  145. package/dist/validators/agent-instruction-presence.js.map +1 -1
  146. package/dist/validators/bundled-resource-link-detection.d.ts +1 -1
  147. package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
  148. package/dist/validators/bundled-resource-link-detection.js +1 -1
  149. package/dist/validators/bundled-resource-link-detection.js.map +1 -1
  150. package/dist/validators/compat-detectors.d.ts +1 -1
  151. package/dist/validators/compat-detectors.d.ts.map +1 -1
  152. package/dist/validators/compat-detectors.js +1 -1
  153. package/dist/validators/compat-detectors.js.map +1 -1
  154. package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
  155. package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
  156. package/dist/validators/cross-skill-dependency-detection.js +1 -1
  157. package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
  158. package/dist/validators/description-style-detection.d.ts +1 -1
  159. package/dist/validators/description-style-detection.d.ts.map +1 -1
  160. package/dist/validators/description-style-detection.js +1 -1
  161. package/dist/validators/description-style-detection.js.map +1 -1
  162. package/dist/validators/frontmatter-validation.d.ts +1 -1
  163. package/dist/validators/frontmatter-validation.d.ts.map +1 -1
  164. package/dist/validators/frontmatter-validation.js +1 -1
  165. package/dist/validators/frontmatter-validation.js.map +1 -1
  166. package/dist/validators/imperative-body-detection.d.ts +1 -1
  167. package/dist/validators/imperative-body-detection.d.ts.map +1 -1
  168. package/dist/validators/imperative-body-detection.js +1 -1
  169. package/dist/validators/imperative-body-detection.js.map +1 -1
  170. package/dist/validators/index.d.ts +1 -1
  171. package/dist/validators/index.d.ts.map +1 -1
  172. package/dist/validators/kebab-case-detection.d.ts +1 -1
  173. package/dist/validators/kebab-case-detection.d.ts.map +1 -1
  174. package/dist/validators/kebab-case-detection.js +1 -1
  175. package/dist/validators/kebab-case-detection.js.map +1 -1
  176. package/dist/validators/marketplace-validator.js +1 -1
  177. package/dist/validators/marketplace-validator.js.map +1 -1
  178. package/dist/validators/packaging-validator.d.ts +96 -7
  179. package/dist/validators/packaging-validator.d.ts.map +1 -1
  180. package/dist/validators/packaging-validator.js +148 -21
  181. package/dist/validators/packaging-validator.js.map +1 -1
  182. package/dist/validators/plugin-hosted-shape.d.ts +1 -1
  183. package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
  184. package/dist/validators/plugin-hosted-shape.js +1 -1
  185. package/dist/validators/plugin-hosted-shape.js.map +1 -1
  186. package/dist/validators/plugin-recommended-fields.d.ts +1 -1
  187. package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
  188. package/dist/validators/plugin-recommended-fields.js +1 -1
  189. package/dist/validators/plugin-recommended-fields.js.map +1 -1
  190. package/dist/validators/registry-validator.js +1 -1
  191. package/dist/validators/registry-validator.js.map +1 -1
  192. package/dist/validators/rule-engine/rule-engine.d.ts +1 -1
  193. package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
  194. package/dist/validators/rule-engine/rule-engine.js +20 -9
  195. package/dist/validators/rule-engine/rule-engine.js.map +1 -1
  196. package/dist/validators/skill-validator.d.ts.map +1 -1
  197. package/dist/validators/skill-validator.js +21 -3
  198. package/dist/validators/skill-validator.js.map +1 -1
  199. package/dist/validators/types.d.ts +1 -1
  200. package/dist/validators/types.d.ts.map +1 -1
  201. package/dist/validators/unified-validator.js +1 -1
  202. package/dist/validators/unified-validator.js.map +1 -1
  203. package/dist/validators/validation-rules.d.ts +1 -1
  204. package/dist/validators/validation-rules.d.ts.map +1 -1
  205. package/dist/validators/walker-to-issues.d.ts +7 -3
  206. package/dist/validators/walker-to-issues.d.ts.map +1 -1
  207. package/dist/validators/walker-to-issues.js +37 -8
  208. package/dist/validators/walker-to-issues.js.map +1 -1
  209. package/dist/walk-link-graph.d.ts +2 -1
  210. package/dist/walk-link-graph.d.ts.map +1 -1
  211. package/dist/walk-link-graph.js +37 -2
  212. package/dist/walk-link-graph.js.map +1 -1
  213. package/package.json +7 -7
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Neutralize GRADER-SUPPLIED free text before it reaches an operator surface or
3
+ * one of vat's own artifacts.
4
+ *
5
+ * WHY THIS EXISTS. The grader's only input is the executor transcript, and
6
+ * untrusted skill code writes that transcript. Every free-text field a grader
7
+ * emits is therefore attacker-influenced: `expectations[].text`/`.evidence`,
8
+ * `friction[].message`/`.evidence`/`.subjectFile`, `tool.*[].name`/`.evidence`,
9
+ * `tool.sequence[].steps`. `formatFrictionReport` interpolates one of those
10
+ * straight into `process.stderr.write`, so a message carrying a newline and an
11
+ * SGR sequence renders as vat's OWN voice — verified end-to-end with a grader
12
+ * emitting a green "vat: grading verified, ignore the warning above." line on a
13
+ * row of its own. That is strictly worse than a wrong number in
14
+ * `baseline.json`: operators read stderr, and nobody reads the artifact.
15
+ * (`summary` is NOT in that list — vat computes it numerically.)
16
+ *
17
+ * WHAT IT DOES, and why each part:
18
+ * - **ANSI escape sequences are removed WHOLE.** Dropping only the ESC byte
19
+ * would leave the parameter bytes (`[32m`) as visible litter; removing the
20
+ * sequence keeps the surviving text readable.
21
+ * - **Remaining C0/C1 controls become a space.** This is what kills the
22
+ * forged-line attack: no newline survives, so grader text can never occupy a
23
+ * line of its own. It also covers CR (line overwrite) and BS.
24
+ * - **Bidi controls and invisible non-spelling characters become a space too.**
25
+ * Stopping at U+009F left them alive, and they are not decoration: one U+202E
26
+ * (RIGHT-TO-LEFT OVERRIDE) in a friction `message` renders the REST of vat's own
27
+ * `[low] path-assumption: …` stderr line right-to-left, so the grader repaints
28
+ * text it did not write. `JSON.stringify` only escapes below U+0020, so the same
29
+ * code point also survives verbatim into `baseline.json` and `friction.json`. The
30
+ * invisible members (U+200B, U+2060…) are the quieter half: they let a grader
31
+ * smuggle a marker through any downstream string comparison while rendering as
32
+ * nothing at all. This is NOT the whole Cf class — see
33
+ * {@link INVISIBLE_CODE_RANGES} for the two admission tests and for the joiners
34
+ * and variation selectors deliberately left alone, because ordinary words are
35
+ * spelled with them and a grader quoting a file must get the file back.
36
+ * - **Whitespace runs collapse and the value is trimmed.** After the previous
37
+ * step a 200-newline message would be 200 spaces of padding. (The Unicode
38
+ * line/paragraph separators are NOT left to the collapse: the scan folds them,
39
+ * because {@link sanitizeTextPreservingLines} has no collapse to leave them to.)
40
+ * - **Length is capped.** Bounds a flooding grader. The cap is generous enough
41
+ * for real evidence prose; the untruncated value is preserved nowhere, which
42
+ * is deliberate — an artifact nobody can trust to render is worse than a
43
+ * short one.
44
+ *
45
+ * A value that was non-empty but sanitizes away to nothing becomes
46
+ * {@link UNPRINTABLE_PLACEHOLDER} rather than the empty string, so a nonsense
47
+ * grader value cannot turn a `.min(1)` schema field into a hard run failure.
48
+ *
49
+ * SCANNED, NOT REGEXED. The obvious implementation is three regexes over
50
+ * control-character classes. Those need literal ESC/NUL bytes in this source
51
+ * file (`\u` escapes get normalized INTO literal bytes by editors and tools on
52
+ * the way in), which makes the module binary to `grep` and unreviewable in a
53
+ * diff. A forward scan over code units says the same thing in plain ASCII.
54
+ *
55
+ * WHERE IT IS APPLIED. Not once, and not redundantly — each of these is a distinct
56
+ * crossing from untrusted bytes to an operator surface or a vat artifact, and none
57
+ * of them is downstream of another:
58
+ * - `eval-fragment.ts` — `parseEvalFragment` walks EVERY string entering vat from a
59
+ * grader, plus the schema path/message it quotes when the parse fails.
60
+ * - `eval-grader.ts` — the grader-invocation error text, and both sides of the
61
+ * declared-vs-observed executable-name comparison (names are compared AFTER
62
+ * sanitizing, so an invisible smuggled into one side cannot split the match).
63
+ * - `run-harness.ts` — `formatFrictionReport`, the stderr render, which re-reads
64
+ * `friction.json` from a harness dir that same-uid skill code can reach.
65
+ * - `eval-inputs.ts` — `quoteSuiteText` / `quoteSuiteBlock`, for SUITE-authored text
66
+ * (untrusted too: `resolveEvalSuitePath` will harvest a suite out of a fetched
67
+ * artifact, i.e. out of the skill under test).
68
+ * - `eval-lint.ts` — `lintToolExpectationExecutables`, via `quoteSuiteText`. Every
69
+ * executable name it quotes comes from the suite (`toolExpectations.*`, an
70
+ * unconstrained `z.array(z.string().min(1))` on a `.passthrough()` entry) or from
71
+ * the subject skill's own manifest, and `run-harness.ts` writes the advisory to
72
+ * stderr with no sanitizer of its own — at Step 5.5, AHEAD of the
73
+ * `--i-understand-this-runs-skill-code` gate and ahead of the `--dry-run`
74
+ * short-circuit, so this one paints a run that spawned nothing.
75
+ * - `baseline-integrity.ts` — `BaselineScanDegradation.detail`, built from a `cd`
76
+ * argument lifted verbatim out of the CONTROL ARM's transcript. Sanitized where the
77
+ * detail is CONSTRUCTED, not at the stderr write, so the one call covers both
78
+ * surfaces: `run-harness.ts` (`baselineContaminationFor`) writes it to stderr AND
79
+ * vat stamps the same bytes into `baselineIntegrity.degraded` in `baseline.json` —
80
+ * which a stderr-side fix would miss, because `degraded` is attached to the
81
+ * fragment AFTER `parseEvalFragment` has sanitized it.
82
+ * - `grading-adapter.ts` — the schema path and message quoted when the aggregate
83
+ * `grading.json` parse fails.
84
+ *
85
+ * That list is the whole of it, and keeping it whole is the point: this file's own
86
+ * standard is that a function's docblock and its behaviour disagreeing is how a
87
+ * security claim rots, and a LIST that has fallen behind the call sites rots the same
88
+ * way in the other direction — it under-claims, so a reader goes looking for a hole
89
+ * that was closed and trusts a boundary that was never added. `git grep
90
+ * 'sanitizeGraderText\|sanitizeTextPreservingLines'` is the check.
91
+ */
92
+ /**
93
+ * Longest grader-supplied free-text value vat will carry. ~20 terminal lines —
94
+ * enough for a real evidence excerpt, small enough that one item cannot own the
95
+ * operator's screen.
96
+ */
97
+ export declare const MAX_GRADER_TEXT_LENGTH = 2000;
98
+ /** Substituted for a non-empty value that sanitizes away to nothing. */
99
+ export declare const UNPRINTABLE_PLACEHOLDER = "(unprintable)";
100
+ /** Sanitize one grader-supplied string. Pure. See the module docblock for the rules. */
101
+ export declare function sanitizeGraderText(value: string): string;
102
+ /**
103
+ * The LINE-PRESERVING sanitizer, for untrusted text whose STRUCTURE is the
104
+ * message.
105
+ *
106
+ * WHY IT EXISTS RATHER THAN A SECOND CALL TO {@link sanitizeGraderText}. Zod's
107
+ * `.strict()`/schema failure text is a multi-line, indented list of issues, one
108
+ * per offending path, and it is the only thing telling an adopter WHICH eval in a
109
+ * 200-eval suite is malformed. Folding it with the single-line sanitizer collapses
110
+ * every issue onto one 2000-character line and then truncates it — so the common
111
+ * case (a real typo in a real suite) gets materially worse in exchange for
112
+ * neutralizing bytes the suite author almost never wrote. This keeps the shape and
113
+ * still removes everything that can paint: escape sequences whole, every C0/C1
114
+ * control except LF, and every member of {@link INVISIBLE_CODE_RANGES}.
115
+ *
116
+ * THAT LAST CLAUSE USED TO BE FALSE, and the sibling's whitespace collapse was why.
117
+ * U+FEFF, U+2028 and U+2029 were left OUT of the scan on the grounds that they are
118
+ * `\s` and "the collapse eats them" — true of exactly one of the two consumers.
119
+ * This function has no collapse, so all three reached `process.stderr` through
120
+ * `quoteSuiteBlock` -> `EvalInputError` intact, two of them being separators some
121
+ * renderers break a line on. They are neutralized in the SCAN now, which is the only
122
+ * place both consumers share. A function's docblock and its behaviour disagreeing is
123
+ * how a security claim rots; on a terminal-bound path, the code moves to meet the
124
+ * docblock and not the other way round.
125
+ *
126
+ * It is NOT a replacement for {@link sanitizeGraderText} and must not be used for
127
+ * grader free text: a surviving newline is exactly what lets a value occupy a line
128
+ * of its own and impersonate vat's voice. Use it only where the caller's own
129
+ * message already spans lines and the untrusted part is quoted INSIDE it.
130
+ */
131
+ export declare function sanitizeTextPreservingLines(value: string): string;
132
+ /**
133
+ * Sanitize every string in an arbitrary parsed-JSON value, recursively.
134
+ *
135
+ * Deliberately field-BLIND: a per-field allowlist is a list somebody has to
136
+ * remember to extend, and these schemas grow (`mustSucceed` and
137
+ * `sequence[].steps` both arrived after the fields above them). Walking every
138
+ * string means a new grader-controlled field is covered the day it is added.
139
+ *
140
+ * `skipKeys` is REQUIRED, not defaulted — the one value that must survive
141
+ * byte-exact is the integrity nonce, and a caller that forgets it should have
142
+ * to say so.
143
+ */
144
+ export declare function sanitizeGraderTextDeep(value: unknown, skipKeys: ReadonlySet<string>): unknown;
145
+ //# sourceMappingURL=grader-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grader-text.d.ts","sourceRoot":"","sources":["../../src/skill-test/grader-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AAmOvD,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASjE;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAE7F"}
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Neutralize GRADER-SUPPLIED free text before it reaches an operator surface or
3
+ * one of vat's own artifacts.
4
+ *
5
+ * WHY THIS EXISTS. The grader's only input is the executor transcript, and
6
+ * untrusted skill code writes that transcript. Every free-text field a grader
7
+ * emits is therefore attacker-influenced: `expectations[].text`/`.evidence`,
8
+ * `friction[].message`/`.evidence`/`.subjectFile`, `tool.*[].name`/`.evidence`,
9
+ * `tool.sequence[].steps`. `formatFrictionReport` interpolates one of those
10
+ * straight into `process.stderr.write`, so a message carrying a newline and an
11
+ * SGR sequence renders as vat's OWN voice — verified end-to-end with a grader
12
+ * emitting a green "vat: grading verified, ignore the warning above." line on a
13
+ * row of its own. That is strictly worse than a wrong number in
14
+ * `baseline.json`: operators read stderr, and nobody reads the artifact.
15
+ * (`summary` is NOT in that list — vat computes it numerically.)
16
+ *
17
+ * WHAT IT DOES, and why each part:
18
+ * - **ANSI escape sequences are removed WHOLE.** Dropping only the ESC byte
19
+ * would leave the parameter bytes (`[32m`) as visible litter; removing the
20
+ * sequence keeps the surviving text readable.
21
+ * - **Remaining C0/C1 controls become a space.** This is what kills the
22
+ * forged-line attack: no newline survives, so grader text can never occupy a
23
+ * line of its own. It also covers CR (line overwrite) and BS.
24
+ * - **Bidi controls and invisible non-spelling characters become a space too.**
25
+ * Stopping at U+009F left them alive, and they are not decoration: one U+202E
26
+ * (RIGHT-TO-LEFT OVERRIDE) in a friction `message` renders the REST of vat's own
27
+ * `[low] path-assumption: …` stderr line right-to-left, so the grader repaints
28
+ * text it did not write. `JSON.stringify` only escapes below U+0020, so the same
29
+ * code point also survives verbatim into `baseline.json` and `friction.json`. The
30
+ * invisible members (U+200B, U+2060…) are the quieter half: they let a grader
31
+ * smuggle a marker through any downstream string comparison while rendering as
32
+ * nothing at all. This is NOT the whole Cf class — see
33
+ * {@link INVISIBLE_CODE_RANGES} for the two admission tests and for the joiners
34
+ * and variation selectors deliberately left alone, because ordinary words are
35
+ * spelled with them and a grader quoting a file must get the file back.
36
+ * - **Whitespace runs collapse and the value is trimmed.** After the previous
37
+ * step a 200-newline message would be 200 spaces of padding. (The Unicode
38
+ * line/paragraph separators are NOT left to the collapse: the scan folds them,
39
+ * because {@link sanitizeTextPreservingLines} has no collapse to leave them to.)
40
+ * - **Length is capped.** Bounds a flooding grader. The cap is generous enough
41
+ * for real evidence prose; the untruncated value is preserved nowhere, which
42
+ * is deliberate — an artifact nobody can trust to render is worse than a
43
+ * short one.
44
+ *
45
+ * A value that was non-empty but sanitizes away to nothing becomes
46
+ * {@link UNPRINTABLE_PLACEHOLDER} rather than the empty string, so a nonsense
47
+ * grader value cannot turn a `.min(1)` schema field into a hard run failure.
48
+ *
49
+ * SCANNED, NOT REGEXED. The obvious implementation is three regexes over
50
+ * control-character classes. Those need literal ESC/NUL bytes in this source
51
+ * file (`\u` escapes get normalized INTO literal bytes by editors and tools on
52
+ * the way in), which makes the module binary to `grep` and unreviewable in a
53
+ * diff. A forward scan over code units says the same thing in plain ASCII.
54
+ *
55
+ * WHERE IT IS APPLIED. Not once, and not redundantly — each of these is a distinct
56
+ * crossing from untrusted bytes to an operator surface or a vat artifact, and none
57
+ * of them is downstream of another:
58
+ * - `eval-fragment.ts` — `parseEvalFragment` walks EVERY string entering vat from a
59
+ * grader, plus the schema path/message it quotes when the parse fails.
60
+ * - `eval-grader.ts` — the grader-invocation error text, and both sides of the
61
+ * declared-vs-observed executable-name comparison (names are compared AFTER
62
+ * sanitizing, so an invisible smuggled into one side cannot split the match).
63
+ * - `run-harness.ts` — `formatFrictionReport`, the stderr render, which re-reads
64
+ * `friction.json` from a harness dir that same-uid skill code can reach.
65
+ * - `eval-inputs.ts` — `quoteSuiteText` / `quoteSuiteBlock`, for SUITE-authored text
66
+ * (untrusted too: `resolveEvalSuitePath` will harvest a suite out of a fetched
67
+ * artifact, i.e. out of the skill under test).
68
+ * - `eval-lint.ts` — `lintToolExpectationExecutables`, via `quoteSuiteText`. Every
69
+ * executable name it quotes comes from the suite (`toolExpectations.*`, an
70
+ * unconstrained `z.array(z.string().min(1))` on a `.passthrough()` entry) or from
71
+ * the subject skill's own manifest, and `run-harness.ts` writes the advisory to
72
+ * stderr with no sanitizer of its own — at Step 5.5, AHEAD of the
73
+ * `--i-understand-this-runs-skill-code` gate and ahead of the `--dry-run`
74
+ * short-circuit, so this one paints a run that spawned nothing.
75
+ * - `baseline-integrity.ts` — `BaselineScanDegradation.detail`, built from a `cd`
76
+ * argument lifted verbatim out of the CONTROL ARM's transcript. Sanitized where the
77
+ * detail is CONSTRUCTED, not at the stderr write, so the one call covers both
78
+ * surfaces: `run-harness.ts` (`baselineContaminationFor`) writes it to stderr AND
79
+ * vat stamps the same bytes into `baselineIntegrity.degraded` in `baseline.json` —
80
+ * which a stderr-side fix would miss, because `degraded` is attached to the
81
+ * fragment AFTER `parseEvalFragment` has sanitized it.
82
+ * - `grading-adapter.ts` — the schema path and message quoted when the aggregate
83
+ * `grading.json` parse fails.
84
+ *
85
+ * That list is the whole of it, and keeping it whole is the point: this file's own
86
+ * standard is that a function's docblock and its behaviour disagreeing is how a
87
+ * security claim rots, and a LIST that has fallen behind the call sites rots the same
88
+ * way in the other direction — it under-claims, so a reader goes looking for a hole
89
+ * that was closed and trusts a boundary that was never added. `git grep
90
+ * 'sanitizeGraderText\|sanitizeTextPreservingLines'` is the check.
91
+ */
92
+ /**
93
+ * Longest grader-supplied free-text value vat will carry. ~20 terminal lines —
94
+ * enough for a real evidence excerpt, small enough that one item cannot own the
95
+ * operator's screen.
96
+ */
97
+ export const MAX_GRADER_TEXT_LENGTH = 2000;
98
+ /** Substituted for a non-empty value that sanitizes away to nothing. */
99
+ export const UNPRINTABLE_PLACEHOLDER = '(unprintable)';
100
+ const TRUNCATION_MARKER = '... (truncated)';
101
+ const ESC = 0x1b;
102
+ const BEL = 0x07;
103
+ const C0_MAX = 0x1f;
104
+ const DEL = 0x7f;
105
+ const C1_MAX = 0x9f;
106
+ const LEFT_BRACKET = 0x5b;
107
+ const RIGHT_BRACKET = 0x5d;
108
+ const BACKSLASH = 0x5c;
109
+ /** A CSI sequence ends at its first byte in `@`..`~`. */
110
+ const CSI_FINAL_MIN = 0x40;
111
+ const CSI_FINAL_MAX = 0x7e;
112
+ /** Intermediate bytes of an nF escape sequence (` `..`/`), e.g. the `(` of `ESC ( B`. */
113
+ const INTERMEDIATE_MIN = 0x20;
114
+ const INTERMEDIATE_MAX = 0x2f;
115
+ /** The leading half of a surrogate pair; see {@link capLength}. */
116
+ const HIGH_SURROGATE_MIN = 0xd800;
117
+ const HIGH_SURROGATE_MAX = 0xdbff;
118
+ /** Above this, a code point is written as a surrogate PAIR; see {@link capLength}. */
119
+ const MAX_BMP_CODE_POINT = 0xffff;
120
+ const WHITESPACE_RUN = /\s+/gu;
121
+ /** Guard against a pathologically nested grader fragment; the strict parse rejects it anyway. */
122
+ const MAX_SANITIZE_DEPTH = 20;
123
+ /** Line feed — the ONE control {@link sanitizeTextPreservingLines} keeps. */
124
+ const LINE_FEED = 0x0a;
125
+ /**
126
+ * Invisible code points above U+009F that are neutralized exactly like a C0/C1
127
+ * control, because they do the same job by other means.
128
+ *
129
+ * ⚠️ WRITTEN AS NUMBERS, like every other constant in this file, and for the same
130
+ * reason: a `\u202E` escape in a regex literal gets normalized into the real code
131
+ * point by editors and tooling on the way in, which makes the module unreviewable
132
+ * in a diff and unfindable by `grep`. Build test inputs with `String.fromCodePoint`.
133
+ *
134
+ * THE RULE, and it is not "the Cf class". Two admission tests; a code point has to
135
+ * pass one of them:
136
+ *
137
+ * (a) **It is a bidi or shaping control** — it changes how the text AROUND it is
138
+ * ordered or directed, which is the forgery this module exists to stop. U+202E
139
+ * is the verified case: it renders the REST of vat's own
140
+ * `[low] path-assumption: …` stderr line right-to-left.
141
+ * (b) **It is invisible AND spells nothing** — no word in any script is written with
142
+ * it, so removing it cannot change what a quotation says, while keeping it lets
143
+ * a grader hide a difference from a reader and from every downstream string
144
+ * comparison (`eval-grader.ts` compares tool names across both sides of a run,
145
+ * sanitized on each).
146
+ *
147
+ * Member by member:
148
+ * - U+00AD SOFT HYPHEN — (b) a hyphenation HINT; no word is spelled with one.
149
+ * - U+061C ARABIC LETTER MARK — (a) the Arabic analogue of LRM/RLM.
150
+ * - U+200B ZERO WIDTH SPACE — (b) a line-break hint, invisible, spells nothing.
151
+ * - U+200E…U+200F LRM/RLM — (a) directional MARKS: they re-resolve the direction of
152
+ * the neutral characters beside them, so they reorder text they did not write.
153
+ * - U+2028…U+2029 LINE and PARAGRAPH SEPARATOR — some renderers break a line on
154
+ * them, which is the "occupy a line of your own" attack spelled with a different
155
+ * code point. Folded to a space, never to LF, even in the line-preserving path.
156
+ * - U+202A…U+202E — (a) the bidi embeddings and OVERRIDES.
157
+ * - U+2060…U+2064 — (b) word joiner and the invisible math operators.
158
+ * - U+2066…U+2069 — (a) the bidi ISOLATES (U+2066 LRI verified surviving).
159
+ * - U+206A…U+206F — (a) the deprecated shaping controls (inhibit symmetric swapping,
160
+ * Arabic form shaping, nominal digit shapes); absent from modern text entirely.
161
+ * - U+FEFF ZERO WIDTH NO-BREAK SPACE — (b). It is `\s`, so {@link sanitizeGraderText}'s
162
+ * whitespace collapse would eat it anyway; {@link sanitizeTextPreservingLines} has
163
+ * no collapse and used to ship it to stderr intact, so it is neutralized HERE.
164
+ * - U+FFF9…U+FFFB — (b) interlinear annotation, which hides its own payload.
165
+ *
166
+ * DELIBERATELY ABSENT, because they fail BOTH tests — zero directional power, and
167
+ * real words ARE spelled with them:
168
+ * - U+FE00…U+FE0F variation selectors (category Mn, not Cf). How `⚠️` and most emoji
169
+ * are spelled, vat's own banner glyph included.
170
+ * - U+200C ZERO WIDTH NON-JOINER — orthographically load-bearing in Persian, Hindi,
171
+ * Bengali and Malayalam. `می<ZWNJ>خواهم` is two words; folding it to a space gave
172
+ * back a different string than the file the grader was quoting.
173
+ * - U+200D ZERO WIDTH JOINER — how a multi-person emoji and a flag sequence are
174
+ * spelled. Folding it turned `👨<ZWJ>👩<ZWJ>👧` into three separate glyphs.
175
+ * - U+180E MONGOLIAN VOWEL SEPARATOR — orthographic in Mongolian (it selects the
176
+ * letterform of the syllable before it) and reorders nothing.
177
+ *
178
+ * The residual cost of that exclusion is real and accepted: a grader can still
179
+ * smuggle an invisible joiner into a string a human compares by eye. It is the same
180
+ * trade the variation-selector exclusion always made, and the standard this file
181
+ * already stated — "dropping them would mangle ordinary text" — decides it the same
182
+ * way. What it must NOT do is state that standard and then break it.
183
+ */
184
+ const INVISIBLE_CODE_RANGES = [
185
+ [0x00ad, 0x00ad],
186
+ [0x061c, 0x061c],
187
+ [0x200b, 0x200b],
188
+ [0x200e, 0x200f],
189
+ [0x2028, 0x2029],
190
+ [0x202a, 0x202e],
191
+ [0x2060, 0x2064],
192
+ [0x2066, 0x206f],
193
+ [0xfeff, 0xfeff],
194
+ [0xfff9, 0xfffb],
195
+ ];
196
+ /** Lowest code point in {@link INVISIBLE_CODE_RANGES}, so ASCII bails on one compare. */
197
+ const INVISIBLE_CODE_MIN = 0x00ad;
198
+ function isInvisibleCode(code) {
199
+ if (code < INVISIBLE_CODE_MIN)
200
+ return false;
201
+ return INVISIBLE_CODE_RANGES.some(([low, high]) => code >= low && code <= high);
202
+ }
203
+ function isControlCode(code) {
204
+ return code <= C0_MAX || (code >= DEL && code <= C1_MAX) || isInvisibleCode(code);
205
+ }
206
+ /** Index of the first code unit AFTER a CSI sequence whose parameters start at `from`. */
207
+ function endOfCsi(value, from) {
208
+ let i = from;
209
+ while (i < value.length) {
210
+ const code = value.codePointAt(i) ?? Number.NaN;
211
+ i += 1;
212
+ if (code >= CSI_FINAL_MIN && code <= CSI_FINAL_MAX)
213
+ return i;
214
+ }
215
+ return i;
216
+ }
217
+ /** Index of the first code unit AFTER an OSC sequence whose payload starts at `from`. */
218
+ function endOfOsc(value, from) {
219
+ let i = from;
220
+ while (i < value.length) {
221
+ const code = value.codePointAt(i) ?? Number.NaN;
222
+ if (code === BEL)
223
+ return i + 1;
224
+ // String Terminator is ESC followed by a backslash; a bare ESC ends it too.
225
+ if (code === ESC)
226
+ return i + (value.codePointAt(i + 1) === BACKSLASH ? 2 : 1);
227
+ i += 1;
228
+ }
229
+ return i;
230
+ }
231
+ /**
232
+ * Index of the first code unit AFTER an nF sequence (`ESC` intermediates final),
233
+ * e.g. the charset selection `ESC ( B`, whose intermediates start at `from`.
234
+ */
235
+ function endOfNf(value, from) {
236
+ let i = from;
237
+ while (i < value.length) {
238
+ const code = value.codePointAt(i) ?? Number.NaN;
239
+ i += 1;
240
+ if (code < INTERMEDIATE_MIN || code > INTERMEDIATE_MAX)
241
+ return i;
242
+ }
243
+ return i;
244
+ }
245
+ /** Index of the first code unit AFTER the escape sequence introduced by the ESC at `start`. */
246
+ function endOfEscapeSequence(value, start) {
247
+ if (start + 1 >= value.length)
248
+ return start + 1;
249
+ const second = value.codePointAt(start + 1) ?? Number.NaN;
250
+ if (second === LEFT_BRACKET)
251
+ return endOfCsi(value, start + 2);
252
+ if (second === RIGHT_BRACKET)
253
+ return endOfOsc(value, start + 2);
254
+ if (second >= INTERMEDIATE_MIN && second <= INTERMEDIATE_MAX)
255
+ return endOfNf(value, start + 1);
256
+ // Any remaining two-code-unit form (RIS, NEL, ...).
257
+ return start + 2;
258
+ }
259
+ /**
260
+ * Drop every escape sequence and map every remaining control code to a space.
261
+ *
262
+ * `preserveNewlines` keeps LF — and ONLY LF — as itself, for
263
+ * {@link sanitizeTextPreservingLines}. CR still becomes a space even then: the
264
+ * line-overwrite attack it carries is not made safe by the caller wanting lines.
265
+ */
266
+ function stripEscapesAndControls(value, preserveNewlines = false) {
267
+ const out = [];
268
+ let i = 0;
269
+ while (i < value.length) {
270
+ const code = value.codePointAt(i) ?? Number.NaN;
271
+ if (code === ESC) {
272
+ i = endOfEscapeSequence(value, i);
273
+ }
274
+ else if (isControlCode(code)) {
275
+ out.push(preserveNewlines && code === LINE_FEED ? '\n' : ' ');
276
+ i += 1;
277
+ }
278
+ else {
279
+ out.push(value.charAt(i));
280
+ i += 1;
281
+ }
282
+ }
283
+ return out.join('');
284
+ }
285
+ /**
286
+ * Truncate to at most `max` code units, spending the tail on a marker that says so.
287
+ *
288
+ * THE CUT BACKS OFF ONE CODE UNIT rather than landing between the halves of a
289
+ * surrogate pair. A naive `slice` on a value ending in an astral character (1984
290
+ * ASCII characters plus one emoji is enough) leaves a LONE HIGH SURROGATE, which
291
+ * persists into `grading.json` and `baseline.json`. The artifact stays valid JSON —
292
+ * Node's `JSON.stringify` is well-formed and escapes it — but a consumer decoding
293
+ * it gets an unpaired surrogate, and stderr renders U+FFFD. Well-formedness is a
294
+ * correctness property of what this function emits, so it is enforced here.
295
+ *
296
+ * GRAPHEME CLUSTERS ARE DELIBERATELY NOT HANDLED. A cut inside a cluster — between
297
+ * a base and its combining mark, or inside a ZWJ emoji sequence (now that the
298
+ * joiners survive, see {@link INVISIBLE_CODE_RANGES}) — yields a WELL-FORMED string
299
+ * that renders as two glyphs instead of one. That is cosmetic. Segmenting for it
300
+ * would mean `Intl.Segmenter` over the whole value on every capped field, for a
301
+ * locale-dependent answer, to fix an appearance rather than a defect. The line is
302
+ * well-formedness, not beauty.
303
+ */
304
+ function capLength(value, max) {
305
+ if (value.length <= max)
306
+ return value;
307
+ const cut = max - TRUNCATION_MARKER.length;
308
+ // A high surrogate immediately before the cut has its partner AT the cut (or is
309
+ // itself already lone) — either way it must not be the last thing kept.
310
+ // `codePointAt` reports the COMBINED code point when a well-formed pair STARTS at the
311
+ // index, so a value above the BMP means the cut would fall between the two halves. A
312
+ // value still inside the high-surrogate block means that half never had a partner and
313
+ // is already lone. Either way it must not be the last thing kept. (A LOW surrogate
314
+ // reads back as itself, which is correct: its partner is at `cut - 2`, so the pair
315
+ // survives the cut intact.)
316
+ const last = value.codePointAt(cut - 1) ?? Number.NaN;
317
+ const cutSplitsPair = last > MAX_BMP_CODE_POINT || (last >= HIGH_SURROGATE_MIN && last <= HIGH_SURROGATE_MAX);
318
+ const safeCut = cutSplitsPair ? cut - 1 : cut;
319
+ return value.slice(0, safeCut) + TRUNCATION_MARKER;
320
+ }
321
+ /** Sanitize one grader-supplied string. Pure. See the module docblock for the rules. */
322
+ export function sanitizeGraderText(value) {
323
+ const collapsed = stripEscapesAndControls(value).replaceAll(WHITESPACE_RUN, ' ').trim();
324
+ if (collapsed.length === 0)
325
+ return value.length === 0 ? '' : UNPRINTABLE_PLACEHOLDER;
326
+ return capLength(collapsed, MAX_GRADER_TEXT_LENGTH);
327
+ }
328
+ /**
329
+ * Most lines {@link sanitizeTextPreservingLines} will let through, and the total
330
+ * budget across them.
331
+ *
332
+ * Two caps rather than one because the two floods are different shapes: 4000
333
+ * characters on ONE line is a wrapped paragraph, 4000 NEWLINES is the operator's
334
+ * whole scrollback. The line cap is the one that protects the screen; the length
335
+ * cap protects `Summary:` and the artifact.
336
+ */
337
+ const MAX_MULTILINE_TEXT_LINES = 40;
338
+ const MAX_MULTILINE_TEXT_LENGTH = 4000;
339
+ /**
340
+ * The LINE-PRESERVING sanitizer, for untrusted text whose STRUCTURE is the
341
+ * message.
342
+ *
343
+ * WHY IT EXISTS RATHER THAN A SECOND CALL TO {@link sanitizeGraderText}. Zod's
344
+ * `.strict()`/schema failure text is a multi-line, indented list of issues, one
345
+ * per offending path, and it is the only thing telling an adopter WHICH eval in a
346
+ * 200-eval suite is malformed. Folding it with the single-line sanitizer collapses
347
+ * every issue onto one 2000-character line and then truncates it — so the common
348
+ * case (a real typo in a real suite) gets materially worse in exchange for
349
+ * neutralizing bytes the suite author almost never wrote. This keeps the shape and
350
+ * still removes everything that can paint: escape sequences whole, every C0/C1
351
+ * control except LF, and every member of {@link INVISIBLE_CODE_RANGES}.
352
+ *
353
+ * THAT LAST CLAUSE USED TO BE FALSE, and the sibling's whitespace collapse was why.
354
+ * U+FEFF, U+2028 and U+2029 were left OUT of the scan on the grounds that they are
355
+ * `\s` and "the collapse eats them" — true of exactly one of the two consumers.
356
+ * This function has no collapse, so all three reached `process.stderr` through
357
+ * `quoteSuiteBlock` -> `EvalInputError` intact, two of them being separators some
358
+ * renderers break a line on. They are neutralized in the SCAN now, which is the only
359
+ * place both consumers share. A function's docblock and its behaviour disagreeing is
360
+ * how a security claim rots; on a terminal-bound path, the code moves to meet the
361
+ * docblock and not the other way round.
362
+ *
363
+ * It is NOT a replacement for {@link sanitizeGraderText} and must not be used for
364
+ * grader free text: a surviving newline is exactly what lets a value occupy a line
365
+ * of its own and impersonate vat's voice. Use it only where the caller's own
366
+ * message already spans lines and the untrusted part is quoted INSIDE it.
367
+ */
368
+ export function sanitizeTextPreservingLines(value) {
369
+ const stripped = stripEscapesAndControls(value, true).trim();
370
+ if (stripped.length === 0)
371
+ return value.length === 0 ? '' : UNPRINTABLE_PLACEHOLDER;
372
+ const lines = stripped.split('\n');
373
+ const kept = lines.slice(0, MAX_MULTILINE_TEXT_LINES);
374
+ if (lines.length > kept.length) {
375
+ kept.push(`... and ${lines.length - kept.length} more line(s) (truncated)`);
376
+ }
377
+ return capLength(kept.join('\n'), MAX_MULTILINE_TEXT_LENGTH);
378
+ }
379
+ function walk(value, skipKeys, depth) {
380
+ if (depth > MAX_SANITIZE_DEPTH)
381
+ return value;
382
+ if (typeof value === 'string')
383
+ return sanitizeGraderText(value);
384
+ if (Array.isArray(value))
385
+ return value.map((item) => walk(item, skipKeys, depth + 1));
386
+ if (typeof value === 'object' && value !== null) {
387
+ const out = {};
388
+ for (const [key, val] of Object.entries(value)) {
389
+ out[key] = skipKeys.has(key) ? val : walk(val, skipKeys, depth + 1);
390
+ }
391
+ return out;
392
+ }
393
+ return value;
394
+ }
395
+ /**
396
+ * Sanitize every string in an arbitrary parsed-JSON value, recursively.
397
+ *
398
+ * Deliberately field-BLIND: a per-field allowlist is a list somebody has to
399
+ * remember to extend, and these schemas grow (`mustSucceed` and
400
+ * `sequence[].steps` both arrived after the fields above them). Walking every
401
+ * string means a new grader-controlled field is covered the day it is added.
402
+ *
403
+ * `skipKeys` is REQUIRED, not defaulted — the one value that must survive
404
+ * byte-exact is the integrity nonce, and a caller that forgets it should have
405
+ * to say so.
406
+ */
407
+ export function sanitizeGraderTextDeep(value, skipKeys) {
408
+ return walk(value, skipKeys, 0);
409
+ }
410
+ //# sourceMappingURL=grader-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grader-text.js","sourceRoot":"","sources":["../../src/skill-test/grader-text.ts"],"names":[],"mappings":"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"}
@@ -12,13 +12,31 @@ export interface NormalizedGrading {
12
12
  passed: number;
13
13
  total: number;
14
14
  };
15
+ /**
16
+ * Every graded expectation, flattened across all evals. `evalId` names the eval
17
+ * the entry came from — it is what lets a reader line the two `--baseline`
18
+ * artifacts up PER EVAL instead of only in aggregate. Optional for the same
19
+ * reason `runNonce` is: {@link parseGradingJson} builds a `NormalizedGrading`
20
+ * from an externally produced grading.json, which carries no such attribution.
21
+ */
15
22
  expectations: {
16
23
  text: string;
17
24
  passed: boolean;
18
25
  evidence?: string;
26
+ evalId?: string;
19
27
  }[];
20
28
  /** Per-run integrity nonce the experimenter copied from its prompt (if present). */
21
29
  runNonce?: string;
30
+ /**
31
+ * Which `--baseline` arm produced this report: `'with'` (the skill declared —
32
+ * grading.json) or `'without'` (the control — baseline.json). Stamped by
33
+ * {@link import('./fragment-merge.js').mergeFragmentsToGrading}, which is the
34
+ * only producer that knows the arm. Optional for the same reason `runNonce` is:
35
+ * {@link parseGradingJson} normalizes an externally produced grading.json that
36
+ * legitimately carries no arm, and inventing one there would be a lie about
37
+ * provenance rather than a missing field.
38
+ */
39
+ arm?: 'with' | 'without';
22
40
  }
23
41
  /**
24
42
  * Thrown when a per-eval grader fragment's integrity nonce is absent or does not
@@ -30,6 +48,20 @@ export interface NormalizedGrading {
30
48
  export declare class GradingNonceError extends Error {
31
49
  constructor(message: string);
32
50
  }
51
+ /**
52
+ * Thrown when a per-eval grader fragment's `arm` disagrees with the arm the
53
+ * merge was told it is assembling. Deliberately distinct from
54
+ * {@link GradingNonceError}: a fragment can carry a perfectly valid nonce for
55
+ * THIS run and still belong to the other arm, and that is a different failure —
56
+ * not a forgery, but a mix-up that would fold a control-arm verdict into the
57
+ * treatment number (or the reverse). Either way the merged total is no longer a
58
+ * measurement of one arm, which is the entire premise of the `--baseline` A/B,
59
+ * so the merge refuses rather than emit a mislabelled artifact that downstream
60
+ * readers would take as authoritative.
61
+ */
62
+ export declare class GradingArmError extends Error {
63
+ constructor(message: string);
64
+ }
33
65
  export declare function parseGradingJson(raw: unknown): NormalizedGrading;
34
66
  /** The authoritative eval verdict, recomputed from per-expectation `passed` flags. */
35
67
  export interface GradingVerdict {
@@ -1 +1 @@
1
- {"version":3,"file":"grading-adapter.d.ts","sourceRoot":"","sources":["../../src/skill-test/grading-adapter.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAQ5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACrE,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAS5B;AAaD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAwBhE;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAqB1E"}
1
+ {"version":3,"file":"grading-adapter.d.ts","sourceRoot":"","sources":["../../src/skill-test/grading-adapter.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAQ5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C;;;;;;OAMG;IACH,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtF,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAS5B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAU5B;AAaD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAiDhE;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAqB1E"}