vigiles 14.7.0 → 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 +1 -1
- package/dist/audit-report.template.html +15 -15
- package/dist/audit-score.d.ts +1 -1
- package/dist/audit-score.js +19 -19
- package/dist/audit-verdict.d.ts +1 -1
- package/dist/audit-verdict.js +3 -3
- 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/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/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/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:
|
|
102
|
+
weight: score_core_js_1.W_DANGLING_REF,
|
|
103
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
|
@@ -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
|
@@ -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,7 +238,7 @@ 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
244
|
return `${capitalize(numberWord(fixesToNextGrade))} ${fixNoun(fixesToNextGrade)} away from ${article(nextGrade)} ${nextGrade}.`;
|
|
@@ -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;
|
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) {
|
|
@@ -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
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { mcpContractToolMessage } from "./mcp-contract-message.js";
|
|
2
|
+
import type { McpContractToolError, McpContractToolReason } from "./mcp-contract-message.js";
|
|
3
|
+
export { mcpContractToolMessage };
|
|
4
|
+
export type { McpContractToolError, McpContractToolReason };
|
|
1
5
|
export interface McpServerConfig {
|
|
2
6
|
readonly command: string;
|
|
3
7
|
readonly args?: readonly string[];
|
|
@@ -43,19 +47,7 @@ export declare function loadMcpServers(cwd: string): Record<string, McpServerCon
|
|
|
43
47
|
* to an undeclared server, an unreachable server, or a missing tool is an error.
|
|
44
48
|
*/
|
|
45
49
|
export declare function verifyMcpRefs(markdown: string, mcpServers: Record<string, McpServerConfig>, timeoutMs?: number): Promise<McpRefError[]>;
|
|
46
|
-
export type McpContractToolReason = "server-unreachable" | "tool-missing";
|
|
47
|
-
export interface McpContractToolError {
|
|
48
|
-
/** The full `mcp__server__tool` reference (restriction suffix stripped). */
|
|
49
|
-
readonly tool: string;
|
|
50
|
-
readonly server: string;
|
|
51
|
-
/** The tool segment (what's looked up on the server). */
|
|
52
|
-
readonly toolName: string;
|
|
53
|
-
readonly reason: McpContractToolReason;
|
|
54
|
-
readonly suggestions: string[];
|
|
55
|
-
}
|
|
56
50
|
export declare function verifyMcpContractTools(tools: readonly string[], servers: Record<string, McpServerConfig>, dialect: import("./dialect.js").HarnessDialect, timeoutMs?: number): Promise<McpContractToolError[]>;
|
|
57
|
-
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
58
|
-
export declare function mcpContractToolMessage(e: McpContractToolError): string;
|
|
59
51
|
/** Human-readable message for an MCP reference error (with "did you mean"). */
|
|
60
52
|
export declare function mcpRefMessage(e: McpRefError): string;
|
|
61
53
|
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/core/mcp.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mcpContractToolMessage = void 0;
|
|
3
4
|
exports.listMcpTools = listMcpTools;
|
|
4
5
|
exports.verifyMcpTool = verifyMcpTool;
|
|
5
6
|
exports.parseMcpRefs = parseMcpRefs;
|
|
6
7
|
exports.loadMcpServers = loadMcpServers;
|
|
7
8
|
exports.verifyMcpRefs = verifyMcpRefs;
|
|
8
9
|
exports.verifyMcpContractTools = verifyMcpContractTools;
|
|
9
|
-
exports.mcpContractToolMessage = mcpContractToolMessage;
|
|
10
10
|
exports.mcpRefMessage = mcpRefMessage;
|
|
11
11
|
/**
|
|
12
12
|
* Minimal MCP client over stdio — start a server, do the JSON-RPC handshake, and
|
|
@@ -24,6 +24,8 @@ const node_path_1 = require("node:path");
|
|
|
24
24
|
const refs_js_1 = require("./refs.js");
|
|
25
25
|
const hash_js_1 = require("./hash.js");
|
|
26
26
|
const mcp_tool_js_1 = require("./mcp-tool.js");
|
|
27
|
+
const mcp_contract_message_js_1 = require("./mcp-contract-message.js");
|
|
28
|
+
Object.defineProperty(exports, "mcpContractToolMessage", { enumerable: true, get: function () { return mcp_contract_message_js_1.mcpContractToolMessage; } });
|
|
27
29
|
function dispatch(line, pending) {
|
|
28
30
|
let msg;
|
|
29
31
|
try {
|
|
@@ -283,19 +285,6 @@ async function verifyMcpContractTools(tools, servers, dialect, timeoutMs = 10000
|
|
|
283
285
|
}
|
|
284
286
|
return errors;
|
|
285
287
|
}
|
|
286
|
-
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
287
|
-
function mcpContractToolMessage(e) {
|
|
288
|
-
switch (e.reason) {
|
|
289
|
-
case "server-unreachable":
|
|
290
|
-
return `MCP tool "${e.tool}" — server "${e.server}" failed to start`;
|
|
291
|
-
case "tool-missing":
|
|
292
|
-
return `MCP tool "${e.tool}" not found on server "${e.server}"${e.suggestions.length > 0
|
|
293
|
-
? ` — did you mean ${e.suggestions.map((s) => `"${s}"`).join(", ")}?`
|
|
294
|
-
: ""}`;
|
|
295
|
-
default:
|
|
296
|
-
return (0, hash_js_1.assertNever)(e.reason);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
288
|
/** Human-readable message for an MCP reference error (with "did you mean"). */
|
|
300
289
|
function mcpRefMessage(e) {
|
|
301
290
|
switch (e.reason) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized Compression Distance — information-theoretic similarity.
|
|
3
|
+
*
|
|
4
|
+
* NCD(x, y) = (C(xy) - min(C(x), C(y))) / max(C(x), C(y))
|
|
5
|
+
*
|
|
6
|
+
* Range: [0, 1+ε] where 0 = identical information content.
|
|
7
|
+
* Deterministic. No model dependency. Approximates the universal distance metric.
|
|
8
|
+
*
|
|
9
|
+
* Reference: Li, Chen, Li, Ma, Vitányi (2004) "The Similarity Metric"
|
|
10
|
+
*/
|
|
11
|
+
export declare function ncd(a: string, b: string): number;
|
|
12
|
+
//# sourceMappingURL=ncd.d.ts.map
|
package/dist/core/ncd.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ncd = ncd;
|
|
4
|
+
/**
|
|
5
|
+
* Normalized Compression Distance (NCD) — the model-free similarity engine.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from proofs.ts as a NODE-FREE leaf: it depends only on `gzipSync`
|
|
8
|
+
* (aliased to `pako` in the browser build) and `TextEncoder` (a Web/Node global),
|
|
9
|
+
* so the description-overlap detector can reach it WITHOUT dragging proofs.ts's
|
|
10
|
+
* `node:crypto` (hash.js) import into the in-browser audit bundle. proofs.ts
|
|
11
|
+
* re-exports `ncd` for its existing consumers; the detector imports it from here.
|
|
12
|
+
*
|
|
13
|
+
* `TextEncoder` (not `Buffer.from`) keeps the byte input identical across Node and
|
|
14
|
+
* the browser and removes the node-only `Buffer` global — utf-8 bytes are the same
|
|
15
|
+
* either way, so gzip sees identical input and the compressed length matches.
|
|
16
|
+
*/
|
|
17
|
+
const node_zlib_1 = require("node:zlib");
|
|
18
|
+
const utf8 = new TextEncoder();
|
|
19
|
+
/**
|
|
20
|
+
* Compressed size of a string via gzip — approximates Kolmogorov complexity
|
|
21
|
+
* (the length of the shortest program that produces the string).
|
|
22
|
+
*/
|
|
23
|
+
function compressedSize(s) {
|
|
24
|
+
return (0, node_zlib_1.gzipSync)(utf8.encode(s), { level: 9 }).length;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Normalized Compression Distance — information-theoretic similarity.
|
|
28
|
+
*
|
|
29
|
+
* NCD(x, y) = (C(xy) - min(C(x), C(y))) / max(C(x), C(y))
|
|
30
|
+
*
|
|
31
|
+
* Range: [0, 1+ε] where 0 = identical information content.
|
|
32
|
+
* Deterministic. No model dependency. Approximates the universal distance metric.
|
|
33
|
+
*
|
|
34
|
+
* Reference: Li, Chen, Li, Ma, Vitányi (2004) "The Similarity Metric"
|
|
35
|
+
*/
|
|
36
|
+
function ncd(a, b) {
|
|
37
|
+
if (a === b)
|
|
38
|
+
return 0;
|
|
39
|
+
if (a.length === 0 && b.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
const ca = compressedSize(a);
|
|
42
|
+
const cb = compressedSize(b);
|
|
43
|
+
const cab = compressedSize(a + b);
|
|
44
|
+
const minC = Math.min(ca, cb);
|
|
45
|
+
const maxC = Math.max(ca, cb);
|
|
46
|
+
if (maxC === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return (cab - minC) / maxC;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=ncd.js.map
|
|
@@ -6,13 +6,13 @@ export interface PluginLayoutFinding {
|
|
|
6
6
|
readonly message: string;
|
|
7
7
|
}
|
|
8
8
|
export interface PluginLayoutOptions {
|
|
9
|
-
/**
|
|
10
|
-
readonly existsSync
|
|
9
|
+
/** REQUIRED, injected: does this path exist? (disk: node:fs existsSync) */
|
|
10
|
+
readonly existsSync: (p: string) => boolean;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* REQUIRED, injected: is this path a directory? (disk: node:fs
|
|
13
13
|
* statSync(p).isDirectory(), returning false on any throw)
|
|
14
14
|
*/
|
|
15
|
-
readonly isDirectory
|
|
15
|
+
readonly isDirectory: (p: string) => boolean;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Surface directories found nested INSIDE the manifest directory, where they are
|
|
@@ -26,5 +26,5 @@ export interface PluginLayoutOptions {
|
|
|
26
26
|
* Returns `[]` when the manifest dir doesn't exist or holds no misplaced surface
|
|
27
27
|
* dirs.
|
|
28
28
|
*/
|
|
29
|
-
export declare function pluginDirLayoutIssues(manifestDir: string, surfaceDirNames: readonly string[], opts
|
|
29
|
+
export declare function pluginDirLayoutIssues(manifestDir: string, surfaceDirNames: readonly string[], opts: PluginLayoutOptions): PluginLayoutFinding[];
|
|
30
30
|
//# sourceMappingURL=plugin-dir-layout.d.ts.map
|