vigiles 14.6.7 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code/agent-runtime.d.ts +2 -19
- package/dist/adapters/claude-code/agent-runtime.js +5 -30
- package/dist/adapters/claude-code/agent-tools.d.ts +20 -0
- package/dist/adapters/claude-code/agent-tools.js +40 -0
- package/dist/audit-report.d.ts +2 -2
- package/dist/audit-report.template.html +15 -15
- package/dist/audit-score.d.ts +1 -1
- package/dist/audit-score.js +20 -20
- package/dist/audit-verdict.d.ts +2 -2
- package/dist/audit-verdict.js +5 -5
- package/dist/core/assert-never.d.ts +9 -0
- package/dist/core/assert-never.js +14 -0
- package/dist/core/description-overlap.js +2 -2
- package/dist/core/edit-distance.d.ts +12 -0
- package/dist/core/edit-distance.js +36 -0
- package/dist/core/effects.js +3 -3
- package/dist/core/hash.d.ts +1 -2
- package/dist/core/hash.js +6 -4
- package/dist/core/hook-block-ineffective.d.ts +55 -6
- package/dist/core/hook-block-ineffective.js +9 -14
- package/dist/core/hook-events.js +2 -2
- package/dist/core/linters.d.ts +2 -6
- package/dist/core/linters.js +4 -29
- package/dist/core/mcp-contract-message.d.ts +22 -0
- package/dist/core/mcp-contract-message.js +29 -0
- package/dist/core/mcp.d.ts +4 -12
- package/dist/core/mcp.js +3 -14
- package/dist/core/ncd.d.ts +12 -0
- package/dist/core/ncd.js +50 -0
- package/dist/core/plugin-dir-layout.d.ts +5 -5
- package/dist/core/plugin-dir-layout.js +10 -22
- package/dist/core/proofs.d.ts +2 -11
- package/dist/core/proofs.js +4 -39
- package/dist/core/skill-resources.d.ts +3 -3
- package/dist/core/skill-resources.js +9 -8
- package/dist/core/tool-contract.js +2 -2
- package/dist/leaderboard.d.ts +2 -51
- package/dist/leaderboard.js +20 -225
- package/dist/optimize.d.ts +1 -1
- package/dist/optimize.js +3 -3
- package/dist/posix-path.d.ts +40 -0
- package/dist/posix-path.js +293 -0
- package/dist/scan-core.d.ts +154 -0
- package/dist/scan-core.js +690 -0
- package/dist/scan-files.d.ts +28 -0
- package/dist/scan-files.js +489 -0
- package/dist/scan.d.ts +11 -34
- package/dist/scan.js +55 -668
- package/dist/score-core.d.ts +73 -0
- package/dist/score-core.js +226 -0
- package/dist/test-coverage-files.d.ts +11 -0
- package/dist/test-coverage-files.js +208 -0
- package/package.json +1 -1
package/dist/audit-score.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
|
|
27
27
|
* overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
|
|
28
28
|
*/
|
|
29
|
-
import { type PluginScore } from "./
|
|
29
|
+
import { type PluginScore } from "./score-core.js";
|
|
30
30
|
import type { ScanReport } from "./scan.js";
|
|
31
31
|
export type CategoryKey = "Truthfulness" | "Triggering" | "Structure" | "Safety" | "Tested";
|
|
32
32
|
export interface CategoryScore {
|
package/dist/audit-score.js
CHANGED
|
@@ -30,7 +30,7 @@ exports.formatAuditScore = formatAuditScore;
|
|
|
30
30
|
* A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
|
|
31
31
|
* overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
|
|
32
32
|
*/
|
|
33
|
-
const
|
|
33
|
+
const score_core_js_1 = require("./score-core.js");
|
|
34
34
|
// Per-item penalties are the SHARED leaderboard weights (imported above) so the
|
|
35
35
|
// category rings and the single health number can never drift. W_UNTESTED is
|
|
36
36
|
// audit-only — untested surfaces are advisory (shown, never scored into overall).
|
|
@@ -64,12 +64,12 @@ function truthfulness(r) {
|
|
|
64
64
|
const { score, findings } = scoreFrom([
|
|
65
65
|
{
|
|
66
66
|
n: r.danglingRefs.length,
|
|
67
|
-
weight:
|
|
67
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
68
68
|
label: "broken intra-plugin reference(s)",
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
n: missingHooks,
|
|
72
|
-
weight:
|
|
72
|
+
weight: score_core_js_1.W_MISSING_HOOK,
|
|
73
73
|
label: "hook script(s) missing (never run)",
|
|
74
74
|
},
|
|
75
75
|
]);
|
|
@@ -80,12 +80,12 @@ function triggering(r) {
|
|
|
80
80
|
const { score, findings } = scoreFrom([
|
|
81
81
|
{
|
|
82
82
|
n: noDesc,
|
|
83
|
-
weight:
|
|
83
|
+
weight: score_core_js_1.W_NO_DESCRIPTION,
|
|
84
84
|
label: "skill(s) with no usable description (can't trigger)",
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
n: r.descriptionOverlaps.length,
|
|
88
|
-
weight:
|
|
88
|
+
weight: score_core_js_1.W_OVERLAP,
|
|
89
89
|
label: "near-identical skill description(s) (wrong one fires)",
|
|
90
90
|
},
|
|
91
91
|
]);
|
|
@@ -99,42 +99,42 @@ function structure(r) {
|
|
|
99
99
|
const { score, findings } = scoreFrom([
|
|
100
100
|
{
|
|
101
101
|
n: deadTools,
|
|
102
|
-
weight:
|
|
103
|
-
label: "agent tool(s)
|
|
102
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
103
|
+
label: "unavailable agent tool(s) (typo / never-available)",
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
n: deadMcpTools,
|
|
107
|
-
weight:
|
|
107
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
108
108
|
label: "agent MCP tool(s) whose server isn't declared",
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
n: r.hookEventIssues.length,
|
|
112
|
-
weight:
|
|
112
|
+
weight: score_core_js_1.W_MISSING_HOOK,
|
|
113
113
|
label: "hook(s) on an unknown event (never fire)",
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
n: r.mcpIssues.length,
|
|
117
|
-
weight:
|
|
117
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
118
118
|
label: "MCP server(s) that can't start (no command/url)",
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
n: r.mcpHookIssues.length,
|
|
122
|
-
weight:
|
|
122
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
123
123
|
label: "mcp_tool hook(s) incomplete / undeclared server",
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
n: r.frontmatterIssues.length,
|
|
127
|
-
weight:
|
|
127
|
+
weight: score_core_js_1.W_NO_DESCRIPTION,
|
|
128
128
|
label: "surface(s) missing required frontmatter",
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
n: r.frontmatterValueIssues.length,
|
|
132
|
-
weight:
|
|
132
|
+
weight: score_core_js_1.W_NO_CONTRACT,
|
|
133
133
|
label: "agent(s) with an invalid model/color (silent fallback)",
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
n: deadDisallowed,
|
|
137
|
-
weight:
|
|
137
|
+
weight: score_core_js_1.W_NO_CONTRACT,
|
|
138
138
|
label: "disallowedTools typo(s) that block nothing",
|
|
139
139
|
},
|
|
140
140
|
]);
|
|
@@ -185,7 +185,7 @@ function safety(r) {
|
|
|
185
185
|
const { score, findings } = scoreFrom([
|
|
186
186
|
{
|
|
187
187
|
n: hard.length,
|
|
188
|
-
weight:
|
|
188
|
+
weight: score_core_js_1.W_TRIFECTA,
|
|
189
189
|
label: "unit(s) holding all three lethal-trifecta legs (prompt-injection exfil path)",
|
|
190
190
|
},
|
|
191
191
|
]);
|
|
@@ -193,7 +193,7 @@ function safety(r) {
|
|
|
193
193
|
// note but never graded (mirrors the Structure inherits-all advisory).
|
|
194
194
|
const advisory = r.trifectaFindings
|
|
195
195
|
.filter((f) => f.finding.severity === "advisory")
|
|
196
|
-
.map((f) => `${f.name} inherits all tools —
|
|
196
|
+
.map((f) => `${f.name} inherits all tools — the "lethal trifecta" (reads data, reaches the web, runs commands), so a prompt injection could exfiltrate secrets (advisory)`);
|
|
197
197
|
return {
|
|
198
198
|
key: "Safety",
|
|
199
199
|
score,
|
|
@@ -217,7 +217,7 @@ function tested(r) {
|
|
|
217
217
|
* an instruction file as a surface.)
|
|
218
218
|
*/
|
|
219
219
|
function isEmptyAudit(r) {
|
|
220
|
-
return (0,
|
|
220
|
+
return (0, score_core_js_1.isEmptyMachine)(r) && !r.instructions;
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
223
|
* Bucket a scan report into the five deterministic Lighthouse categories as a
|
|
@@ -237,7 +237,7 @@ function auditScore(report) {
|
|
|
237
237
|
];
|
|
238
238
|
return {
|
|
239
239
|
overall: 0,
|
|
240
|
-
grade: (0,
|
|
240
|
+
grade: (0, score_core_js_1.gradeFor)(0),
|
|
241
241
|
categories: categories.map((key) => ({
|
|
242
242
|
key,
|
|
243
243
|
score: null,
|
|
@@ -258,8 +258,8 @@ function auditScore(report) {
|
|
|
258
258
|
// of the rings — averaging would let a real problem in one category be diluted
|
|
259
259
|
// by clean siblings. The rings above stay a diagnostic breakdown; Tested
|
|
260
260
|
// (advisory) is never summed in (untested surfaces don't drag the grade).
|
|
261
|
-
const { score: overall } = (0,
|
|
262
|
-
return { overall, grade: (0,
|
|
261
|
+
const { score: overall } = (0, score_core_js_1.computeIntegrityScore)((0, score_core_js_1.reportDeductions)(report));
|
|
262
|
+
return { overall, grade: (0, score_core_js_1.gradeFor)(overall), categories, empty: false };
|
|
263
263
|
}
|
|
264
264
|
// A 22-cell bar gauge ("ring" in the terminal; the real rings are the HTML).
|
|
265
265
|
const BAR_CELLS = 22;
|
package/dist/audit-verdict.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 1. `pointsIfFixed` per recommendation — the exact number of overall points the
|
|
12
12
|
* grade gains if THAT one fix is applied (so a fix card can show `+N pts` and
|
|
13
13
|
* sort by it). Computed as `overall(report − thisFinding) − overall(report)`.
|
|
14
|
-
* 2. A verdict `sentence` for the report header — e.g. "Two
|
|
14
|
+
* 2. A verdict `sentence` for the report header — e.g. "Two fixes away
|
|
15
15
|
* from an A." — where the COUNT is the minimal number of fixes whose COMBINED
|
|
16
16
|
* removal actually crosses the next grade threshold (a real cumulative
|
|
17
17
|
* re-score), and `pointsToNextGrade` is the real threshold gap.
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { type AuditScore } from "./audit-score.js";
|
|
42
42
|
import type { Recommendation } from "./optimize.js";
|
|
43
|
-
import { type PluginScore } from "./
|
|
43
|
+
import { type PluginScore } from "./score-core.js";
|
|
44
44
|
import type { ScanReport } from "./scan.js";
|
|
45
45
|
/**
|
|
46
46
|
* The inputs the verdict needs — exactly the pieces {@link buildAuditReport}
|
package/dist/audit-verdict.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 1. `pointsIfFixed` per recommendation — the exact number of overall points the
|
|
13
13
|
* grade gains if THAT one fix is applied (so a fix card can show `+N pts` and
|
|
14
14
|
* sort by it). Computed as `overall(report − thisFinding) − overall(report)`.
|
|
15
|
-
* 2. A verdict `sentence` for the report header — e.g. "Two
|
|
15
|
+
* 2. A verdict `sentence` for the report header — e.g. "Two fixes away
|
|
16
16
|
* from an A." — where the COUNT is the minimal number of fixes whose COMBINED
|
|
17
17
|
* removal actually crosses the next grade threshold (a real cumulative
|
|
18
18
|
* re-score), and `pointsToNextGrade` is the real threshold gap.
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.computeVerdict = computeVerdict;
|
|
44
44
|
const audit_score_js_1 = require("./audit-score.js");
|
|
45
|
-
const
|
|
45
|
+
const score_core_js_1 = require("./score-core.js");
|
|
46
46
|
// The grade-band FLOORS (A ≥90 … D ≥60; below 60 is F), mirroring gradeFor.
|
|
47
47
|
const GRADE_FLOORS = [60, 70, 80, 90];
|
|
48
48
|
/** The smallest band floor strictly above `overall`, or null when already an A. */
|
|
@@ -195,7 +195,7 @@ function overallWithout(report, recs) {
|
|
|
195
195
|
*/
|
|
196
196
|
function dominantDeduction(report) {
|
|
197
197
|
let best = null;
|
|
198
|
-
for (const d of (0,
|
|
198
|
+
for (const d of (0, score_core_js_1.reportDeductions)(report)) {
|
|
199
199
|
if (d.n <= 0)
|
|
200
200
|
continue;
|
|
201
201
|
const cost = d.n * d.weight;
|
|
@@ -238,10 +238,10 @@ function buildSentence(input, pointsToNextGrade, fixesToNextGrade) {
|
|
|
238
238
|
return `A — nothing blocking the grade; ${numberWord(n)} deterministic ${fixNoun(n)} would harden it further.`;
|
|
239
239
|
}
|
|
240
240
|
// The next band's grade is gradeFor(its FLOOR); the floor is base + the gap.
|
|
241
|
-
const nextGrade = (0,
|
|
241
|
+
const nextGrade = (0, score_core_js_1.gradeFor)(score.overall + pointsToNextGrade);
|
|
242
242
|
// Reachable by the deterministic fix list: fix-count-forward (the actionable framing).
|
|
243
243
|
if (fixesToNextGrade !== null) {
|
|
244
|
-
return `${capitalize(numberWord(fixesToNextGrade))}
|
|
244
|
+
return `${capitalize(numberWord(fixesToNextGrade))} ${fixNoun(fixesToNextGrade)} away from ${article(nextGrade)} ${nextGrade}.`;
|
|
245
245
|
}
|
|
246
246
|
// Not reachable by recommendations alone — lead with the dominant blocking finding.
|
|
247
247
|
const dom = dominantDeduction(report);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assertNever` — the exhaustive-switch guard, in its own NODE-FREE leaf so a
|
|
3
|
+
* consumer can import it WITHOUT pulling `core/hash.ts` (which imports
|
|
4
|
+
* `node:crypto` for `sha256short`) into a browser bundle. The deterministic audit
|
|
5
|
+
* engine reaches this via `core/effects.ts`; `hash.ts` re-exports it so every
|
|
6
|
+
* existing `import { assertNever } from "./hash.js"` is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertNever(x: never): never;
|
|
9
|
+
//# sourceMappingURL=assert-never.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertNever = assertNever;
|
|
4
|
+
/**
|
|
5
|
+
* `assertNever` — the exhaustive-switch guard, in its own NODE-FREE leaf so a
|
|
6
|
+
* consumer can import it WITHOUT pulling `core/hash.ts` (which imports
|
|
7
|
+
* `node:crypto` for `sha256short`) into a browser bundle. The deterministic audit
|
|
8
|
+
* engine reaches this via `core/effects.ts`; `hash.ts` re-exports it so every
|
|
9
|
+
* existing `import { assertNever } from "./hash.js"` is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
function assertNever(x) {
|
|
12
|
+
throw new Error(`Unexpected value: ${String(x)}`);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=assert-never.js.map
|
|
@@ -17,7 +17,7 @@ exports.findDescriptionOverlaps = findDescriptionOverlaps;
|
|
|
17
17
|
* copy-pasted description with a word or two changed) — never on a parallel but
|
|
18
18
|
* distinct pair. Warn-level, reports the PAIR (not a unilateral defect).
|
|
19
19
|
*/
|
|
20
|
-
const
|
|
20
|
+
const ncd_js_1 = require("./ncd.js");
|
|
21
21
|
/**
|
|
22
22
|
* The NCD cutoff below which two descriptions count as a near-duplicate. 0.2 sits
|
|
23
23
|
* safely under the sweep's most-similar legitimately-distinct pair (0.25), so
|
|
@@ -35,7 +35,7 @@ function findDescriptionOverlaps(surfaces, cutoff = exports.OVERLAP_NCD_CUTOFF)
|
|
|
35
35
|
const overlaps = [];
|
|
36
36
|
for (let i = 0; i < surfaces.length; i++) {
|
|
37
37
|
for (let j = i + 1; j < surfaces.length; j++) {
|
|
38
|
-
const d = (0,
|
|
38
|
+
const d = (0, ncd_js_1.ncd)(surfaces[i].description, surfaces[j].description);
|
|
39
39
|
if (d >= cutoff)
|
|
40
40
|
continue;
|
|
41
41
|
const a = surfaces[i].name;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levenshtein distance for short-string typo detection. Rule names, tool names,
|
|
3
|
+
* and hook events are short, so edit distance is more appropriate than NCD
|
|
4
|
+
* (which is tuned for longer texts).
|
|
5
|
+
*
|
|
6
|
+
* Extracted to its own zero-dependency leaf module so the typo detectors
|
|
7
|
+
* (tool-contract, hook-events) can import it WITHOUT pulling in `core/linters.ts`,
|
|
8
|
+
* which runs a `node:fs`/`process`/`glob` side effect at import time — the
|
|
9
|
+
* blocker to running those detectors in a browser (the in-browser audit demo).
|
|
10
|
+
*/
|
|
11
|
+
export declare function editDistance(a: string, b: string): number;
|
|
12
|
+
//# sourceMappingURL=edit-distance.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.editDistance = editDistance;
|
|
4
|
+
/**
|
|
5
|
+
* Levenshtein distance for short-string typo detection. Rule names, tool names,
|
|
6
|
+
* and hook events are short, so edit distance is more appropriate than NCD
|
|
7
|
+
* (which is tuned for longer texts).
|
|
8
|
+
*
|
|
9
|
+
* Extracted to its own zero-dependency leaf module so the typo detectors
|
|
10
|
+
* (tool-contract, hook-events) can import it WITHOUT pulling in `core/linters.ts`,
|
|
11
|
+
* which runs a `node:fs`/`process`/`glob` side effect at import time — the
|
|
12
|
+
* blocker to running those detectors in a browser (the in-browser audit demo).
|
|
13
|
+
*/
|
|
14
|
+
function editDistance(a, b) {
|
|
15
|
+
if (a === b)
|
|
16
|
+
return 0;
|
|
17
|
+
const m = a.length;
|
|
18
|
+
const n = b.length;
|
|
19
|
+
if (m === 0)
|
|
20
|
+
return n;
|
|
21
|
+
if (n === 0)
|
|
22
|
+
return m;
|
|
23
|
+
const dp = Array.from({ length: n + 1 }, (_, i) => i);
|
|
24
|
+
for (let i = 1; i <= m; i++) {
|
|
25
|
+
let prev = dp[0];
|
|
26
|
+
dp[0] = i;
|
|
27
|
+
for (let j = 1; j <= n; j++) {
|
|
28
|
+
const tmp = dp[j];
|
|
29
|
+
dp[j] =
|
|
30
|
+
a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
31
|
+
prev = tmp;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return dp[n];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=edit-distance.js.map
|
package/dist/core/effects.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.effectSurface = effectSurface;
|
|
|
5
5
|
exports.purityViolations = purityViolations;
|
|
6
6
|
exports.pureContractViolations = pureContractViolations;
|
|
7
7
|
exports.decidePurityGate = decidePurityGate;
|
|
8
|
-
const
|
|
8
|
+
const assert_never_js_1 = require("./assert-never.js");
|
|
9
9
|
const bash_effects_js_1 = require("./bash-effects.js");
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Internal helpers
|
|
@@ -83,7 +83,7 @@ function effectSurface(tools, dialect) {
|
|
|
83
83
|
unknown.add(base);
|
|
84
84
|
break;
|
|
85
85
|
default:
|
|
86
|
-
(0,
|
|
86
|
+
(0, assert_never_js_1.assertNever)(effect);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
const purity = hasWildcard || hasBash || unknown.size > 0
|
|
@@ -170,7 +170,7 @@ function purityViolations(tools, dialect, declared) {
|
|
|
170
170
|
});
|
|
171
171
|
break;
|
|
172
172
|
default:
|
|
173
|
-
(0,
|
|
173
|
+
(0, assert_never_js_1.assertNever)(effect);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
return violations;
|
package/dist/core/hash.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
export { assertNever } from "./assert-never.js";
|
|
1
2
|
declare const __brand: unique symbol;
|
|
2
3
|
export type SHA256Hash = string & {
|
|
3
4
|
readonly [__brand]: "SHA256Hash";
|
|
4
5
|
};
|
|
5
6
|
export declare function sha256short(data: string | Buffer): SHA256Hash;
|
|
6
|
-
export declare function assertNever(x: never): never;
|
|
7
|
-
export {};
|
|
8
7
|
//# sourceMappingURL=hash.d.ts.map
|
package/dist/core/hash.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertNever = void 0;
|
|
3
4
|
exports.sha256short = sha256short;
|
|
4
|
-
exports.assertNever = assertNever;
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
// `assertNever` lives in a node-free leaf (no crypto), re-exported here so every
|
|
7
|
+
// existing `import { assertNever } from "./hash.js"` is unchanged while a
|
|
8
|
+
// browser-bound module can import it from ./assert-never.js without pulling crypto.
|
|
9
|
+
var assert_never_js_1 = require("./assert-never.js");
|
|
10
|
+
Object.defineProperty(exports, "assertNever", { enumerable: true, get: function () { return assert_never_js_1.assertNever; } });
|
|
6
11
|
const HASH_LENGTH = 16;
|
|
7
12
|
function sha256short(data) {
|
|
8
13
|
return (0, node_crypto_1.createHash)("sha256")
|
|
@@ -10,7 +15,4 @@ function sha256short(data) {
|
|
|
10
15
|
.digest("hex")
|
|
11
16
|
.slice(0, HASH_LENGTH);
|
|
12
17
|
}
|
|
13
|
-
function assertNever(x) {
|
|
14
|
-
throw new Error(`Unexpected value: ${String(x)}`);
|
|
15
|
-
}
|
|
16
18
|
//# sourceMappingURL=hash.js.map
|
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook-block-ineffective detector — the #1 verified hook pain ("false confidence").
|
|
3
|
+
*
|
|
4
|
+
* A safety hook that LOOKS like it blocks but SILENTLY DOESN'T. Two shapes:
|
|
5
|
+
*
|
|
6
|
+
* 1. **wrong-event** — the script tries to block (contains `exit 2`, a legacy
|
|
7
|
+
* `"decision":"block"` JSON, or a `"permissionDecision":"deny"`) but is
|
|
8
|
+
* registered on an event where a block is SILENTLY IGNORED ENTIRELY —
|
|
9
|
+
* SessionStart / SessionEnd / Notification / PreCompact, where exit 2 writes
|
|
10
|
+
* stderr only to the user (no veto, no model feedback). The author believes a
|
|
11
|
+
* gate is in place; nothing happens. (#19009 names the class.) NB `PostToolUse`
|
|
12
|
+
* is deliberately NOT flagged: there exit 2 feeds stderr back to the model — a
|
|
13
|
+
* legitimate FEEDBACK channel (a nudge/lint hook), not a failed block, and the
|
|
14
|
+
* block-vs-feedback intent isn't deterministically separable.
|
|
15
|
+
*
|
|
16
|
+
* 2. **wrong-field** — the hook IS on a permission-gated event (e.g. PreToolUse)
|
|
17
|
+
* but emits the LEGACY top-level `"decision":"block"` field instead of the
|
|
18
|
+
* required `hookSpecificOutput.permissionDecision:"deny"`. A copied template
|
|
19
|
+
* (PostToolUse style → PreToolUse registration) is the usual cause; the deny
|
|
20
|
+
* is silently discarded, nothing is blocked.
|
|
21
|
+
*
|
|
22
|
+
* Both shapes cause identical user-visible behaviour: the hook "works" (exits,
|
|
23
|
+
* no crash) but never actually stops anything. The only signal is "why did this
|
|
24
|
+
* run anyway?" after an incident.
|
|
25
|
+
*
|
|
26
|
+
* FP-SAFETY — conservative literal patterns only, `warn` severity by default:
|
|
27
|
+
* - `exit 2` is matched by a shell-context regex that requires surrounding
|
|
28
|
+
* whitespace/control chars to avoid false-positives on `exit 200` or a
|
|
29
|
+
* `git status --exit-code 2` argument.
|
|
30
|
+
* - JSON decision patterns are matched literally (no partial JSON walking).
|
|
31
|
+
* - Nothing is flagged on an unknown event (we only know which events CAN
|
|
32
|
+
* block because the caller injected that set).
|
|
33
|
+
*
|
|
34
|
+
* HARNESS-NEUTRAL — the sets of blocking events and permission-decision events
|
|
35
|
+
* are INJECTED from the dialect (never hard-coded here). The caller supplies the
|
|
36
|
+
* Claude Code sets; a Codex adapter supplies its own. This is the same
|
|
37
|
+
* dependency-injection pattern used by `verifyHookEvents` and `verifyToolContract`
|
|
38
|
+
* (core ⊄ adapter — one-detector-no-drift).
|
|
39
|
+
*
|
|
40
|
+
* ONE detector reused by scan + the `hook-block-ineffective` lint rule (two
|
|
41
|
+
* callers, no drift). See `research/hook-pain-points.md` for the verified corpus
|
|
42
|
+
* and `docs/compiled-hooks.md` for the authoritative fix (compiled hooks make
|
|
43
|
+
* this whole class unrepresentable).
|
|
44
|
+
*
|
|
45
|
+
* Node-free: the `readFileSync` IO is REQUIRED (injected by the caller — the disk
|
|
46
|
+
* scan passes `node:fs`, the browser engine a map-backed read), so this module
|
|
47
|
+
* never statically imports `node:fs` and bundles clean in a browser.
|
|
48
|
+
*/
|
|
1
49
|
/** The two "looks like it blocks but doesn't" failure shapes. */
|
|
2
50
|
export type HookBlockKind = "wrong-event" | "wrong-field";
|
|
3
51
|
/** One false-confidence finding: a hook that appears to block but silently won't. */
|
|
@@ -41,11 +89,12 @@ export interface HookBlockOptions {
|
|
|
41
89
|
*/
|
|
42
90
|
readonly permissionDecisionEvents: ReadonlySet<string>;
|
|
43
91
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
92
|
+
* REQUIRED file read (the disk caller injects `node:fs`
|
|
93
|
+
* `readFileSync(p, "utf8")`; the browser engine a map-backed read). Should
|
|
94
|
+
* return `""` on any error so a missing / unreadable script doesn't crash the
|
|
95
|
+
* detector — it simply produces no findings for that entry.
|
|
47
96
|
*/
|
|
48
|
-
readonly readFileSync
|
|
97
|
+
readonly readFileSync: (p: string) => string;
|
|
49
98
|
}
|
|
50
99
|
/**
|
|
51
100
|
* Detect false-confidence "blocks" across a set of hook entries.
|
|
@@ -55,8 +104,8 @@ export interface HookBlockOptions {
|
|
|
55
104
|
* (event, kind, scriptPath) pairs are de-duped.
|
|
56
105
|
*
|
|
57
106
|
* @param entries - The hook registrations to inspect (event + command/script).
|
|
58
|
-
* @param opts - Injected sets of blocking/permission events, and
|
|
59
|
-
* `readFileSync` (
|
|
107
|
+
* @param opts - Injected sets of blocking/permission events, and the required
|
|
108
|
+
* `readFileSync` (disk caller: node:fs; browser: map-backed).
|
|
60
109
|
*/
|
|
61
110
|
export declare function hookBlockIssues(entries: readonly HookScriptEntry[], opts: HookBlockOptions): HookBlockFinding[];
|
|
62
111
|
//# sourceMappingURL=hook-block-ineffective.d.ts.map
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hookBlockIssues = hookBlockIssues;
|
|
4
2
|
/**
|
|
5
3
|
* Hook-block-ineffective detector — the #1 verified hook pain ("false confidence").
|
|
6
4
|
*
|
|
@@ -44,8 +42,13 @@ exports.hookBlockIssues = hookBlockIssues;
|
|
|
44
42
|
* callers, no drift). See `research/hook-pain-points.md` for the verified corpus
|
|
45
43
|
* and `docs/compiled-hooks.md` for the authoritative fix (compiled hooks make
|
|
46
44
|
* this whole class unrepresentable).
|
|
45
|
+
*
|
|
46
|
+
* Node-free: the `readFileSync` IO is REQUIRED (injected by the caller — the disk
|
|
47
|
+
* scan passes `node:fs`, the browser engine a map-backed read), so this module
|
|
48
|
+
* never statically imports `node:fs` and bundles clean in a browser.
|
|
47
49
|
*/
|
|
48
|
-
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.hookBlockIssues = hookBlockIssues;
|
|
49
52
|
// ---------------------------------------------------------------------------
|
|
50
53
|
// Block-mechanism patterns (conservative / FP-safe)
|
|
51
54
|
// ---------------------------------------------------------------------------
|
|
@@ -79,14 +82,6 @@ const PERMISSION_DENY = /"permissionDecision"\s*:\s*"(deny|ask)"/;
|
|
|
79
82
|
// ---------------------------------------------------------------------------
|
|
80
83
|
// Detector
|
|
81
84
|
// ---------------------------------------------------------------------------
|
|
82
|
-
function defaultReadFile(p) {
|
|
83
|
-
try {
|
|
84
|
-
return (0, node_fs_1.readFileSync)(p, "utf8");
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return "";
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
85
|
/**
|
|
91
86
|
* Detect false-confidence "blocks" across a set of hook entries.
|
|
92
87
|
*
|
|
@@ -95,12 +90,12 @@ function defaultReadFile(p) {
|
|
|
95
90
|
* (event, kind, scriptPath) pairs are de-duped.
|
|
96
91
|
*
|
|
97
92
|
* @param entries - The hook registrations to inspect (event + command/script).
|
|
98
|
-
* @param opts - Injected sets of blocking/permission events, and
|
|
99
|
-
* `readFileSync` (
|
|
93
|
+
* @param opts - Injected sets of blocking/permission events, and the required
|
|
94
|
+
* `readFileSync` (disk caller: node:fs; browser: map-backed).
|
|
100
95
|
*/
|
|
101
96
|
function hookBlockIssues(entries, opts) {
|
|
102
97
|
const { noEffectEvents, permissionDecisionEvents } = opts;
|
|
103
|
-
const readFile = opts.readFileSync
|
|
98
|
+
const readFile = opts.readFileSync;
|
|
104
99
|
const findings = [];
|
|
105
100
|
const seen = new Set();
|
|
106
101
|
for (const entry of entries) {
|
package/dist/core/hook-events.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.confidentHookEventIssues = confidentHookEventIssues;
|
|
4
4
|
exports.verifyHookEvents = verifyHookEvents;
|
|
5
|
-
const
|
|
5
|
+
const edit_distance_js_1 = require("./edit-distance.js");
|
|
6
6
|
/** Closest known hook event by edit distance (≤ 2) — a confidence signal. */
|
|
7
7
|
function closestEvent(event, dialect) {
|
|
8
8
|
let best = null;
|
|
9
9
|
let bestDistance = Infinity;
|
|
10
10
|
for (const known of dialect.hookEvents) {
|
|
11
|
-
const d = (0,
|
|
11
|
+
const d = (0, edit_distance_js_1.editDistance)(event.toLowerCase(), known.toLowerCase());
|
|
12
12
|
if (d < bestDistance) {
|
|
13
13
|
bestDistance = d;
|
|
14
14
|
best = known;
|
package/dist/core/linters.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* This is the core moat — no other tool resolves rules across 7 catalog APIs
|
|
10
10
|
* (6 linters + Cedar policy language) and checks config-enabled status.
|
|
11
11
|
*/
|
|
12
|
+
import { editDistance } from "./edit-distance.js";
|
|
12
13
|
export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
|
|
13
14
|
export interface LinterCheckResult {
|
|
14
15
|
exists: boolean;
|
|
@@ -24,12 +25,7 @@ export interface DetectedLinter {
|
|
|
24
25
|
}
|
|
25
26
|
/** @internal */ export declare function extractLinterName(enforcedBy: string): string;
|
|
26
27
|
/** @internal */ export declare function extractRuleName(enforcedBy: string): string | null;
|
|
27
|
-
|
|
28
|
-
* Levenshtein distance for short-string typo detection. Rule names are
|
|
29
|
-
* short so edit distance is more appropriate than NCD (which is tuned
|
|
30
|
-
* for longer texts).
|
|
31
|
-
*/
|
|
32
|
-
export declare function editDistance(a: string, b: string): number;
|
|
28
|
+
export { editDistance };
|
|
33
29
|
/** @internal */ export declare function clearCedarCache(): void;
|
|
34
30
|
/**
|
|
35
31
|
* Check a single linter rule reference (e.g., "eslint/no-console").
|
package/dist/core/linters.js
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
* (6 linters + Cedar policy language) and checks config-enabled status.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.editDistance = void 0;
|
|
14
15
|
exports.extractLinterName = extractLinterName;
|
|
15
16
|
exports.extractRuleName = extractRuleName;
|
|
16
|
-
exports.editDistance = editDistance;
|
|
17
17
|
exports.clearCedarCache = clearCedarCache;
|
|
18
18
|
exports.checkLinterRule = checkLinterRule;
|
|
19
19
|
const node_fs_1 = require("node:fs");
|
|
20
20
|
const node_path_1 = require("node:path");
|
|
21
|
+
const edit_distance_js_1 = require("./edit-distance.js");
|
|
22
|
+
Object.defineProperty(exports, "editDistance", { enumerable: true, get: function () { return edit_distance_js_1.editDistance; } });
|
|
21
23
|
const node_child_process_1 = require("node:child_process");
|
|
22
24
|
const node_module_1 = require("node:module");
|
|
23
25
|
const glob_1 = require("glob");
|
|
@@ -378,38 +380,11 @@ function ruleFileExists(ruleName, rulesDir, basePath) {
|
|
|
378
380
|
function makeResult(ctx, exists, enabled = "unknown", error) {
|
|
379
381
|
return { exists, enabled, linter: ctx.linterName, rule: ctx.ruleName, error };
|
|
380
382
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Levenshtein distance for short-string typo detection. Rule names are
|
|
383
|
-
* short so edit distance is more appropriate than NCD (which is tuned
|
|
384
|
-
* for longer texts).
|
|
385
|
-
*/
|
|
386
|
-
function editDistance(a, b) {
|
|
387
|
-
if (a === b)
|
|
388
|
-
return 0;
|
|
389
|
-
const m = a.length;
|
|
390
|
-
const n = b.length;
|
|
391
|
-
if (m === 0)
|
|
392
|
-
return n;
|
|
393
|
-
if (n === 0)
|
|
394
|
-
return m;
|
|
395
|
-
const dp = Array.from({ length: n + 1 }, (_, i) => i);
|
|
396
|
-
for (let i = 1; i <= m; i++) {
|
|
397
|
-
let prev = dp[0];
|
|
398
|
-
dp[0] = i;
|
|
399
|
-
for (let j = 1; j <= n; j++) {
|
|
400
|
-
const tmp = dp[j];
|
|
401
|
-
dp[j] =
|
|
402
|
-
a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
403
|
-
prev = tmp;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return dp[n];
|
|
407
|
-
}
|
|
408
383
|
/** Top-N closest rule names by edit distance, filtered by a max distance. */
|
|
409
384
|
function closestRuleNames(target, candidates, limit = 3, maxDistance = 4) {
|
|
410
385
|
const scored = [];
|
|
411
386
|
for (const c of candidates) {
|
|
412
|
-
const d = editDistance(target, c);
|
|
387
|
+
const d = (0, edit_distance_js_1.editDistance)(target, c);
|
|
413
388
|
if (d <= maxDistance)
|
|
414
389
|
scored.push({ name: c, dist: d });
|
|
415
390
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure, node-free half of the live MCP contract-tool check: the error shape
|
|
3
|
+
* and its human-readable formatter, split out of `core/mcp.ts` (which statically
|
|
4
|
+
* imports `node:child_process` + `./refs.js` for the SPAWN path) so a consumer
|
|
5
|
+
* that only needs the message — the deterministic audit report — doesn't drag the
|
|
6
|
+
* live-verification machinery into its bundle. `mcp.ts` re-exports both, so its
|
|
7
|
+
* existing consumers are unchanged; `verifyMcpContractTools` (the spawn) stays
|
|
8
|
+
* there. Node-free by construction (a local `assertNever`, no `node:` import).
|
|
9
|
+
*/
|
|
10
|
+
export type McpContractToolReason = "server-unreachable" | "tool-missing";
|
|
11
|
+
export interface McpContractToolError {
|
|
12
|
+
/** The full `mcp__server__tool` reference (restriction suffix stripped). */
|
|
13
|
+
readonly tool: string;
|
|
14
|
+
readonly server: string;
|
|
15
|
+
/** The tool segment (what's looked up on the server). */
|
|
16
|
+
readonly toolName: string;
|
|
17
|
+
readonly reason: McpContractToolReason;
|
|
18
|
+
readonly suggestions: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
21
|
+
export declare function mcpContractToolMessage(e: McpContractToolError): string;
|
|
22
|
+
//# sourceMappingURL=mcp-contract-message.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The pure, node-free half of the live MCP contract-tool check: the error shape
|
|
4
|
+
* and its human-readable formatter, split out of `core/mcp.ts` (which statically
|
|
5
|
+
* imports `node:child_process` + `./refs.js` for the SPAWN path) so a consumer
|
|
6
|
+
* that only needs the message — the deterministic audit report — doesn't drag the
|
|
7
|
+
* live-verification machinery into its bundle. `mcp.ts` re-exports both, so its
|
|
8
|
+
* existing consumers are unchanged; `verifyMcpContractTools` (the spawn) stays
|
|
9
|
+
* there. Node-free by construction (a local `assertNever`, no `node:` import).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.mcpContractToolMessage = mcpContractToolMessage;
|
|
13
|
+
function assertNever(x) {
|
|
14
|
+
throw new Error(`Unexpected value: ${String(x)}`);
|
|
15
|
+
}
|
|
16
|
+
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
17
|
+
function mcpContractToolMessage(e) {
|
|
18
|
+
switch (e.reason) {
|
|
19
|
+
case "server-unreachable":
|
|
20
|
+
return `MCP tool "${e.tool}" — server "${e.server}" failed to start`;
|
|
21
|
+
case "tool-missing":
|
|
22
|
+
return `MCP tool "${e.tool}" not found on server "${e.server}"${e.suggestions.length > 0
|
|
23
|
+
? ` — did you mean ${e.suggestions.map((s) => `"${s}"`).join(", ")}?`
|
|
24
|
+
: ""}`;
|
|
25
|
+
default:
|
|
26
|
+
return assertNever(e.reason);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mcp-contract-message.js.map
|