@vibe-agent-toolkit/agent-skills 0.1.39-rc.9 → 0.1.39

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 (95) hide show
  1. package/dist/files-config.d.ts +61 -0
  2. package/dist/files-config.d.ts.map +1 -1
  3. package/dist/files-config.js +262 -6
  4. package/dist/files-config.js.map +1 -1
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugin-distribution-layout.d.ts +70 -0
  10. package/dist/plugin-distribution-layout.d.ts.map +1 -0
  11. package/dist/plugin-distribution-layout.js +97 -0
  12. package/dist/plugin-distribution-layout.js.map +1 -0
  13. package/dist/skill-packager.d.ts +43 -1
  14. package/dist/skill-packager.d.ts.map +1 -1
  15. package/dist/skill-packager.js +131 -53
  16. package/dist/skill-packager.js.map +1 -1
  17. package/dist/skill-source/git-clone.d.ts +2 -2
  18. package/dist/skill-source/git-clone.d.ts.map +1 -1
  19. package/dist/skill-source/git-clone.js +33 -10
  20. package/dist/skill-source/git-clone.js.map +1 -1
  21. package/dist/skill-source/sources/url-source.d.ts.map +1 -1
  22. package/dist/skill-source/sources/url-source.js +23 -8
  23. package/dist/skill-source/sources/url-source.js.map +1 -1
  24. package/dist/skill-source/sources/workspace-source.d.ts.map +1 -1
  25. package/dist/skill-source/sources/workspace-source.js +16 -8
  26. package/dist/skill-source/sources/workspace-source.js.map +1 -1
  27. package/dist/skill-source/stage.js +6 -2
  28. package/dist/skill-source/stage.js.map +1 -1
  29. package/dist/skill-test/configure-writer.d.ts +19 -11
  30. package/dist/skill-test/configure-writer.d.ts.map +1 -1
  31. package/dist/skill-test/configure-writer.js +23 -15
  32. package/dist/skill-test/configure-writer.js.map +1 -1
  33. package/dist/skill-test/declared-env.d.ts +72 -0
  34. package/dist/skill-test/declared-env.d.ts.map +1 -0
  35. package/dist/skill-test/declared-env.js +85 -0
  36. package/dist/skill-test/declared-env.js.map +1 -0
  37. package/dist/skill-test/eval-inputs.d.ts +159 -0
  38. package/dist/skill-test/eval-inputs.d.ts.map +1 -0
  39. package/dist/skill-test/eval-inputs.js +158 -0
  40. package/dist/skill-test/eval-inputs.js.map +1 -0
  41. package/dist/skill-test/evals-template.d.ts.map +1 -1
  42. package/dist/skill-test/evals-template.js +27 -10
  43. package/dist/skill-test/evals-template.js.map +1 -1
  44. package/dist/skill-test/exit-codes.d.ts +43 -5
  45. package/dist/skill-test/exit-codes.d.ts.map +1 -1
  46. package/dist/skill-test/exit-codes.js +60 -9
  47. package/dist/skill-test/exit-codes.js.map +1 -1
  48. package/dist/skill-test/experimenter-prompt.d.ts +22 -0
  49. package/dist/skill-test/experimenter-prompt.d.ts.map +1 -1
  50. package/dist/skill-test/experimenter-prompt.js +38 -2
  51. package/dist/skill-test/experimenter-prompt.js.map +1 -1
  52. package/dist/skill-test/grading-adapter.d.ts +42 -0
  53. package/dist/skill-test/grading-adapter.d.ts.map +1 -1
  54. package/dist/skill-test/grading-adapter.js +63 -1
  55. package/dist/skill-test/grading-adapter.js.map +1 -1
  56. package/dist/skill-test/grading-schema.d.ts +73 -5
  57. package/dist/skill-test/grading-schema.d.ts.map +1 -1
  58. package/dist/skill-test/grading-schema.js +20 -5
  59. package/dist/skill-test/grading-schema.js.map +1 -1
  60. package/dist/skill-test/harness-location.d.ts +22 -4
  61. package/dist/skill-test/harness-location.d.ts.map +1 -1
  62. package/dist/skill-test/harness-location.js +83 -18
  63. package/dist/skill-test/harness-location.js.map +1 -1
  64. package/dist/skill-test/index.d.ts +7 -4
  65. package/dist/skill-test/index.d.ts.map +1 -1
  66. package/dist/skill-test/index.js +7 -4
  67. package/dist/skill-test/index.js.map +1 -1
  68. package/dist/skill-test/lock.d.ts +23 -0
  69. package/dist/skill-test/lock.d.ts.map +1 -1
  70. package/dist/skill-test/lock.js +34 -0
  71. package/dist/skill-test/lock.js.map +1 -1
  72. package/dist/skill-test/preflight.d.ts.map +1 -1
  73. package/dist/skill-test/preflight.js +1 -2
  74. package/dist/skill-test/preflight.js.map +1 -1
  75. package/dist/skill-test/run-harness.d.ts +193 -19
  76. package/dist/skill-test/run-harness.d.ts.map +1 -1
  77. package/dist/skill-test/run-harness.js +366 -76
  78. package/dist/skill-test/run-harness.js.map +1 -1
  79. package/dist/skill-test/staging.d.ts.map +1 -1
  80. package/dist/skill-test/staging.js +31 -15
  81. package/dist/skill-test/staging.js.map +1 -1
  82. package/dist/skill-test/vendor-manifest.d.ts +3 -3
  83. package/dist/skill-test/vendor-manifest.d.ts.map +1 -1
  84. package/dist/skill-test/vendor-manifest.js +13 -3
  85. package/dist/skill-test/vendor-manifest.js.map +1 -1
  86. package/dist/validators/packaging-validator.d.ts.map +1 -1
  87. package/dist/validators/packaging-validator.js +134 -1
  88. package/dist/validators/packaging-validator.js.map +1 -1
  89. package/dist/validators/skill-validator.d.ts.map +1 -1
  90. package/dist/validators/skill-validator.js +18 -14
  91. package/dist/validators/skill-validator.js.map +1 -1
  92. package/dist/walk-link-graph.d.ts.map +1 -1
  93. package/dist/walk-link-graph.js +26 -1
  94. package/dist/walk-link-graph.js.map +1 -1
  95. package/package.json +6 -5
@@ -6,6 +6,7 @@ export interface BuildPromptOptions {
6
6
  evalsPath: string;
7
7
  gradingOut: string;
8
8
  frictionOut: string;
9
+ workspacesRoot: string;
9
10
  baseline: boolean;
10
11
  }
11
12
  /**
@@ -16,5 +17,26 @@ export interface BuildPromptOptions {
16
17
  */
17
18
  export declare const DEFAULT_EXPERIMENTER_PROMPT: string;
18
19
  export declare function buildExperimenterPrompt(opts: BuildPromptOptions): string;
20
+ /**
21
+ * Sentinel the persisted (audit) copy of the prompt shows in place of the real
22
+ * nonce, so the on-disk artifact never leaks the secret to skill code.
23
+ */
24
+ export declare const REDACTED_NONCE_PLACEHOLDER = "<run-nonce redacted>";
25
+ /**
26
+ * Append the per-run integrity-nonce directive to the effective prompt. The
27
+ * experimenter must copy `nonce` verbatim into grading.json's top-level
28
+ * `runNonce` field; the harness then rejects any grading.json whose nonce is
29
+ * absent or wrong (see run-harness.ts). This is defense-in-depth against
30
+ * untrusted skill code forging a passing grading.json in the shared sandbox —
31
+ * without the secret nonce (delivered only via stdin, never written to disk) a
32
+ * forged grading cannot be authenticated.
33
+ *
34
+ * Appended AFTER assertPromptInvariants and AFTER any user `experimenterPrompt`
35
+ * override, so the nonce requirement is ALWAYS enforced and a committed config
36
+ * cannot opt out of it.
37
+ */
38
+ export declare function appendIntegrityNonceDirective(prompt: string, nonce: string): string;
39
+ /** Redact the per-run nonce from a prompt for the on-disk audit copy. */
40
+ export declare function redactNonce(prompt: string, nonce: string): string;
19
41
  export declare function assertPromptInvariants(prompt: string): void;
20
42
  //# sourceMappingURL=experimenter-prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"experimenter-prompt.d.ts","sourceRoot":"","sources":["../../src/skill-test/experimenter-prompt.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAuB5B,CAAC;AAKb,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAOxE;AAmBD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAI3D"}
1
+ {"version":3,"file":"experimenter-prompt.d.ts","sourceRoot":"","sources":["../../src/skill-test/experimenter-prompt.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QA2B5B,CAAC;AAKb,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAQxE;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AAEjE;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnF;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjE;AAmBD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAI3D"}
@@ -15,11 +15,15 @@ export const DEFAULT_EXPERIMENTER_PROMPT = [
15
15
  '',
16
16
  'For each eval in {{EVALS_PATH}}:',
17
17
  ' 1. Dispatch ONE executor subagent. Tell it ONLY the task prompt and the staged subject path {{SUBJECT_PATH}}.',
18
- ' Never tell the executor it is being tested.',
18
+ ' When the eval declares input `files`, ALSO tell the executor its working directory is {{WORKSPACES_ROOT}}/<id>',
19
+ ' (substitute the eval\'s own id) — the project it should operate on. Never tell the executor it is being tested.',
19
20
  ' 2. Grade the executor output against the eval\'s `expectations` using skill-creator\'s grader.md rubric.',
21
+ ' If the eval includes an `expected_output`, treat it as the author\'s prose description of a',
22
+ ' correct result and use it as CONTEXT when judging — but the pass/fail verdict is still decided',
23
+ ' per `expectations` entry; `expected_output` informs your judgment, it is not itself a checklist item.',
20
24
  ' 3. Append each graded expectation to the SINGLE top-level `expectations` array in {{GRADING_OUT}} IMMEDIATELY',
21
25
  ' (incremental flush — a mid-run kill must leave partial results).',
22
- ' 4. Record any packaging-fidelity friction to {{FRICTION_OUT}} using the vat friction schema.',
26
+ ' 4. Record any packaging-fidelity friction to {{FRICTION_OUT}} using the vat friction.json schema.',
23
27
  ' If a file referenced by the skill is absent from the staged tree, record a `missing-bundled-file` friction entry.',
24
28
  '',
25
29
  '{{GRADING_OUT}} MUST be ONE flat JSON object in skill-creator\'s grading.json shape (references/schemas.md):',
@@ -39,10 +43,42 @@ export function buildExperimenterPrompt(opts) {
39
43
  return DEFAULT_EXPERIMENTER_PROMPT
40
44
  .replace('{{EVALS_PATH}}', opts.evalsPath)
41
45
  .replace('{{SUBJECT_PATH}}', opts.subjectPath)
46
+ .replaceAll('{{WORKSPACES_ROOT}}', opts.workspacesRoot)
42
47
  .replaceAll('{{GRADING_OUT}}', opts.gradingOut)
43
48
  .replace('{{FRICTION_OUT}}', opts.frictionOut)
44
49
  .replace('{{BASELINE_BLOCK}}', opts.baseline ? BASELINE_BLOCK : '');
45
50
  }
51
+ /**
52
+ * Sentinel the persisted (audit) copy of the prompt shows in place of the real
53
+ * nonce, so the on-disk artifact never leaks the secret to skill code.
54
+ */
55
+ export const REDACTED_NONCE_PLACEHOLDER = '<run-nonce redacted>';
56
+ /**
57
+ * Append the per-run integrity-nonce directive to the effective prompt. The
58
+ * experimenter must copy `nonce` verbatim into grading.json's top-level
59
+ * `runNonce` field; the harness then rejects any grading.json whose nonce is
60
+ * absent or wrong (see run-harness.ts). This is defense-in-depth against
61
+ * untrusted skill code forging a passing grading.json in the shared sandbox —
62
+ * without the secret nonce (delivered only via stdin, never written to disk) a
63
+ * forged grading cannot be authenticated.
64
+ *
65
+ * Appended AFTER assertPromptInvariants and AFTER any user `experimenterPrompt`
66
+ * override, so the nonce requirement is ALWAYS enforced and a committed config
67
+ * cannot opt out of it.
68
+ */
69
+ export function appendIntegrityNonceDirective(prompt, nonce) {
70
+ return [
71
+ prompt,
72
+ '',
73
+ 'INTEGRITY (required, do this LAST): the grading.json you write MUST include a',
74
+ `top-level string field "runNonce" whose value is EXACTLY: ${nonce}`,
75
+ 'Copy it verbatim. vat rejects a grading.json whose runNonce is missing or wrong.',
76
+ ].join('\n');
77
+ }
78
+ /** Redact the per-run nonce from a prompt for the on-disk audit copy. */
79
+ export function redactNonce(prompt, nonce) {
80
+ return prompt.replaceAll(nonce, REDACTED_NONCE_PLACEHOLDER);
81
+ }
46
82
  const REQUIRED_PATTERNS = [
47
83
  { test: /\bSTOP\b/, label: 'must instruct the experimenter to STOP' },
48
84
  { test: /grading\.json|\{\{GRADING_OUT\}\}/i, label: 'must write grading.json' },
@@ -1 +1 @@
1
- {"version":3,"file":"experimenter-prompt.js","sourceRoot":"","sources":["../../src/skill-test/experimenter-prompt.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,2CAA2C,OAAO,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAUD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,yGAAyG;IACzG,EAAE;IACF,kCAAkC;IAClC,iHAAiH;IACjH,kDAAkD;IAClD,4GAA4G;IAC5G,iHAAiH;IACjH,uEAAuE;IACvE,gGAAgG;IAChG,wHAAwH;IACxH,EAAE;IACF,8GAA8G;IAC9G,8GAA8G;IAC9G,gHAAgH;IAChH,wEAAwE;IACxE,qGAAqG;IACrG,EAAE;IACF,mFAAmF;IACnF,EAAE;IACF,6GAA6G;IAC7G,4FAA4F;IAC5F,oBAAoB;CACrB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,cAAc,GAClB,sIAAsI,CAAC;AAEzI,MAAM,UAAU,uBAAuB,CAAC,IAAwB;IAC9D,OAAO,2BAA2B;SAC/B,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC;SACzC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,WAAW,CAAC;SAC7C,UAAU,CAAC,iBAAiB,EAAE,IAAI,CAAC,UAAU,CAAC;SAC9C,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,WAAW,CAAC;SAC7C,OAAO,CAAC,oBAAoB,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,iBAAiB,GAAsC;IAC3D,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,wCAAwC,EAAE;IACrE,EAAE,IAAI,EAAE,oCAAoC,EAAE,KAAK,EAAE,yBAAyB,EAAE;IAChF,EAAE,IAAI,EAAE,sCAAsC,EAAE,KAAK,EAAE,0BAA0B,EAAE;IACnF,EAAE,IAAI,EAAE,yBAAyB,EAAE,KAAK,EAAE,oDAAoD,EAAE;IAChG,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,iDAAiD,EAAE;IACrF,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,yBAAyB,EAAE;IACxD;QACE,IAAI,EAAE,+BAA+B;QACrC,KAAK,EAAE,4EAA4E;KACpF;IACD;QACE,IAAI,EAAE,qCAAqC;QAC3C,KAAK,EAAE,0DAA0D;KAClE;CACF,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,iBAAiB,EAAE,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"experimenter-prompt.js","sourceRoot":"","sources":["../../src/skill-test/experimenter-prompt.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,48 @@ export interface NormalizedGrading {
17
17
  passed: boolean;
18
18
  evidence?: string;
19
19
  }[];
20
+ /** Per-run integrity nonce the experimenter copied from its prompt (if present). */
21
+ runNonce?: string;
20
22
  }
23
+ /**
24
+ * Thrown when grading.json's per-run integrity nonce is absent or does not match
25
+ * the nonce the harness stamped into the experimenter prompt for THIS run. A
26
+ * missing/wrong nonce means the grading was not produced by the experimenter we
27
+ * prompted — most likely a grading.json forged or left behind by untrusted skill
28
+ * code in the shared sandbox — so the verdict cannot be trusted.
29
+ */
30
+ export declare class GradingNonceError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ /**
34
+ * Assert grading.json carries the exact per-run nonce the harness expected.
35
+ * Called before the verdict is trusted. Throws {@link GradingNonceError} on a
36
+ * missing or mismatched nonce.
37
+ */
38
+ export declare function assertGradingNonce(actual: string | undefined, expected: string): void;
21
39
  export declare function parseGradingJson(raw: unknown): NormalizedGrading;
40
+ /** The authoritative eval verdict, recomputed from per-expectation `passed` flags. */
41
+ export interface GradingVerdict {
42
+ passed: number;
43
+ total: number;
44
+ allPassed: boolean;
45
+ }
46
+ /**
47
+ * Reconcile the grader's self-reported `summary` against the authoritative
48
+ * per-expectation `passed` flags and return the recomputed verdict.
49
+ *
50
+ * The grader emits BOTH an aggregate `summary.{passed,total}` AND one
51
+ * `expectations[]` entry per graded expectation (each with its own `passed`).
52
+ * The schema (docs/skill-test-grading-schema.md) defines `summary` as the exact
53
+ * aggregate of `expectations`, so the two MUST agree. We never trust the
54
+ * self-reported summary alone — a grader that emits `summary {5,5}` alongside a
55
+ * failing expectation would otherwise be a false green.
56
+ *
57
+ * Throws {@link GradingSkewError} when:
58
+ * - there are zero expectations (the grader graded NOTHING — an error, never a
59
+ * pass); or
60
+ * - the summary disagrees with the recomputed counts (a grader bug, surfaced
61
+ * loudly rather than silently flowing a wrong verdict downstream).
62
+ */
63
+ export declare function reconcileGrading(report: NormalizedGrading): GradingVerdict;
22
64
  //# sourceMappingURL=grading-adapter.d.ts.map
@@ -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;CACtE;AAaD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAuBhE"}
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;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOrF;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"}
@@ -13,6 +13,35 @@ export class GradingSkewError extends Error {
13
13
  this.name = 'GradingSkewError';
14
14
  }
15
15
  }
16
+ /**
17
+ * Thrown when grading.json's per-run integrity nonce is absent or does not match
18
+ * the nonce the harness stamped into the experimenter prompt for THIS run. A
19
+ * missing/wrong nonce means the grading was not produced by the experimenter we
20
+ * prompted — most likely a grading.json forged or left behind by untrusted skill
21
+ * code in the shared sandbox — so the verdict cannot be trusted.
22
+ */
23
+ export class GradingNonceError extends Error {
24
+ constructor(message) {
25
+ super(`grading.json integrity check failed: ${message}. The harness stamps a secret ` +
26
+ 'per-run nonce into the experimenter prompt (never written to disk) and requires ' +
27
+ 'grading.json to echo it; a missing or wrong nonce means the grading was not ' +
28
+ 'produced by the prompted experimenter and is rejected.');
29
+ this.name = 'GradingNonceError';
30
+ }
31
+ }
32
+ /**
33
+ * Assert grading.json carries the exact per-run nonce the harness expected.
34
+ * Called before the verdict is trusted. Throws {@link GradingNonceError} on a
35
+ * missing or mismatched nonce.
36
+ */
37
+ export function assertGradingNonce(actual, expected) {
38
+ if (actual === undefined) {
39
+ throw new GradingNonceError('grading.json has no top-level `runNonce`');
40
+ }
41
+ if (actual !== expected) {
42
+ throw new GradingNonceError('grading.json `runNonce` does not match this run');
43
+ }
44
+ }
16
45
  /**
17
46
  * Detect the common per-eval nested mistake: `{ evals: [ { expectations, ... } ] }`
18
47
  * with no top-level `expectations`. The grader (an LLM) reaches for this when the
@@ -36,7 +65,7 @@ export function parseGradingJson(raw) {
36
65
  const path = firstIssue?.path.join('.') ?? '(root)';
37
66
  throw new GradingSkewError(`missing/invalid field at "${path}" (${firstIssue?.message ?? 'unknown'})`);
38
67
  }
39
- const { summary, expectations } = result.data;
68
+ const { summary, expectations, runNonce } = result.data;
40
69
  return {
41
70
  summary: { passed: summary.passed, total: summary.total },
42
71
  expectations: expectations.map(e => ({
@@ -44,6 +73,39 @@ export function parseGradingJson(raw) {
44
73
  passed: e.passed,
45
74
  ...(e.evidence === undefined ? {} : { evidence: e.evidence }),
46
75
  })),
76
+ ...(runNonce === undefined ? {} : { runNonce }),
47
77
  };
48
78
  }
79
+ /**
80
+ * Reconcile the grader's self-reported `summary` against the authoritative
81
+ * per-expectation `passed` flags and return the recomputed verdict.
82
+ *
83
+ * The grader emits BOTH an aggregate `summary.{passed,total}` AND one
84
+ * `expectations[]` entry per graded expectation (each with its own `passed`).
85
+ * The schema (docs/skill-test-grading-schema.md) defines `summary` as the exact
86
+ * aggregate of `expectations`, so the two MUST agree. We never trust the
87
+ * self-reported summary alone — a grader that emits `summary {5,5}` alongside a
88
+ * failing expectation would otherwise be a false green.
89
+ *
90
+ * Throws {@link GradingSkewError} when:
91
+ * - there are zero expectations (the grader graded NOTHING — an error, never a
92
+ * pass); or
93
+ * - the summary disagrees with the recomputed counts (a grader bug, surfaced
94
+ * loudly rather than silently flowing a wrong verdict downstream).
95
+ */
96
+ export function reconcileGrading(report) {
97
+ const computedPassed = report.expectations.filter(e => e.passed).length;
98
+ const computedTotal = report.expectations.length;
99
+ if (computedTotal === 0) {
100
+ throw new GradingSkewError('the grader recorded zero expectations — nothing was graded, so this can never be a pass. ' +
101
+ 'A valid grading.json lists every graded expectation in top-level `expectations`');
102
+ }
103
+ const { passed, total } = report.summary;
104
+ if (passed !== computedPassed || total !== computedTotal) {
105
+ throw new GradingSkewError(`the grader's summary disagrees with its expectations: summary={passed:${passed},total:${total}} ` +
106
+ `but expectations recompute to {passed:${computedPassed},total:${computedTotal}}. ` +
107
+ '`summary` must be the exact aggregate of `expectations` (one entry per graded expectation)');
108
+ }
109
+ return { passed: computedPassed, total: computedTotal, allPassed: computedPassed === computedTotal };
110
+ }
49
111
  //# sourceMappingURL=grading-adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grading-adapter.js","sourceRoot":"","sources":["../../src/skill-test/grading-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CACH,4BAA4B,OAAO,gDAAgD;YACjF,0EAA0E;YAC1E,yFAAyF,CAC5F,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAOD;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,GAAY;IACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,gBAAgB,CACxB,oFAAoF;gBAClF,kFAAkF;gBAClF,qCAAqC,CACxC,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC;QACpD,MAAM,IAAI,gBAAgB,CAAC,6BAA6B,IAAI,MAAM,UAAU,EAAE,OAAO,IAAI,SAAS,GAAG,CAAC,CAAC;IACzG,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;IAC9C,OAAO;QACL,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;QACzD,YAAY,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,GAAG,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;SAC9D,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"grading-adapter.js","sourceRoot":"","sources":["../../src/skill-test/grading-adapter.ts"],"names":[],"mappings":"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"}
@@ -41,8 +41,12 @@ export declare const GradedExpectationSchema: z.ZodObject<{
41
41
  evidence: z.ZodOptional<z.ZodString>;
42
42
  }, z.ZodTypeAny, "passthrough">>;
43
43
  export type GradedExpectation = z.infer<typeof GradedExpectationSchema>;
44
- /** Aggregate pass/fail counts. `failed`/`pass_rate` are documented but optional. */
45
- export declare const GradingSummarySchema: z.ZodObject<{
44
+ /**
45
+ * Aggregate pass/fail counts. `failed`/`pass_rate` are documented but optional.
46
+ * Counts are non-negative integers (a float or negative count is a grader bug),
47
+ * and `passed` can never exceed `total`.
48
+ */
49
+ export declare const GradingSummarySchema: z.ZodEffects<z.ZodObject<{
46
50
  passed: z.ZodNumber;
47
51
  total: z.ZodNumber;
48
52
  failed: z.ZodOptional<z.ZodNumber>;
@@ -57,6 +61,16 @@ export declare const GradingSummarySchema: z.ZodObject<{
57
61
  total: z.ZodNumber;
58
62
  failed: z.ZodOptional<z.ZodNumber>;
59
63
  pass_rate: z.ZodOptional<z.ZodNumber>;
64
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
65
+ passed: z.ZodNumber;
66
+ total: z.ZodNumber;
67
+ failed: z.ZodOptional<z.ZodNumber>;
68
+ pass_rate: z.ZodOptional<z.ZodNumber>;
69
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
70
+ passed: z.ZodNumber;
71
+ total: z.ZodNumber;
72
+ failed: z.ZodOptional<z.ZodNumber>;
73
+ pass_rate: z.ZodOptional<z.ZodNumber>;
60
74
  }, z.ZodTypeAny, "passthrough">>;
61
75
  export type GradingSummary = z.infer<typeof GradingSummarySchema>;
62
76
  /**
@@ -78,7 +92,7 @@ export declare const GradingReportSchema: z.ZodObject<{
78
92
  passed: z.ZodBoolean;
79
93
  evidence: z.ZodOptional<z.ZodString>;
80
94
  }, z.ZodTypeAny, "passthrough">>, "many">;
81
- summary: z.ZodObject<{
95
+ summary: z.ZodEffects<z.ZodObject<{
82
96
  passed: z.ZodNumber;
83
97
  total: z.ZodNumber;
84
98
  failed: z.ZodOptional<z.ZodNumber>;
@@ -93,7 +107,25 @@ export declare const GradingReportSchema: z.ZodObject<{
93
107
  total: z.ZodNumber;
94
108
  failed: z.ZodOptional<z.ZodNumber>;
95
109
  pass_rate: z.ZodOptional<z.ZodNumber>;
110
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
111
+ passed: z.ZodNumber;
112
+ total: z.ZodNumber;
113
+ failed: z.ZodOptional<z.ZodNumber>;
114
+ pass_rate: z.ZodOptional<z.ZodNumber>;
115
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
116
+ passed: z.ZodNumber;
117
+ total: z.ZodNumber;
118
+ failed: z.ZodOptional<z.ZodNumber>;
119
+ pass_rate: z.ZodOptional<z.ZodNumber>;
96
120
  }, z.ZodTypeAny, "passthrough">>;
121
+ /**
122
+ * Per-run integrity nonce the experimenter is told to copy verbatim from its
123
+ * prompt (see experimenter-prompt.ts). Optional in the schema (a grading.json
124
+ * validated by external tooling need not carry it), but the harness REQUIRES
125
+ * it to match the run's secret nonce before trusting the verdict — this is how
126
+ * a forged grading.json written by untrusted skill code is rejected.
127
+ */
128
+ runNonce: z.ZodOptional<z.ZodString>;
97
129
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
98
130
  expectations: z.ZodArray<z.ZodObject<{
99
131
  text: z.ZodString;
@@ -108,7 +140,7 @@ export declare const GradingReportSchema: z.ZodObject<{
108
140
  passed: z.ZodBoolean;
109
141
  evidence: z.ZodOptional<z.ZodString>;
110
142
  }, z.ZodTypeAny, "passthrough">>, "many">;
111
- summary: z.ZodObject<{
143
+ summary: z.ZodEffects<z.ZodObject<{
112
144
  passed: z.ZodNumber;
113
145
  total: z.ZodNumber;
114
146
  failed: z.ZodOptional<z.ZodNumber>;
@@ -123,7 +155,25 @@ export declare const GradingReportSchema: z.ZodObject<{
123
155
  total: z.ZodNumber;
124
156
  failed: z.ZodOptional<z.ZodNumber>;
125
157
  pass_rate: z.ZodOptional<z.ZodNumber>;
158
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
159
+ passed: z.ZodNumber;
160
+ total: z.ZodNumber;
161
+ failed: z.ZodOptional<z.ZodNumber>;
162
+ pass_rate: z.ZodOptional<z.ZodNumber>;
163
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
164
+ passed: z.ZodNumber;
165
+ total: z.ZodNumber;
166
+ failed: z.ZodOptional<z.ZodNumber>;
167
+ pass_rate: z.ZodOptional<z.ZodNumber>;
126
168
  }, z.ZodTypeAny, "passthrough">>;
169
+ /**
170
+ * Per-run integrity nonce the experimenter is told to copy verbatim from its
171
+ * prompt (see experimenter-prompt.ts). Optional in the schema (a grading.json
172
+ * validated by external tooling need not carry it), but the harness REQUIRES
173
+ * it to match the run's secret nonce before trusting the verdict — this is how
174
+ * a forged grading.json written by untrusted skill code is rejected.
175
+ */
176
+ runNonce: z.ZodOptional<z.ZodString>;
127
177
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
128
178
  expectations: z.ZodArray<z.ZodObject<{
129
179
  text: z.ZodString;
@@ -138,7 +188,7 @@ export declare const GradingReportSchema: z.ZodObject<{
138
188
  passed: z.ZodBoolean;
139
189
  evidence: z.ZodOptional<z.ZodString>;
140
190
  }, z.ZodTypeAny, "passthrough">>, "many">;
141
- summary: z.ZodObject<{
191
+ summary: z.ZodEffects<z.ZodObject<{
142
192
  passed: z.ZodNumber;
143
193
  total: z.ZodNumber;
144
194
  failed: z.ZodOptional<z.ZodNumber>;
@@ -153,7 +203,25 @@ export declare const GradingReportSchema: z.ZodObject<{
153
203
  total: z.ZodNumber;
154
204
  failed: z.ZodOptional<z.ZodNumber>;
155
205
  pass_rate: z.ZodOptional<z.ZodNumber>;
206
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
207
+ passed: z.ZodNumber;
208
+ total: z.ZodNumber;
209
+ failed: z.ZodOptional<z.ZodNumber>;
210
+ pass_rate: z.ZodOptional<z.ZodNumber>;
211
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
212
+ passed: z.ZodNumber;
213
+ total: z.ZodNumber;
214
+ failed: z.ZodOptional<z.ZodNumber>;
215
+ pass_rate: z.ZodOptional<z.ZodNumber>;
156
216
  }, z.ZodTypeAny, "passthrough">>;
217
+ /**
218
+ * Per-run integrity nonce the experimenter is told to copy verbatim from its
219
+ * prompt (see experimenter-prompt.ts). Optional in the schema (a grading.json
220
+ * validated by external tooling need not carry it), but the harness REQUIRES
221
+ * it to match the run's secret nonce before trusting the verdict — this is how
222
+ * a forged grading.json written by untrusted skill code is rejected.
223
+ */
224
+ runNonce: z.ZodOptional<z.ZodString>;
157
225
  }, z.ZodTypeAny, "passthrough">>;
158
226
  export type GradingReport = z.infer<typeof GradingReportSchema>;
159
227
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grading-schema.d.ts","sourceRoot":"","sources":["../../src/skill-test/grading-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,sFAAsF;AACtF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;gCAMpB,CAAC;AAEjB,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;gCAOjB,CAAC;AAEjB,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAKhB,CAAC;AAEjB,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;CAAyD,CAAC"}
1
+ {"version":3,"file":"grading-schema.d.ts","sourceRoot":"","sources":["../../src/skill-test/grading-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,sFAAsF;AACtF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;gCAMpB,CAAC;AAEjB,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;gCAU7B,CAAC;AAEL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAI5B;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IANH;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IANH;;;;;;OAMG;;gCAGS,CAAC;AAEjB,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;CAAyD,CAAC"}
@@ -35,15 +35,22 @@ export const GradedExpectationSchema = z
35
35
  evidence: z.string().optional(),
36
36
  })
37
37
  .passthrough();
38
- /** Aggregate pass/fail counts. `failed`/`pass_rate` are documented but optional. */
38
+ /**
39
+ * Aggregate pass/fail counts. `failed`/`pass_rate` are documented but optional.
40
+ * Counts are non-negative integers (a float or negative count is a grader bug),
41
+ * and `passed` can never exceed `total`.
42
+ */
39
43
  export const GradingSummarySchema = z
40
44
  .object({
41
- passed: z.number(),
42
- total: z.number(),
43
- failed: z.number().optional(),
45
+ passed: z.number().int().nonnegative(),
46
+ total: z.number().int().nonnegative(),
47
+ failed: z.number().int().nonnegative().optional(),
44
48
  pass_rate: z.number().optional(),
45
49
  })
46
- .passthrough();
50
+ .passthrough()
51
+ .refine(s => s.passed <= s.total, {
52
+ message: 'summary.passed must not exceed summary.total',
53
+ });
47
54
  /**
48
55
  * The full grading.json contract. Required: top-level `expectations[]` and
49
56
  * `summary`. Everything else passes through untouched (forward-compatible with
@@ -53,6 +60,14 @@ export const GradingReportSchema = z
53
60
  .object({
54
61
  expectations: z.array(GradedExpectationSchema),
55
62
  summary: GradingSummarySchema,
63
+ /**
64
+ * Per-run integrity nonce the experimenter is told to copy verbatim from its
65
+ * prompt (see experimenter-prompt.ts). Optional in the schema (a grading.json
66
+ * validated by external tooling need not carry it), but the harness REQUIRES
67
+ * it to match the run's secret nonce before trusting the verdict — this is how
68
+ * a forged grading.json written by untrusted skill code is rejected.
69
+ */
70
+ runNonce: z.string().optional(),
56
71
  })
57
72
  .passthrough();
58
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grading-schema.js","sourceRoot":"","sources":["../../src/skill-test/grading-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,sFAAsF;AACtF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC;KACrC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB,oFAAoF;AACpF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,CAAC;IACN,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC;IAC9C,OAAO,EAAE,oBAAoB;CAC9B,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,eAAe,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"grading-schema.js","sourceRoot":"","sources":["../../src/skill-test/grading-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,sFAAsF;AACtF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC;KACrC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACtC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACrC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IACjD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC;KACD,WAAW,EAAE;KACb,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,EAAE;IAChC,OAAO,EAAE,8CAA8C;CACxD,CAAC,CAAC;AAIL;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,CAAC;IACN,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC;IAC9C,OAAO,EAAE,oBAAoB;IAC7B;;;;;;OAMG;IACH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,eAAe,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC"}
@@ -15,8 +15,16 @@ export declare function resolveHarnessRoot(skillNames: string[], tmpRoot?: strin
15
15
  * Refuse a --workdir whose ancestry contains CLAUDE.md or .claude/ — cwd
16
16
  * discovery would re-pollute the run (spec §7). Defense in depth with
17
17
  * --setting-sources "".
18
+ *
19
+ * `stopAt` (optional) bounds the ancestry walk: when the walk reaches that
20
+ * directory it stops *before* inspecting it (exclusive boundary), mirroring
21
+ * `assertSafeHarnessRoot`'s `trustedRoot`. Default (undefined) walks to the
22
+ * filesystem root, preserving the strictest production behavior. Tests use it
23
+ * to scope the walk to a sandbox — necessary on Windows, where the OS tmp dir
24
+ * lives inside the user's home and would otherwise surface an ambient
25
+ * ~/.claude during the walk.
18
26
  */
19
- export declare function assertSafeWorkdir(dir: string): void;
27
+ export declare function assertSafeWorkdir(dir: string, stopAt?: string): void;
20
28
  /**
21
29
  * Prepare the harness root directory so that `assertSafeHarnessRoot` will
22
30
  * pass on the next call. If the path does not exist, this is a no-op (the
@@ -31,10 +39,20 @@ export declare function assertSafeWorkdir(dir: string): void;
31
39
  */
32
40
  export declare function prepareHarnessRoot(dir: string): void;
33
41
  /**
34
- * FS-bound hardening for the shared-tmp harness root (spec §7): the root must
35
- * be 0700 and owned by the current uid, and no path component may be a symlink.
42
+ * FS-bound hardening for the shared-tmp harness root (spec §7). Validates EVERY
43
+ * path component from the leaf up to (but excluding) `trustedRoot`: no component
44
+ * may be a symlink, and on POSIX each must be owned by the current uid. This
45
+ * closes the shared-/tmp TOCTOU where the recursively-created intermediate parent
46
+ * (`<tmp>/vat-skill-test`) — not just the leaf — could be pre-created as a symlink
47
+ * or under another user's ownership. The leaf must additionally be 0700.
48
+ *
49
+ * `trustedRoot` (default: the OS tmp dir) is the boundary: it is system-owned
50
+ * (sticky-bit /tmp) so ownership/mode checks are not applied to it. When the leaf
51
+ * is not a descendant of `trustedRoot` (an explicit --out elsewhere), validation
52
+ * degrades to the leaf alone.
53
+ *
36
54
  * Integration-tested (requires real lstat/stat). On Windows, uid checks are
37
55
  * skipped (process.getuid is undefined) but the symlink refusal still applies.
38
56
  */
39
- export declare function assertSafeHarnessRoot(dir: string, currentUid: number): void;
57
+ export declare function assertSafeHarnessRoot(dir: string, currentUid: number, trustedRoot?: string): void;
40
58
  //# sourceMappingURL=harness-location.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness-location.d.ts","sourceRoot":"","sources":["../../src/skill-test/harness-location.ts"],"names":[],"mappings":"AAKA,sDAAsD;AACtD,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,QAAQ,EAAG,CAAC,CAAU;gBACnB,OAAO,EAAE,MAAM;CAI5B;AAOD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ7D;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAGjF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAenD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAiB3E"}
1
+ {"version":3,"file":"harness-location.d.ts","sourceRoot":"","sources":["../../src/skill-test/harness-location.ts"],"names":[],"mappings":"AAKA,sDAAsD;AACtD,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,QAAQ,EAAG,CAAC,CAAU;gBACnB,OAAO,EAAE,MAAM;CAI5B;AAOD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ7D;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAGjF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAgBpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAkDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,MAA2B,GACvC,IAAI,CAkBN"}
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { chmodSync, existsSync, lstatSync, statSync } from 'node:fs';
3
- import { normalizedTmpdir, safePath } from '@vibe-agent-toolkit/utils';
3
+ import { isAbsolutePath, normalizedTmpdir, safePath } from '@vibe-agent-toolkit/utils';
4
4
  /** Harness-location failure — maps to exit code 2. */
5
5
  export class HarnessLocationError extends Error {
6
6
  exitCode = 2;
@@ -36,11 +36,20 @@ export function resolveHarnessRoot(skillNames, tmpRoot) {
36
36
  * Refuse a --workdir whose ancestry contains CLAUDE.md or .claude/ — cwd
37
37
  * discovery would re-pollute the run (spec §7). Defense in depth with
38
38
  * --setting-sources "".
39
+ *
40
+ * `stopAt` (optional) bounds the ancestry walk: when the walk reaches that
41
+ * directory it stops *before* inspecting it (exclusive boundary), mirroring
42
+ * `assertSafeHarnessRoot`'s `trustedRoot`. Default (undefined) walks to the
43
+ * filesystem root, preserving the strictest production behavior. Tests use it
44
+ * to scope the walk to a sandbox — necessary on Windows, where the OS tmp dir
45
+ * lives inside the user's home and would otherwise surface an ambient
46
+ * ~/.claude during the walk.
39
47
  */
40
- export function assertSafeWorkdir(dir) {
48
+ export function assertSafeWorkdir(dir, stopAt) {
49
+ const boundary = stopAt === undefined ? undefined : safePath.resolve(stopAt);
41
50
  let current = safePath.resolve(dir);
42
51
  let previous = '';
43
- while (current !== previous) {
52
+ while (current !== previous && current !== boundary) {
44
53
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- ancestry walk over a user-supplied workdir
45
54
  if (existsSync(safePath.join(current, 'CLAUDE.md'))) {
46
55
  throw new HarnessLocationError(`--workdir is inside a project: CLAUDE.md found at ${current}. Use an OS-tmp location.`);
@@ -83,29 +92,85 @@ export function prepareHarnessRoot(dir) {
83
92
  }
84
93
  }
85
94
  }
95
+ /** True when `child` is a strict descendant of `root` (neither equal nor escaping). */
96
+ function isStrictlyUnder(root, child) {
97
+ if (child === root)
98
+ return false;
99
+ const rel = safePath.relative(root, child);
100
+ return rel !== '' && rel !== '..' && !rel.startsWith('../') && !isAbsolutePath(rel);
101
+ }
102
+ /**
103
+ * The harness path components to validate, leaf-first: the leaf and every
104
+ * ancestor strictly between it and `trustedRoot` (exclusive). When the leaf is
105
+ * not a descendant of `trustedRoot` (e.g. an explicit --out elsewhere), only the
106
+ * leaf is returned — we cannot bound the walk without a trusted boundary, so we
107
+ * fall back to leaf-only validation.
108
+ */
109
+ function harnessAncestry(leaf, trustedRoot) {
110
+ if (!isStrictlyUnder(trustedRoot, leaf))
111
+ return [leaf];
112
+ const components = [];
113
+ let current = leaf;
114
+ while (current !== trustedRoot) {
115
+ const parent = safePath.join(current, '..');
116
+ if (parent === current)
117
+ break; // reached the filesystem root defensively
118
+ components.push(current);
119
+ current = parent;
120
+ }
121
+ return components;
122
+ }
86
123
  /**
87
- * FS-bound hardening for the shared-tmp harness root (spec §7): the root must
88
- * be 0700 and owned by the current uid, and no path component may be a symlink.
124
+ * Reject a single harness path component that is a symlink or not owned by the
125
+ * current user. A non-existent component is skipped (an absent ancestor cannot
126
+ * be a live attacker-controlled symlink). On Windows the ownership check is
127
+ * skipped (no real uids); the symlink refusal still applies.
128
+ */
129
+ function assertComponentSafe(component, currentUid) {
130
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived harness path component
131
+ if (!existsSync(component))
132
+ return;
133
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived harness path component
134
+ const ls = lstatSync(component);
135
+ if (ls.isSymbolicLink()) {
136
+ throw new HarnessLocationError(`Refusing to use a symlinked harness path component: ${component}.`);
137
+ }
138
+ if (process.platform !== 'win32' && typeof ls.uid === 'number' && ls.uid !== currentUid) {
139
+ throw new HarnessLocationError(`Harness path component ${component} is not owned by the current user (uid ${ls.uid} != ${currentUid}).`);
140
+ }
141
+ }
142
+ /**
143
+ * FS-bound hardening for the shared-tmp harness root (spec §7). Validates EVERY
144
+ * path component from the leaf up to (but excluding) `trustedRoot`: no component
145
+ * may be a symlink, and on POSIX each must be owned by the current uid. This
146
+ * closes the shared-/tmp TOCTOU where the recursively-created intermediate parent
147
+ * (`<tmp>/vat-skill-test`) — not just the leaf — could be pre-created as a symlink
148
+ * or under another user's ownership. The leaf must additionally be 0700.
149
+ *
150
+ * `trustedRoot` (default: the OS tmp dir) is the boundary: it is system-owned
151
+ * (sticky-bit /tmp) so ownership/mode checks are not applied to it. When the leaf
152
+ * is not a descendant of `trustedRoot` (an explicit --out elsewhere), validation
153
+ * degrades to the leaf alone.
154
+ *
89
155
  * Integration-tested (requires real lstat/stat). On Windows, uid checks are
90
156
  * skipped (process.getuid is undefined) but the symlink refusal still applies.
91
157
  */
92
- export function assertSafeHarnessRoot(dir, currentUid) {
158
+ export function assertSafeHarnessRoot(dir, currentUid, trustedRoot = normalizedTmpdir()) {
159
+ const resolved = safePath.resolve(dir);
160
+ // eslint-disable-next-line local/no-unsafe-root-join -- single-arg normalization of the trusted boundary itself (no caller-controlled segment is joined here); it is only used as an equality boundary for the ancestry walk.
161
+ const root = safePath.resolve(trustedRoot);
162
+ for (const component of harnessAncestry(resolved, root)) {
163
+ assertComponentSafe(component, currentUid);
164
+ }
165
+ // Leaf-only 0700 check (intermediates are created 0700 by recursive mkdir and
166
+ // need only the symlink/ownership guarantees above). Skipped on win32.
93
167
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived harness root
94
- if (!existsSync(dir))
168
+ if (!existsSync(resolved))
95
169
  return; // not yet created — caller creates it 0700
96
170
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived harness root
97
- const ls = lstatSync(dir);
98
- if (ls.isSymbolicLink()) {
99
- throw new HarnessLocationError(`Refusing to use a symlinked harness root: ${dir}.`);
100
- }
101
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived harness root
102
- const st = statSync(dir);
103
- if (typeof st.uid === 'number' && st.uid !== currentUid) {
104
- throw new HarnessLocationError(`Harness root ${dir} is not owned by the current user (uid ${st.uid} != ${currentUid}).`);
105
- }
106
- const mode = st.mode & 0o777;
171
+ const mode = statSync(resolved).mode & 0o777;
107
172
  if (mode !== 0o700 && process.platform !== 'win32') {
108
- throw new HarnessLocationError(`Harness root ${dir} must be 0700 (found ${mode.toString(8)}).`);
173
+ throw new HarnessLocationError(`Harness root ${resolved} must be 0700 (found ${mode.toString(8)}).`);
109
174
  }
110
175
  }
111
176
  //# sourceMappingURL=harness-location.js.map