vigiles 10.0.0 → 11.0.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/README.md +113 -83
- package/dist/adapters/claude-code/dialect.js +15 -0
- package/dist/audit-report.d.ts +1 -1
- package/dist/audit-report.template.html +1 -1
- package/dist/audit-score.d.ts +19 -12
- package/dist/audit-score.js +65 -11
- package/dist/cli.js +249 -0
- package/dist/core/CLAUDE.md.spec.d.ts +3 -0
- package/dist/core/CLAUDE.md.spec.js +26 -0
- package/dist/core/delegation-trifecta.d.ts +64 -0
- package/dist/core/delegation-trifecta.js +124 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/hook-block-ineffective.d.ts +62 -0
- package/dist/core/hook-block-ineffective.js +153 -0
- package/dist/core/hook-matcher.d.ts +66 -0
- package/dist/core/hook-matcher.js +182 -0
- package/dist/core/hook-normalize.d.ts +43 -0
- package/dist/core/hook-normalize.js +78 -0
- package/dist/core/lethal-trifecta.d.ts +100 -0
- package/dist/core/lethal-trifecta.js +197 -0
- package/dist/core/plugin-dir-layout.d.ts +30 -0
- package/dist/core/plugin-dir-layout.js +73 -0
- package/dist/core/rule-meta.d.ts +82 -0
- package/dist/core/rule-meta.js +266 -0
- package/dist/core/skill-missing-fence.d.ts +47 -0
- package/dist/core/skill-missing-fence.js +119 -0
- package/dist/core/skill-resources.d.ts +27 -0
- package/dist/core/skill-resources.js +167 -0
- package/dist/core/types.d.ts +71 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +26 -4
- package/dist/leaderboard.d.ts +1 -0
- package/dist/leaderboard.js +42 -4
- package/dist/scan.d.ts +106 -0
- package/dist/scan.js +251 -45
- package/dist/setup-plan.d.ts +6 -3
- package/dist/setup-plan.js +12 -2
- package/package.json +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -220,6 +220,77 @@ export interface RulesConfig {
|
|
|
220
220
|
* as `scan` (mcpHookIssues).
|
|
221
221
|
*/
|
|
222
222
|
"mcp-hook-target-resolves"?: RuleSeverity;
|
|
223
|
+
/**
|
|
224
|
+
* Flag a unit (subagent / model-invocable skill) whose declared tools hold all
|
|
225
|
+
* THREE legs of Simon Willison's "lethal trifecta" — read private data, ingest
|
|
226
|
+
* untrusted content, AND exfiltrate — a prompt-injection exfil path with no
|
|
227
|
+
* exploit code (Meta's Rule of Two: allow at most two). A capability SET-
|
|
228
|
+
* intersection over the declared contract, NOT a text scan; high-precision (only
|
|
229
|
+
* well-known tools map to a leg). An EXPLICIT all-three contract is a "hard"
|
|
230
|
+
* finding; an inherits-all unit (no contract → every leg) is "advisory". Default
|
|
231
|
+
* "warn" (don't-cry-wolf rollout); raise to "error" to gate CI. Same detector as
|
|
232
|
+
* `scan` (lethalTrifectaIssues). See docs/rules/lethal-trifecta.md.
|
|
233
|
+
*/
|
|
234
|
+
"lethal-trifecta"?: RuleSeverity;
|
|
235
|
+
/**
|
|
236
|
+
* Flag a SKILL.md body referencing a bundled file (`scripts/`/`references/`/
|
|
237
|
+
* `assets/`, or a relative markdown link with an extension) that doesn't exist
|
|
238
|
+
* on disk under the skill dir — the agent reads the instruction, gets nothing,
|
|
239
|
+
* and silently continues. The cross-reference moat applied to the SKILL.md body.
|
|
240
|
+
* High-precision / FP-safe (skips URLs, `$VAR` tokens, `../` escapes, extension-
|
|
241
|
+
* less mentions). Default "warn"; raise to "error" to gate CI. Same detector as
|
|
242
|
+
* `scan` (skillResourceIssues). See docs/rules/skill-resource-resolves.md.
|
|
243
|
+
*/
|
|
244
|
+
"skill-resource-resolves"?: RuleSeverity;
|
|
245
|
+
/**
|
|
246
|
+
* Flag a SKILL.md that opens with frontmatter-looking keys (`name:`,
|
|
247
|
+
* `description:`, …) but has NO opening `---` fence — the harness loads the
|
|
248
|
+
* whole file as body, so the skill has no name/description/trigger and is
|
|
249
|
+
* invisible (never fires). High-precision (a fixed key whitelist; markdown /
|
|
250
|
+
* prose lines never match). Default "warn"; raise to "error" to gate CI. Same
|
|
251
|
+
* detector as `scan` (skillFenceIssues). See docs/rules/skill-missing-fence.md.
|
|
252
|
+
*/
|
|
253
|
+
"skill-missing-fence"?: RuleSeverity;
|
|
254
|
+
/**
|
|
255
|
+
* Flag a functional surface directory (skills/agents/commands) nested INSIDE
|
|
256
|
+
* the `.claude-plugin/` manifest dir, where only `plugin.json` belongs — the
|
|
257
|
+
* harness can't see it, so the surface is invisible (the #1 plugin-author
|
|
258
|
+
* mistake). Pure filesystem check, FP-safe. Default "warn"; raise to "error"
|
|
259
|
+
* to gate CI. Same detector as `scan` (pluginLayoutIssues). See
|
|
260
|
+
* docs/rules/plugin-dir-layout.md.
|
|
261
|
+
*/
|
|
262
|
+
"plugin-dir-layout"?: RuleSeverity;
|
|
263
|
+
/**
|
|
264
|
+
* Flag a lethal trifecta that EMERGES across a delegation edge — a subagent
|
|
265
|
+
* whose effective (own ∪ delegated-to) capability holds all three legs though
|
|
266
|
+
* no single unit does (the combined blast radius). The capability-diff across
|
|
267
|
+
* the delegation tree; skips units the per-unit `lethal-trifecta` already
|
|
268
|
+
* flags (no double-report), FP-safe (explicit edges, wildcard-guarded). Default
|
|
269
|
+
* "warn"; raise to "error" to gate CI. Same detector as `scan`
|
|
270
|
+
* (delegationTrifecta). See docs/rules/delegation-trifecta.md.
|
|
271
|
+
*/
|
|
272
|
+
"delegation-trifecta"?: RuleSeverity;
|
|
273
|
+
/**
|
|
274
|
+
* Flag a hook that LOOKS like it blocks but silently doesn't — a block decision
|
|
275
|
+
* (`exit 2` / `decision` / `permissionDecision`) on an event that can't veto, or
|
|
276
|
+
* the legacy top-level `decision` field on a permission-gated event where only
|
|
277
|
+
* `hookSpecificOutput.permissionDecision` works (#19009, the #1 verified hook
|
|
278
|
+
* pain). FP-safe (conservative literal patterns; the blocking-event sets are
|
|
279
|
+
* read from the dialect, so it runs only where they're declared). Default "warn";
|
|
280
|
+
* raise to "error" to gate CI. Same detector as `scan` (hookBlockFindings). See
|
|
281
|
+
* docs/rules/hook-block-ineffective.md.
|
|
282
|
+
*/
|
|
283
|
+
"hook-block-ineffective"?: RuleSeverity;
|
|
284
|
+
/**
|
|
285
|
+
* Flag a hook `matcher` string that silently never fires — a close typo of a
|
|
286
|
+
* built-in tool (`bash`→`Bash`), or a malformed/undeclared MCP form
|
|
287
|
+
* (`mcp_memory_*` instead of `mcp__memory__.*`, or a server the plugin doesn't
|
|
288
|
+
* declare). High-precision (close-typo only; MCP gated on a declared set,
|
|
289
|
+
* built-ins allowlisted; wildcards/regex skipped). Default "warn"; raise to
|
|
290
|
+
* "error" to gate CI. Same detector as `scan` (hookMatcherFindings). See
|
|
291
|
+
* docs/rules/hook-matcher.md.
|
|
292
|
+
*/
|
|
293
|
+
"hook-matcher"?: RuleSeverity;
|
|
223
294
|
}
|
|
224
295
|
/** Extract severity from a rule value (handles both simple and tuple forms). */
|
|
225
296
|
export declare function ruleSeverity<T>(rule: RuleWithOptions<T> | undefined): RuleSeverity;
|
package/dist/core/validate.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions } from "./types.js";
|
|
2
2
|
export type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions, };
|
|
3
|
+
export declare const DEFAULT_RULES: Required<RulesConfig>;
|
|
3
4
|
export declare function findInstructionFiles(cwd?: string, configFiles?: string[]): string[];
|
|
4
5
|
export declare function loadConfig(): VigilesConfig;
|
|
5
6
|
export declare function parseRules(content: string, { ruleMarkers }?: ParseOptions): ParsedRule[];
|
package/dist/core/validate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_RULES = void 0;
|
|
3
4
|
exports.findInstructionFiles = findInstructionFiles;
|
|
4
5
|
exports.loadConfig = loadConfig;
|
|
5
6
|
exports.parseRules = parseRules;
|
|
@@ -28,7 +29,7 @@ const VALID_MARKERS = ["headings", "checkboxes"];
|
|
|
28
29
|
const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
29
30
|
// The default instruction file to validate when no config names one.
|
|
30
31
|
const DEFAULT_FILES = [INSTRUCTION_FILES[0]];
|
|
31
|
-
|
|
32
|
+
exports.DEFAULT_RULES = {
|
|
32
33
|
"require-instructions-spec": "warn",
|
|
33
34
|
// Default OFF — the consistent `require-<surface>-spec` parallel. Skills are
|
|
34
35
|
// legitimately hand-written, so requiring a .spec.ts per SKILL.md is the wrong
|
|
@@ -70,10 +71,31 @@ const DEFAULT_RULES = {
|
|
|
70
71
|
"frontmatter-valid": "warn",
|
|
71
72
|
// A mcp_tool hook incomplete / targeting an undeclared server — on by default at warn.
|
|
72
73
|
"mcp-hook-target-resolves": "warn",
|
|
74
|
+
// Lethal-trifecta capability set-intersection (read-private + ingest-untrusted +
|
|
75
|
+
// exfiltrate in one unit) — WARN by default (don't-cry-wolf rollout); raise to error.
|
|
76
|
+
"lethal-trifecta": "warn",
|
|
77
|
+
// A SKILL.md body referencing a missing bundled resource — WARN by default
|
|
78
|
+
// (don't-cry-wolf rollout, FP-safe); raise to error to gate CI.
|
|
79
|
+
"skill-resource-resolves": "warn",
|
|
80
|
+
// A SKILL.md missing its opening `---` fence (invisible skill) — WARN by
|
|
81
|
+
// default (FP-safe key whitelist); raise to error to gate CI.
|
|
82
|
+
"skill-missing-fence": "warn",
|
|
83
|
+
// Functional dirs nested inside `.claude-plugin/` (invisible surfaces) — WARN
|
|
84
|
+
// by default; raise to error to gate CI.
|
|
85
|
+
"plugin-dir-layout": "warn",
|
|
86
|
+
// A lethal trifecta emerging across a delegation edge (combined blast radius) —
|
|
87
|
+
// WARN by default (don't-cry-wolf rollout); raise to error to gate CI.
|
|
88
|
+
"delegation-trifecta": "warn",
|
|
89
|
+
// A hook that looks like it blocks but silently doesn't (#19009) — WARN by
|
|
90
|
+
// default (FP-safe literal patterns); raise to error to gate CI.
|
|
91
|
+
"hook-block-ineffective": "warn",
|
|
92
|
+
// A hook matcher that never fires (tool typo / wrong MCP form) — WARN by
|
|
93
|
+
// default (high-precision); raise to error to gate CI.
|
|
94
|
+
"hook-matcher": "warn",
|
|
73
95
|
};
|
|
74
96
|
const DEFAULT_CONFIG = {
|
|
75
97
|
ruleMarkers: ["headings", "checkboxes"],
|
|
76
|
-
rules: DEFAULT_RULES,
|
|
98
|
+
rules: exports.DEFAULT_RULES,
|
|
77
99
|
files: DEFAULT_FILES,
|
|
78
100
|
};
|
|
79
101
|
// ---------------------------------------------------------------------------
|
|
@@ -99,7 +121,7 @@ function loadConfig() {
|
|
|
99
121
|
const config = {
|
|
100
122
|
...DEFAULT_CONFIG,
|
|
101
123
|
...userConfig,
|
|
102
|
-
rules: { ...DEFAULT_RULES, ...userConfig.rules },
|
|
124
|
+
rules: { ...exports.DEFAULT_RULES, ...userConfig.rules },
|
|
103
125
|
files: Array.isArray(userConfig.files) ? userConfig.files : DEFAULT_FILES,
|
|
104
126
|
};
|
|
105
127
|
if (!Array.isArray(config.ruleMarkers) ||
|
|
@@ -170,7 +192,7 @@ function parseRules(content, { ruleMarkers } = {}) {
|
|
|
170
192
|
// Core validation
|
|
171
193
|
// ---------------------------------------------------------------------------
|
|
172
194
|
function validate(content, { ruleMarkers, rules: rulesConfig, filePath, dialect } = {}) {
|
|
173
|
-
const activeRules = rulesConfig ?? DEFAULT_RULES;
|
|
195
|
+
const activeRules = rulesConfig ?? exports.DEFAULT_RULES;
|
|
174
196
|
const parsedRules = parseRules(content, { ruleMarkers });
|
|
175
197
|
const enforced = parsedRules.filter((r) => r.enforcement === "enforced").length;
|
|
176
198
|
const guidanceOnly = parsedRules.filter((r) => r.enforcement === "guidance").length;
|
package/dist/leaderboard.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare const W_NO_DESCRIPTION = 10;
|
|
|
26
26
|
export declare const W_DANGLING_REF = 8;
|
|
27
27
|
export declare const W_OVERLAP = 8;
|
|
28
28
|
export declare const W_NO_CONTRACT = 5;
|
|
29
|
+
export declare const W_TRIFECTA = 20;
|
|
29
30
|
/** Map a 0–100 structural-health score to its letter grade (A ≥90 … F <60). */
|
|
30
31
|
export declare function gradeFor(score: number): PluginScore["grade"];
|
|
31
32
|
/** One deduction: a count, its per-item weight, and the label if non-zero. */
|
package/dist/leaderboard.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* model and stack on top later; this part runs anywhere in CI for free.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.W_NO_CONTRACT = exports.W_OVERLAP = exports.W_DANGLING_REF = exports.W_NO_DESCRIPTION = exports.W_MISSING_HOOK = void 0;
|
|
15
|
+
exports.W_TRIFECTA = exports.W_NO_CONTRACT = exports.W_OVERLAP = exports.W_DANGLING_REF = exports.W_NO_DESCRIPTION = exports.W_MISSING_HOOK = void 0;
|
|
16
16
|
exports.gradeFor = gradeFor;
|
|
17
17
|
exports.reportDeductions = reportDeductions;
|
|
18
18
|
exports.isEmptyMachine = isEmptyMachine;
|
|
@@ -49,6 +49,7 @@ exports.W_NO_DESCRIPTION = 10; // a skill with no usable description → can't t
|
|
|
49
49
|
exports.W_DANGLING_REF = 8; // a referenced intra-plugin file that's missing → broken path
|
|
50
50
|
exports.W_OVERLAP = 8; // a description collision → the wrong skill fires
|
|
51
51
|
exports.W_NO_CONTRACT = 5; // generic small-footgun weight (disallowedTools typo, invalid model/color)
|
|
52
|
+
exports.W_TRIFECTA = 20; // a HARD lethal-trifecta contract (all three legs, explicit) → a declared prompt-injection exfil path
|
|
52
53
|
// Two things are advisory, NOT graded penalties (shown, never scored — see scoreReport):
|
|
53
54
|
// - untested surfaces — a hardening gap, not breakage.
|
|
54
55
|
// - an agent that inherits all tools (no `tools:` line) — see reportDeductions for why.
|
|
@@ -77,7 +78,17 @@ function reportDeductions(r) {
|
|
|
77
78
|
const deadTools = r.agents.reduce((n, a) => n + a.toolIssues.length, 0);
|
|
78
79
|
const deadMcpTools = r.agents.reduce((n, a) => n + a.mcpToolIssues.length, 0);
|
|
79
80
|
const deadDisallowed = r.agents.reduce((n, a) => n + a.disallowedToolIssues.length, 0);
|
|
81
|
+
// HARD lethal-trifecta findings only — an EXPLICIT contract naming all three
|
|
82
|
+
// legs (a declared exfil path). Advisory (inherits-all) trifecta findings are
|
|
83
|
+
// surfaced but NEVER graded (aligned with the inherits-all stance), so they're
|
|
84
|
+
// excluded here.
|
|
85
|
+
const hardTrifecta = r.trifectaFindings.filter((f) => f.finding.severity === "hard").length;
|
|
80
86
|
return [
|
|
87
|
+
{
|
|
88
|
+
n: hardTrifecta,
|
|
89
|
+
weight: exports.W_TRIFECTA,
|
|
90
|
+
label: "unit(s) holding all three lethal-trifecta legs (prompt-injection exfil path)",
|
|
91
|
+
},
|
|
81
92
|
{
|
|
82
93
|
n: missingHooks,
|
|
83
94
|
weight: exports.W_MISSING_HOOK,
|
|
@@ -146,6 +157,33 @@ function reportDeductions(r) {
|
|
|
146
157
|
weight: exports.W_DANGLING_REF,
|
|
147
158
|
label: "mcp_tool hook(s) incomplete / targeting an undeclared server",
|
|
148
159
|
},
|
|
160
|
+
{
|
|
161
|
+
n: r.skillResourceIssues.length,
|
|
162
|
+
weight: exports.W_DANGLING_REF,
|
|
163
|
+
label: "skill bundled-resource ref(s) that don't resolve on disk",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
n: r.skillFenceIssues.length,
|
|
167
|
+
weight: exports.W_NO_DESCRIPTION,
|
|
168
|
+
label: "invisible skill(s) (frontmatter with no opening `---` fence)",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
n: r.pluginLayoutIssues.length,
|
|
172
|
+
weight: exports.W_NO_DESCRIPTION,
|
|
173
|
+
label: "functional dir(s) misplaced inside `.claude-plugin/` (invisible)",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
n: r.hookBlockFindings.length,
|
|
177
|
+
weight: exports.W_MISSING_HOOK,
|
|
178
|
+
label: "hook(s) that look like they block but silently don't",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
n: r.hookMatcherFindings.length,
|
|
182
|
+
weight: exports.W_MISSING_HOOK,
|
|
183
|
+
label: "hook matcher(s) that never fire (typo / wrong MCP form)",
|
|
184
|
+
},
|
|
185
|
+
// NB: delegationTrifecta (like the advisory per-unit/inherits-all trifecta) is a
|
|
186
|
+
// ⚠ RISK, surfaced but NOT graded — only the HARD per-unit trifecta above scores.
|
|
149
187
|
// NB: untested surfaces are NOT a penalty — an untested surface is a hardening
|
|
150
188
|
// gap, not breakage, so it never drags the health score (it's appended as an
|
|
151
189
|
// advisory note below). The score ranks what's BROKEN.
|
|
@@ -239,11 +277,11 @@ function formatLeaderboard(scores) {
|
|
|
239
277
|
const issue = s.issues.length > 0 ? ` — ${s.issues.join("; ")}` : "";
|
|
240
278
|
out.push(` ${rank} ${score} ${s.grade} ${s.name}${issue}`);
|
|
241
279
|
});
|
|
242
|
-
out.push("", "Structural health only (no model). Weights: missing hook -15, no-description
|
|
280
|
+
out.push("", "Structural health only (no model). Weights: lethal-trifecta unit -20, missing", "hook -15, no-description skill -10, broken intra-plugin ref -8, dead tool/MCP", "ref -8. Inherit-all subagents and untested surfaces are advisory — shown, not", "scored.");
|
|
243
281
|
return out.join("\n");
|
|
244
282
|
}
|
|
245
|
-
const LEADERBOARD_METHOD = "_Structural health only (deterministic, no model):
|
|
246
|
-
"no-description skill −10, broken intra-plugin / dead-tool ref −8. " +
|
|
283
|
+
const LEADERBOARD_METHOD = "_Structural health only (deterministic, no model): lethal-trifecta unit −20, " +
|
|
284
|
+
"missing hook −15, no-description skill −10, broken intra-plugin / dead-tool ref −8. " +
|
|
247
285
|
"Inherit-all subagents and untested surfaces are advisory (shown, not " +
|
|
248
286
|
"scored). Behavioural columns (trigger-rate, collisions, egress) stack on top._";
|
|
249
287
|
/**
|
package/dist/scan.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ import { type DescriptionOverlap } from "./core/description-overlap.js";
|
|
|
20
20
|
import { type McpToolIssue } from "./core/mcp-tool.js";
|
|
21
21
|
import { type McpContractToolError } from "./core/mcp.js";
|
|
22
22
|
import { type McpHookIssue } from "./core/mcp-hook.js";
|
|
23
|
+
import { type TrifectaFinding } from "./core/lethal-trifecta.js";
|
|
24
|
+
import { type SkillResourceFinding } from "./core/skill-resources.js";
|
|
25
|
+
import { type SkillFenceFinding } from "./core/skill-missing-fence.js";
|
|
26
|
+
import { type PluginLayoutFinding } from "./core/plugin-dir-layout.js";
|
|
27
|
+
import { type DelegationTrifectaFinding } from "./core/delegation-trifecta.js";
|
|
28
|
+
import { type HookBlockFinding } from "./core/hook-block-ineffective.js";
|
|
29
|
+
import { type HookMatcherFinding } from "./core/hook-matcher.js";
|
|
23
30
|
import { type PurityLevel, type EffectSurface } from "./core/effects.js";
|
|
24
31
|
/** A named writing system. The label `unexpectedScript` reports + the config's expectation parse into this. */
|
|
25
32
|
export type Script = "Latin" | "Cyrillic" | "Han" | "Japanese" | "Korean" | "Arabic" | "Hebrew" | "Greek" | "Devanagari" | "Thai";
|
|
@@ -43,6 +50,27 @@ export interface ScanSkill {
|
|
|
43
50
|
* `audit` trigger tier / `measureTriggerRate`.
|
|
44
51
|
*/
|
|
45
52
|
readonly descriptionScript: Script | null;
|
|
53
|
+
/**
|
|
54
|
+
* SKILL.md body references to a bundled file (`scripts/`/`references/`/`assets/`
|
|
55
|
+
* or a relative markdown link with an extension) that don't resolve on disk
|
|
56
|
+
* under the skill dir — the agent reads the instruction and gets nothing.
|
|
57
|
+
* Computed by `skillResourceIssues()` (one detector, no drift).
|
|
58
|
+
*/
|
|
59
|
+
readonly resourceIssues: readonly SkillResourceFinding[];
|
|
60
|
+
/**
|
|
61
|
+
* Lethal-trifecta finding when a MODEL-INVOCABLE skill's declared `allowed-tools`
|
|
62
|
+
* hold all three legs (read-private + ingest-untrusted + exfiltrate), else null.
|
|
63
|
+
* A user-invoked skill is excluded (it can't be selected by attacker content).
|
|
64
|
+
* Computed by `lethalTrifectaIssues()` (one detector, no drift).
|
|
65
|
+
*/
|
|
66
|
+
readonly trifecta: TrifectaFinding | null;
|
|
67
|
+
/**
|
|
68
|
+
* Set when the SKILL.md opens with frontmatter-looking keys (`name:`, …) but
|
|
69
|
+
* has NO opening `---` fence, so the whole file loads as body — no name, no
|
|
70
|
+
* description, no trigger (the skill is invisible). Computed by
|
|
71
|
+
* `skillMissingFence()` (one detector, no drift).
|
|
72
|
+
*/
|
|
73
|
+
readonly fenceIssue: SkillFenceFinding | null;
|
|
46
74
|
}
|
|
47
75
|
export interface ScanAgent {
|
|
48
76
|
readonly name: string;
|
|
@@ -68,6 +96,37 @@ export interface ScanAgent {
|
|
|
68
96
|
* unknown-effect (MCP or unrecognized) tool names in the declared contract.
|
|
69
97
|
*/
|
|
70
98
|
readonly effectBuckets: Pick<EffectSurface, "readOnly" | "sideEffecting" | "unknown">;
|
|
99
|
+
/**
|
|
100
|
+
* Lethal-trifecta finding when the subagent's declared tools hold all three legs
|
|
101
|
+
* (read-private + ingest-untrusted + exfiltrate), else null. An inherits-all
|
|
102
|
+
* agent (no `tools:` line) is the "advisory" case. Computed by
|
|
103
|
+
* `lethalTrifectaIssues()` (one detector, no drift).
|
|
104
|
+
*/
|
|
105
|
+
readonly trifecta: TrifectaFinding | null;
|
|
106
|
+
}
|
|
107
|
+
/** A lethal-trifecta finding tagged with the surface (subagent/skill) that holds it. */
|
|
108
|
+
export interface ScanTrifectaFinding {
|
|
109
|
+
readonly path: string;
|
|
110
|
+
readonly kind: "subagent" | "skill";
|
|
111
|
+
readonly name: string;
|
|
112
|
+
readonly finding: TrifectaFinding;
|
|
113
|
+
}
|
|
114
|
+
/** A SKILL.md body resource reference that doesn't resolve, tagged with the skill path. */
|
|
115
|
+
export interface ScanSkillResourceFinding {
|
|
116
|
+
readonly path: string;
|
|
117
|
+
readonly name: string;
|
|
118
|
+
readonly finding: SkillResourceFinding;
|
|
119
|
+
}
|
|
120
|
+
/** A missing-frontmatter-fence finding tagged with the skill path. */
|
|
121
|
+
export interface ScanSkillFenceFinding {
|
|
122
|
+
readonly path: string;
|
|
123
|
+
readonly name: string;
|
|
124
|
+
readonly finding: SkillFenceFinding;
|
|
125
|
+
}
|
|
126
|
+
/** A delegation-trifecta finding tagged with the subagent path that holds it. */
|
|
127
|
+
export interface ScanDelegationFinding {
|
|
128
|
+
readonly path: string;
|
|
129
|
+
readonly finding: DelegationTrifectaFinding;
|
|
71
130
|
}
|
|
72
131
|
/** A skill/agent whose frontmatter is missing a required field (name / description). */
|
|
73
132
|
export interface FrontmatterIssue {
|
|
@@ -162,6 +221,53 @@ export interface ScanReport {
|
|
|
162
221
|
readonly mcpHookIssues: readonly McpHookIssue[];
|
|
163
222
|
/** Pairs of model-invocable skills whose descriptions are near-identical (precision collision). */
|
|
164
223
|
readonly descriptionOverlaps: readonly DescriptionOverlap[];
|
|
224
|
+
/**
|
|
225
|
+
* Lethal-trifecta findings across subagents + model-invocable skills — a unit
|
|
226
|
+
* holding all three legs (read-private + ingest-untrusted + exfiltrate). Each
|
|
227
|
+
* carries the surface path + kind for reporting/annotations. Shared by `scan`
|
|
228
|
+
* (the report) and the `lethal-trifecta` lint rule (one detector, no drift).
|
|
229
|
+
*/
|
|
230
|
+
readonly trifectaFindings: readonly ScanTrifectaFinding[];
|
|
231
|
+
/**
|
|
232
|
+
* SKILL.md body references to a bundled resource that doesn't resolve on disk,
|
|
233
|
+
* across all skills — each carries the skill path for reporting/annotations.
|
|
234
|
+
* Shared by `scan` and the `skill-resource-resolves` lint rule (one detector, no
|
|
235
|
+
* drift).
|
|
236
|
+
*/
|
|
237
|
+
readonly skillResourceIssues: readonly ScanSkillResourceFinding[];
|
|
238
|
+
/**
|
|
239
|
+
* Skills whose frontmatter-looking opening has NO `---` fence, so they load as
|
|
240
|
+
* pure body (invisible — no name/description/trigger). Shared by `scan` and the
|
|
241
|
+
* `skill-missing-fence` lint rule (one detector, no drift).
|
|
242
|
+
*/
|
|
243
|
+
readonly skillFenceIssues: readonly ScanSkillFenceFinding[];
|
|
244
|
+
/**
|
|
245
|
+
* Functional surface dirs (skills/agents/commands) nested INSIDE the manifest
|
|
246
|
+
* dir (`.claude-plugin/`) where the harness can't see them. Shared by `scan`
|
|
247
|
+
* and the `plugin-dir-layout` lint rule (one detector, no drift).
|
|
248
|
+
*/
|
|
249
|
+
readonly pluginLayoutIssues: readonly PluginLayoutFinding[];
|
|
250
|
+
/**
|
|
251
|
+
* Lethal trifectas that EMERGE across a delegation edge — a subagent whose
|
|
252
|
+
* effective (own ∪ delegated-to) capability holds all three legs though no
|
|
253
|
+
* single unit does. Shared by `scan` and the `delegation-trifecta` lint rule
|
|
254
|
+
* (one detector, no drift).
|
|
255
|
+
*/
|
|
256
|
+
readonly delegationTrifecta: readonly ScanDelegationFinding[];
|
|
257
|
+
/**
|
|
258
|
+
* Hooks that LOOK like they block but silently don't — a block decision on a
|
|
259
|
+
* non-blocking event, or the legacy `decision` field on a permission-gated
|
|
260
|
+
* event (#19009, the #1 verified hook pain). Shared by `scan` and the
|
|
261
|
+
* `hook-block-ineffective` lint rule (one detector, no drift). Empty when the
|
|
262
|
+
* dialect doesn't declare its blocking-event semantics.
|
|
263
|
+
*/
|
|
264
|
+
readonly hookBlockFindings: readonly HookBlockFinding[];
|
|
265
|
+
/**
|
|
266
|
+
* Hook `matcher` strings that silently never fire — a tool-name typo or a
|
|
267
|
+
* malformed/undeclared MCP form. Shared by `scan` and the `hook-matcher` lint
|
|
268
|
+
* rule (one detector, no drift).
|
|
269
|
+
*/
|
|
270
|
+
readonly hookMatcherFindings: readonly HookMatcherFinding[];
|
|
165
271
|
/** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
|
|
166
272
|
readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
|
|
167
273
|
readonly warnings: readonly string[];
|