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/audit-score.d.ts
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A single structural-health number (the leaderboard's `scoreReport`) ranks
|
|
5
5
|
* plugins, but it hides WHERE a harness is weak. This buckets the SAME
|
|
6
|
-
* deterministic findings into
|
|
7
|
-
* Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown
|
|
8
|
-
* headline `overall` = `100 − Σ(all graded penalties)` (the SAME
|
|
9
|
-
* the leaderboard's single health number, via the shared
|
|
10
|
-
* — so the two surfaces never disagree). Same detectors,
|
|
11
|
-
* (one-detector-no-drift); all deterministic, no execution.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* deterministic findings into five categories — Truthfulness, Triggering,
|
|
7
|
+
* Structure, Safety, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown
|
|
8
|
+
* beneath one headline `overall` = `100 − Σ(all graded penalties)` (the SAME
|
|
9
|
+
* summed model as the leaderboard's single health number, via the shared
|
|
10
|
+
* `computeIntegrityScore` — so the two surfaces never disagree). Same detectors,
|
|
11
|
+
* no re-detection (one-detector-no-drift); all deterministic, no execution.
|
|
12
|
+
*
|
|
13
|
+
* SAFETY is fed by the STATIC lethal-trifecta capability check
|
|
14
|
+
* (`lethalTrifectaIssues` → `report.trifectaFindings`): a unit holding all three
|
|
15
|
+
* capability legs is a prompt-injection exfil path detectable from the tool-SET
|
|
16
|
+
* alone — nothing executes, so it sidesteps the confinement blocker. A `"hard"`
|
|
17
|
+
* (explicit all-three) finding is GRADED into the overall; a `"advisory"`
|
|
18
|
+
* (inherits-all) finding is SHOWN in the ring but not graded. NB the EXECUTING
|
|
19
|
+
* "do your hooks actually block?" disaster-battery is STILL not an `audit` ring:
|
|
20
|
+
* running arbitrary hooks safely needs cross-platform confinement that isn't
|
|
21
|
+
* shipped yet, so the battery lives in the `vigiles/testing` API via
|
|
22
|
+
* `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.
|
|
16
23
|
*
|
|
17
24
|
* A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
|
|
18
25
|
* overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
|
|
19
26
|
*/
|
|
20
27
|
import { type PluginScore } from "./leaderboard.js";
|
|
21
28
|
import type { ScanReport } from "./scan.js";
|
|
22
|
-
export type CategoryKey = "Truthfulness" | "Triggering" | "Structure" | "Tested";
|
|
29
|
+
export type CategoryKey = "Truthfulness" | "Triggering" | "Structure" | "Safety" | "Tested";
|
|
23
30
|
export interface CategoryScore {
|
|
24
31
|
readonly key: CategoryKey;
|
|
25
32
|
/** 0–100, or `null` when the category isn't assessable (n/a — excluded from overall). */
|
|
@@ -52,7 +59,7 @@ export interface AuditScore {
|
|
|
52
59
|
readonly empty: boolean;
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
|
-
* Bucket a scan report into the
|
|
62
|
+
* Bucket a scan report into the five deterministic Lighthouse categories as a
|
|
56
63
|
* DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
|
|
57
64
|
* penalties)` (the shared summed model — NOT the average of the rings — so it
|
|
58
65
|
* equals the leaderboard's single health number). The advisory Tested ring and
|
package/dist/audit-score.js
CHANGED
|
@@ -7,16 +7,23 @@ exports.formatAuditScore = formatAuditScore;
|
|
|
7
7
|
*
|
|
8
8
|
* A single structural-health number (the leaderboard's `scoreReport`) ranks
|
|
9
9
|
* plugins, but it hides WHERE a harness is weak. This buckets the SAME
|
|
10
|
-
* deterministic findings into
|
|
11
|
-
* Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown
|
|
12
|
-
* headline `overall` = `100 − Σ(all graded penalties)` (the SAME
|
|
13
|
-
* the leaderboard's single health number, via the shared
|
|
14
|
-
* — so the two surfaces never disagree). Same detectors,
|
|
15
|
-
* (one-detector-no-drift); all deterministic, no execution.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
10
|
+
* deterministic findings into five categories — Truthfulness, Triggering,
|
|
11
|
+
* Structure, Safety, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown
|
|
12
|
+
* beneath one headline `overall` = `100 − Σ(all graded penalties)` (the SAME
|
|
13
|
+
* summed model as the leaderboard's single health number, via the shared
|
|
14
|
+
* `computeIntegrityScore` — so the two surfaces never disagree). Same detectors,
|
|
15
|
+
* no re-detection (one-detector-no-drift); all deterministic, no execution.
|
|
16
|
+
*
|
|
17
|
+
* SAFETY is fed by the STATIC lethal-trifecta capability check
|
|
18
|
+
* (`lethalTrifectaIssues` → `report.trifectaFindings`): a unit holding all three
|
|
19
|
+
* capability legs is a prompt-injection exfil path detectable from the tool-SET
|
|
20
|
+
* alone — nothing executes, so it sidesteps the confinement blocker. A `"hard"`
|
|
21
|
+
* (explicit all-three) finding is GRADED into the overall; a `"advisory"`
|
|
22
|
+
* (inherits-all) finding is SHOWN in the ring but not graded. NB the EXECUTING
|
|
23
|
+
* "do your hooks actually block?" disaster-battery is STILL not an `audit` ring:
|
|
24
|
+
* running arbitrary hooks safely needs cross-platform confinement that isn't
|
|
25
|
+
* shipped yet, so the battery lives in the `vigiles/testing` API via
|
|
26
|
+
* `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.
|
|
20
27
|
*
|
|
21
28
|
* A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
|
|
22
29
|
* overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
|
|
@@ -138,6 +145,51 @@ function structure(r) {
|
|
|
138
145
|
findings: [...findings, ...advisory],
|
|
139
146
|
};
|
|
140
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* SAFETY — fed by the STATIC lethal-trifecta check (`report.trifectaFindings`).
|
|
150
|
+
* A `"hard"` finding (an explicit contract naming all three capability legs) is a
|
|
151
|
+
* declared prompt-injection exfil path and is GRADED (`W_TRIFECTA` each, the same
|
|
152
|
+
* weight `reportDeductions` sums into the overall, so the ring and the headline
|
|
153
|
+
* agree). A `"advisory"` finding (inherits-all) is SHOWN in the ring's findings
|
|
154
|
+
* but NOT graded — aligned with the inherits-all-is-advisory stance.
|
|
155
|
+
*
|
|
156
|
+
* Scores `null` (n/a, excluded from the overall) when there's NO tool-bearing
|
|
157
|
+
* surface to assess at all — no subagents AND no model-invocable skills. A
|
|
158
|
+
* user-invoked skill carries no model-driven trifecta risk, so it doesn't count
|
|
159
|
+
* as an assessable surface. When there ARE assessable surfaces but no trifecta,
|
|
160
|
+
* the ring is a clean 100.
|
|
161
|
+
*/
|
|
162
|
+
function safety(r) {
|
|
163
|
+
const modelInvocableSkills = r.skills.filter((s) => !s.userInvoked).length;
|
|
164
|
+
const assessable = r.agents.length + modelInvocableSkills;
|
|
165
|
+
if (assessable === 0) {
|
|
166
|
+
return {
|
|
167
|
+
key: "Safety",
|
|
168
|
+
score: null,
|
|
169
|
+
weight: 1,
|
|
170
|
+
findings: ["no tool-bearing surface to assess"],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const hard = r.trifectaFindings.filter((f) => f.finding.severity === "hard");
|
|
174
|
+
const { score, findings } = scoreFrom([
|
|
175
|
+
{
|
|
176
|
+
n: hard.length,
|
|
177
|
+
weight: leaderboard_js_1.W_TRIFECTA,
|
|
178
|
+
label: "unit(s) holding all three lethal-trifecta legs (prompt-injection exfil path)",
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
// inherits-all trifecta findings are ADVISORY: surfaced as a maximal-blast-radius
|
|
182
|
+
// note but never graded (mirrors the Structure inherits-all advisory).
|
|
183
|
+
const advisory = r.trifectaFindings
|
|
184
|
+
.filter((f) => f.finding.severity === "advisory")
|
|
185
|
+
.map((f) => `${f.name} inherits all tools — maximal trifecta blast radius (advisory)`);
|
|
186
|
+
return {
|
|
187
|
+
key: "Safety",
|
|
188
|
+
score,
|
|
189
|
+
weight: 1,
|
|
190
|
+
findings: [...findings, ...advisory],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
141
193
|
function tested(r) {
|
|
142
194
|
const { score, findings } = scoreFrom([
|
|
143
195
|
{ n: r.untested, weight: W_UNTESTED, label: "untested surface(s)" },
|
|
@@ -157,7 +209,7 @@ function isEmptyAudit(r) {
|
|
|
157
209
|
return (0, leaderboard_js_1.isEmptyMachine)(r) && !r.instructions;
|
|
158
210
|
}
|
|
159
211
|
/**
|
|
160
|
-
* Bucket a scan report into the
|
|
212
|
+
* Bucket a scan report into the five deterministic Lighthouse categories as a
|
|
161
213
|
* DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
|
|
162
214
|
* penalties)` (the shared summed model — NOT the average of the rings — so it
|
|
163
215
|
* equals the leaderboard's single health number). The advisory Tested ring and
|
|
@@ -169,6 +221,7 @@ function auditScore(report) {
|
|
|
169
221
|
"Truthfulness",
|
|
170
222
|
"Triggering",
|
|
171
223
|
"Structure",
|
|
224
|
+
"Safety",
|
|
172
225
|
"Tested",
|
|
173
226
|
];
|
|
174
227
|
return {
|
|
@@ -187,6 +240,7 @@ function auditScore(report) {
|
|
|
187
240
|
truthfulness(report),
|
|
188
241
|
triggering(report),
|
|
189
242
|
structure(report),
|
|
243
|
+
safety(report),
|
|
190
244
|
tested(report),
|
|
191
245
|
];
|
|
192
246
|
// The headline is the SUMMED model (the shared integrity score), NOT the average
|
package/dist/cli.js
CHANGED
|
@@ -678,6 +678,13 @@ function lintExitCode(report) {
|
|
|
678
678
|
report.frontmatterValidErrors > 0 ||
|
|
679
679
|
report.mcpHookErrors > 0 ||
|
|
680
680
|
report.preferCompiledHookErrors > 0 ||
|
|
681
|
+
report.lethalTrifectaErrors > 0 ||
|
|
682
|
+
report.skillResourceErrors > 0 ||
|
|
683
|
+
report.skillFenceErrors > 0 ||
|
|
684
|
+
report.pluginLayoutErrors > 0 ||
|
|
685
|
+
report.delegationTrifectaErrors > 0 ||
|
|
686
|
+
report.hookBlockErrors > 0 ||
|
|
687
|
+
report.hookMatcherErrors > 0 ||
|
|
681
688
|
report.symbolRefErrors > 0 ||
|
|
682
689
|
report.mcpRefErrors > 0)
|
|
683
690
|
return 2;
|
|
@@ -1028,6 +1035,29 @@ async function runLint(restArgs, flags, config) {
|
|
|
1028
1035
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1029
1036
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1030
1037
|
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
1038
|
+
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1039
|
+
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1040
|
+
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1041
|
+
const lethalTrifecta = checkLethalTrifecta(config, silent, adapter);
|
|
1042
|
+
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1043
|
+
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1044
|
+
const skillResources = checkSkillResourceResolves(config, silent, adapter);
|
|
1045
|
+
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1046
|
+
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1047
|
+
const skillFence = checkSkillMissingFence(config, silent, adapter);
|
|
1048
|
+
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1049
|
+
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1050
|
+
const pluginLayout = checkPluginDirLayout(config, silent, adapter);
|
|
1051
|
+
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1052
|
+
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1053
|
+
const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter);
|
|
1054
|
+
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1055
|
+
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1056
|
+
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1057
|
+
const hookBlock = checkHookBlockIneffective(config, silent, adapter);
|
|
1058
|
+
// 7u. Hook-matcher — a hook `matcher` that never fires (tool-name typo, or a
|
|
1059
|
+
// malformed/undeclared MCP form).
|
|
1060
|
+
const hookMatcher = checkHookMatcher(config, silent, adapter);
|
|
1031
1061
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
1032
1062
|
// is to validate every ref; illustrative blocks opt out via
|
|
1033
1063
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1095,6 +1125,20 @@ async function runLint(restArgs, flags, config) {
|
|
|
1095
1125
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1096
1126
|
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1097
1127
|
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1128
|
+
lethalTrifectaIssues: lethalTrifecta.issues,
|
|
1129
|
+
lethalTrifectaErrors: lethalTrifecta.errors,
|
|
1130
|
+
skillResourceIssues: skillResources.issues,
|
|
1131
|
+
skillResourceErrors: skillResources.errors,
|
|
1132
|
+
skillFenceIssues: skillFence.issues,
|
|
1133
|
+
skillFenceErrors: skillFence.errors,
|
|
1134
|
+
pluginLayoutIssues: pluginLayout.issues,
|
|
1135
|
+
pluginLayoutErrors: pluginLayout.errors,
|
|
1136
|
+
delegationTrifectaIssues: delegationTrifecta.issues,
|
|
1137
|
+
delegationTrifectaErrors: delegationTrifecta.errors,
|
|
1138
|
+
hookBlockIssues: hookBlock.issues,
|
|
1139
|
+
hookBlockErrors: hookBlock.errors,
|
|
1140
|
+
hookMatcherIssues: hookMatcher.issues,
|
|
1141
|
+
hookMatcherErrors: hookMatcher.errors,
|
|
1098
1142
|
docRefErrors: docRefReport.errors.length,
|
|
1099
1143
|
symbolRefErrors,
|
|
1100
1144
|
mcpRefErrors,
|
|
@@ -2673,6 +2717,211 @@ function checkDescriptionOverlap(config, silent, adapter) {
|
|
|
2673
2717
|
}
|
|
2674
2718
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2675
2719
|
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Apply the `lethal-trifecta` rule: a unit (subagent / model-invocable skill)
|
|
2722
|
+
* whose declared tools hold all three legs (read-private + ingest-untrusted +
|
|
2723
|
+
* exfiltrate) is a prompt-injection exfil path (Meta's Rule of Two). Reuses
|
|
2724
|
+
* `scanPlugin`'s `trifectaFindings` (a capability SET-intersection, one detector,
|
|
2725
|
+
* no drift). Warning by default; "error" gates CI. Surfaces across BOTH subagents
|
|
2726
|
+
* and skills, so it is NOT gated on the `subagents` capability — a skill-only
|
|
2727
|
+
* harness still has the surface.
|
|
2728
|
+
*/
|
|
2729
|
+
function checkLethalTrifecta(config, silent, adapter) {
|
|
2730
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
|
|
2731
|
+
if (!sev)
|
|
2732
|
+
return { issues: 0, errors: 0 };
|
|
2733
|
+
let found;
|
|
2734
|
+
try {
|
|
2735
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).trifectaFindings;
|
|
2736
|
+
}
|
|
2737
|
+
catch {
|
|
2738
|
+
return { issues: 0, errors: 0 };
|
|
2739
|
+
}
|
|
2740
|
+
if (found.length > 0 && !silent) {
|
|
2741
|
+
console.log("\nLethal-trifecta check:\n");
|
|
2742
|
+
for (const t of found) {
|
|
2743
|
+
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
2744
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${t.path}: ${msg}`);
|
|
2745
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, t.path);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Apply the `skill-resource-resolves` rule: a SKILL.md body referencing a bundled
|
|
2752
|
+
* file (`scripts/`/`references/`/`assets/` or a relative markdown link with an
|
|
2753
|
+
* extension) that doesn't exist on disk — the agent reads the instruction and gets
|
|
2754
|
+
* nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
|
|
2755
|
+
* one detector, no drift). Warning by default; "error" gates CI.
|
|
2756
|
+
*/
|
|
2757
|
+
function checkSkillResourceResolves(config, silent, adapter) {
|
|
2758
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
2759
|
+
if (!sev)
|
|
2760
|
+
return { issues: 0, errors: 0 };
|
|
2761
|
+
let found;
|
|
2762
|
+
try {
|
|
2763
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillResourceIssues;
|
|
2764
|
+
}
|
|
2765
|
+
catch {
|
|
2766
|
+
return { issues: 0, errors: 0 };
|
|
2767
|
+
}
|
|
2768
|
+
if (found.length > 0 && !silent) {
|
|
2769
|
+
console.log("\nSkill-resource check:\n");
|
|
2770
|
+
for (const s of found) {
|
|
2771
|
+
const msg = `${s.name}: bundled resource "${s.finding.ref}" (line ${String(s.finding.line)}) is referenced but missing — the agent reads the instruction and gets nothing.`;
|
|
2772
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
2773
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, s.path);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Apply the `skill-missing-fence` rule: a SKILL.md that opens with
|
|
2780
|
+
* frontmatter-looking keys (`name:`/`description:`) but no `---` fence loads as
|
|
2781
|
+
* pure body — no name, no description, no trigger (the skill is invisible).
|
|
2782
|
+
* Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
|
|
2783
|
+
* default; "error" gates CI.
|
|
2784
|
+
*/
|
|
2785
|
+
function checkSkillMissingFence(config, silent, adapter) {
|
|
2786
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
|
|
2787
|
+
if (!sev)
|
|
2788
|
+
return { issues: 0, errors: 0 };
|
|
2789
|
+
let found;
|
|
2790
|
+
try {
|
|
2791
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillFenceIssues;
|
|
2792
|
+
}
|
|
2793
|
+
catch {
|
|
2794
|
+
return { issues: 0, errors: 0 };
|
|
2795
|
+
}
|
|
2796
|
+
if (found.length > 0 && !silent) {
|
|
2797
|
+
console.log("\nSkill-missing-fence check:\n");
|
|
2798
|
+
for (const s of found) {
|
|
2799
|
+
const msg = `${s.name}: ${s.finding.message}`;
|
|
2800
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
2801
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, s.path);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Apply the `plugin-dir-layout` rule: functional surface dirs (skills/agents/
|
|
2808
|
+
* commands) nested inside the `.claude-plugin/` manifest dir where the harness
|
|
2809
|
+
* can't see them (the #1 plugin-author mistake). Reuses `scanPlugin`'s
|
|
2810
|
+
* `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
|
|
2811
|
+
* gates CI.
|
|
2812
|
+
*/
|
|
2813
|
+
function checkPluginDirLayout(config, silent, adapter) {
|
|
2814
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
|
|
2815
|
+
if (!sev)
|
|
2816
|
+
return { issues: 0, errors: 0 };
|
|
2817
|
+
let found;
|
|
2818
|
+
try {
|
|
2819
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).pluginLayoutIssues;
|
|
2820
|
+
}
|
|
2821
|
+
catch {
|
|
2822
|
+
return { issues: 0, errors: 0 };
|
|
2823
|
+
}
|
|
2824
|
+
if (found.length > 0 && !silent) {
|
|
2825
|
+
console.log("\nPlugin-dir-layout check:\n");
|
|
2826
|
+
for (const p of found) {
|
|
2827
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${p.message}`);
|
|
2828
|
+
ghAnnotate(sev === "error" ? "error" : "warning", p.message);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Apply the `delegation-trifecta` rule: a lethal trifecta that EMERGES across a
|
|
2835
|
+
* delegation edge — a subagent whose effective (own ∪ delegated-to) capability
|
|
2836
|
+
* holds all three legs though no single unit does. Reuses `scanPlugin`'s
|
|
2837
|
+
* `delegationTrifecta` (one detector, no drift). Warning by default; "error"
|
|
2838
|
+
* gates CI. Surfaces across the subagent graph, so it is NOT gated on a
|
|
2839
|
+
* capability the way a surface-specific rule is.
|
|
2840
|
+
*/
|
|
2841
|
+
function checkDelegationTrifecta(config, silent, adapter) {
|
|
2842
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
|
|
2843
|
+
if (!sev)
|
|
2844
|
+
return { issues: 0, errors: 0 };
|
|
2845
|
+
let found;
|
|
2846
|
+
try {
|
|
2847
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).delegationTrifecta;
|
|
2848
|
+
}
|
|
2849
|
+
catch {
|
|
2850
|
+
return { issues: 0, errors: 0 };
|
|
2851
|
+
}
|
|
2852
|
+
if (found.length > 0 && !silent) {
|
|
2853
|
+
console.log("\nDelegation-trifecta check:\n");
|
|
2854
|
+
for (const d of found) {
|
|
2855
|
+
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
2856
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${d.path}: ${msg}`);
|
|
2857
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, d.path);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Apply the `hook-block-ineffective` rule: a hook that LOOKS like it blocks but
|
|
2864
|
+
* silently doesn't — a block decision (`exit 2` / `decision` / `permissionDecision`)
|
|
2865
|
+
* on a non-blocking event, or the legacy top-level `decision` field on a
|
|
2866
|
+
* permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
|
|
2867
|
+
* `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
|
|
2868
|
+
*/
|
|
2869
|
+
function checkHookBlockIneffective(config, silent, adapter) {
|
|
2870
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
|
|
2871
|
+
if (!sev)
|
|
2872
|
+
return { issues: 0, errors: 0 };
|
|
2873
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2874
|
+
reportNotApplicable("Hook-block check", "shell hooks", adapter, silent);
|
|
2875
|
+
return { issues: 0, errors: 0 };
|
|
2876
|
+
}
|
|
2877
|
+
let found;
|
|
2878
|
+
try {
|
|
2879
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookBlockFindings;
|
|
2880
|
+
}
|
|
2881
|
+
catch {
|
|
2882
|
+
return { issues: 0, errors: 0 };
|
|
2883
|
+
}
|
|
2884
|
+
if (found.length > 0 && !silent) {
|
|
2885
|
+
console.log("\nHook-block check:\n");
|
|
2886
|
+
for (const h of found) {
|
|
2887
|
+
const where = h.scriptPath ?? "(inline)";
|
|
2888
|
+
const msg = `[${h.event}] ${where}: ${h.message}`;
|
|
2889
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${msg}`);
|
|
2890
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, h.scriptPath ?? undefined);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Apply the `hook-matcher` rule: a hook `matcher` string that silently never
|
|
2897
|
+
* fires — a tool-name typo (`bash`→`Bash`) or a malformed/undeclared MCP form.
|
|
2898
|
+
* Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
|
|
2899
|
+
* by default; "error" gates CI.
|
|
2900
|
+
*/
|
|
2901
|
+
function checkHookMatcher(config, silent, adapter) {
|
|
2902
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
|
|
2903
|
+
if (!sev)
|
|
2904
|
+
return { issues: 0, errors: 0 };
|
|
2905
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2906
|
+
reportNotApplicable("Hook-matcher check", "shell hooks", adapter, silent);
|
|
2907
|
+
return { issues: 0, errors: 0 };
|
|
2908
|
+
}
|
|
2909
|
+
let found;
|
|
2910
|
+
try {
|
|
2911
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookMatcherFindings;
|
|
2912
|
+
}
|
|
2913
|
+
catch {
|
|
2914
|
+
return { issues: 0, errors: 0 };
|
|
2915
|
+
}
|
|
2916
|
+
if (found.length > 0 && !silent) {
|
|
2917
|
+
console.log("\nHook-matcher check:\n");
|
|
2918
|
+
for (const m of found) {
|
|
2919
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${m.message}`);
|
|
2920
|
+
ghAnnotate(sev === "error" ? "error" : "warning", m.message);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2924
|
+
}
|
|
2676
2925
|
/**
|
|
2677
2926
|
* Apply the `mcp-hook-target-resolves` rule: a `type: "mcp_tool"` hook action
|
|
2678
2927
|
* that's incomplete (no `server`/`tool`) or targets a server the plugin doesn't
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Directory-scoped guidance for working in `src/core/` (the harness-agnostic
|
|
5
|
+
* detectors + domain).
|
|
6
|
+
*
|
|
7
|
+
* The full project rule set is the ROOT `CLAUDE.md` (compiled from
|
|
8
|
+
* `CLAUDE.md.spec.ts`). This nested spec adds only the discipline that belongs
|
|
9
|
+
* next to the detectors themselves — Claude Code loads it as directory memory
|
|
10
|
+
* whenever you work in `src/core/`. Source of truth; `src/core/CLAUDE.md` is a
|
|
11
|
+
* compiled build artifact (`vigiles compile`).
|
|
12
|
+
*/
|
|
13
|
+
const spec_js_1 = require("./spec.js");
|
|
14
|
+
exports.default = (0, spec_js_1.claude)({
|
|
15
|
+
sections: {
|
|
16
|
+
scope: `Working in \`src/core/\`? This is the harness-AGNOSTIC domain (spec, compile, linters, the lint/audit detectors). The root \`CLAUDE.md\` holds the full positioning + rule set — read it first. Two invariants live closest to this code: the core must not import an adapter (\`core ⊄ adapter\`, eslint-enforced) and must not hard-code a Claude Code literal (read it from the injected layout/dialect). This file adds the rule for ADDING or CHANGING a detector.`,
|
|
17
|
+
},
|
|
18
|
+
keyFiles: {
|
|
19
|
+
"src/core/rule-meta.ts": "The RuleMeta registry — every rule's decidability bucket + severity + detector, the single source the detector-meta rule enforces.",
|
|
20
|
+
"src/core/types.ts": "RulesConfig — the rule-name keys the registry is keyed on.",
|
|
21
|
+
},
|
|
22
|
+
rules: {
|
|
23
|
+
"detector-meta": (0, spec_js_1.guidance)("A deterministic DETECTOR here is one half of a RULE — and a rule is not done until it is DECLARED. Three things move together (sibling of one-detector-no-drift + rules-docs-in-sync): (1) the pure detector function (shared by `lint` AND `audit`, never reimplemented per surface; read the layout/dialect, never a CC literal); (2) its entry in `src/core/rule-meta.ts` — the `Record<RuleName, RuleMeta>` won't typecheck without it — declaring its DECIDABILITY BUCKET (structural-closed = a type could prevent it / external-decidable = needs the world, error-capable / heuristic-behavioral = warn-or-measure-only), surface, defaultSeverity, the detector name, and any upstreamPrevention; (3) its `docs/rules/<name>.md` (the coverage test binds the registry to the docs by an EXACT set match, so a missing meta or doc fails CI). The bucket is the CEILING, not a preference — a heuristic proxy may NEVER default to `error` (it cries wolf); a structural/external fact MAY, once proven FP-safe. Before writing a new detector, CLASSIFY the defect into a bucket — that decides whether it can ever gate. The full model + the prose behind the buckets is the root `lint-rule-calibration` rule and `research/enforcement-model.md`."),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=CLAUDE.md.spec.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DELEGATION-TRIFECTA — the lethal trifecta SPLIT across a delegation edge.
|
|
3
|
+
*
|
|
4
|
+
* The per-unit {@link lethalTrifectaIssues} (`./lethal-trifecta.ts`) catches a
|
|
5
|
+
* single subagent / skill that holds all THREE capability legs at once. But the
|
|
6
|
+
* trifecta can EMERGE across a delegation (or inheritance) edge: a parent that
|
|
7
|
+
* can only read private data (leg A) delegates to a child that can ingest
|
|
8
|
+
* untrusted content AND exfiltrate (legs B+C). NEITHER unit trips the per-unit
|
|
9
|
+
* check, yet the CHAIN — the parent plus everything it can reach — forms the full
|
|
10
|
+
* trifecta. A prompt injection in the child's untrusted input can pivot back
|
|
11
|
+
* through the delegation and leak the parent's private data.
|
|
12
|
+
*
|
|
13
|
+
* This is CAPABILITY-DIFF ACROSS THE DELEGATION TREE: the EFFECTIVE (combined)
|
|
14
|
+
* capability of a unit is the union of its own tools plus the tools of every unit
|
|
15
|
+
* reachable through `delegatesTo`. We classify that effective set and flag a unit
|
|
16
|
+
* whose effective set is a full trifecta while its OWN set is not.
|
|
17
|
+
*
|
|
18
|
+
* NO DOUBLE-REPORT (one-detector-no-drift / don't-cry-wolf): a unit whose OWN
|
|
19
|
+
* tools already form a full trifecta is SKIPPED here — {@link lethalTrifectaIssues}
|
|
20
|
+
* owns it. This detector reports ONLY the EMERGENT case the per-unit check can't
|
|
21
|
+
* see.
|
|
22
|
+
*
|
|
23
|
+
* HIGH-PRECISION (FP-safe): if the effective set contains a wildcard (inherits-all)
|
|
24
|
+
* the unit reaches everything and would always "trifecta" — that maximal-blast-
|
|
25
|
+
* radius case is the per-unit ADVISORY detector's job, so we SKIP it. We flag ONLY
|
|
26
|
+
* concrete, explicit tool unions where every leg is supplied by a named tool.
|
|
27
|
+
*
|
|
28
|
+
* Pure, no IO. The delegation graph (nodes + directed `delegatesTo` edges) is the
|
|
29
|
+
* INPUT — this module does not decide where edges come from; a caller supplies them
|
|
30
|
+
* from the parsed harness. The dialect is injected (core ⊄ adapter), reused for the
|
|
31
|
+
* underlying leg classification.
|
|
32
|
+
*/
|
|
33
|
+
import type { HarnessDialect } from "./dialect.js";
|
|
34
|
+
import { type TrifectaLegs } from "./lethal-trifecta.js";
|
|
35
|
+
/** One unit (subagent/skill) in the delegation graph. */
|
|
36
|
+
export interface CapabilityNode {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly kind: "skill" | "agent";
|
|
39
|
+
/** This unit's OWN declared tools. [] = none declared. ["*"] = inherits-all (wildcard). */
|
|
40
|
+
readonly tools: readonly string[];
|
|
41
|
+
/** Names of units this one can delegate to / inherits capabilities from (directed edges). */
|
|
42
|
+
readonly delegatesTo: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/** A trifecta that EMERGES across delegation — present in a unit's effective set but NOT its own. */
|
|
45
|
+
export interface DelegationTrifectaFinding {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly kind: "skill" | "agent";
|
|
48
|
+
/** The delegated-to units (by name) that supplied at least one leg the unit lacks on its own. */
|
|
49
|
+
readonly via: readonly string[];
|
|
50
|
+
/** The tools that supplied each leg in the EFFECTIVE (combined) set. */
|
|
51
|
+
readonly legs: TrifectaLegs;
|
|
52
|
+
/** Ready-to-show, actionable message. */
|
|
53
|
+
readonly message: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Find units whose EFFECTIVE (own + delegated) capability set forms a full lethal
|
|
57
|
+
* trifecta that their OWN set does not — an emergent, cross-delegation exfil path.
|
|
58
|
+
*
|
|
59
|
+
* Returns findings in stable order (by node name). See the module header for the
|
|
60
|
+
* skip rules (own-set already a trifecta → owned by the per-unit detector; an
|
|
61
|
+
* effective wildcard → owned by the per-unit advisory).
|
|
62
|
+
*/
|
|
63
|
+
export declare function delegationTrifectaIssues(nodes: readonly CapabilityNode[], dialect: HarnessDialect): DelegationTrifectaFinding[];
|
|
64
|
+
//# sourceMappingURL=delegation-trifecta.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.delegationTrifectaIssues = delegationTrifectaIssues;
|
|
4
|
+
const lethal_trifecta_js_1 = require("./lethal-trifecta.js");
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Internal helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** A full trifecta = all three legs non-empty. */
|
|
9
|
+
function isFullTrifecta(legs) {
|
|
10
|
+
return (legs.private.length > 0 &&
|
|
11
|
+
legs.untrusted.length > 0 &&
|
|
12
|
+
legs.exfil.length > 0);
|
|
13
|
+
}
|
|
14
|
+
/** Strips a `Tool(restriction)` suffix and returns the base tool name. */
|
|
15
|
+
function baseTool(raw) {
|
|
16
|
+
return raw.split("(")[0].trim();
|
|
17
|
+
}
|
|
18
|
+
/** True for the wildcard sentinels that mean "inherits-all". */
|
|
19
|
+
function isWildcard(tool) {
|
|
20
|
+
return tool === "" || tool === "*";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The set of node names reachable from `start` over `delegatesTo`, INCLUDING
|
|
24
|
+
* `start` itself. Cycle-safe (a `visited` set). An edge naming a node not in the
|
|
25
|
+
* map is skipped — its tools can't be resolved.
|
|
26
|
+
*/
|
|
27
|
+
function effectiveReach(start, byName) {
|
|
28
|
+
const visited = new Set();
|
|
29
|
+
const stack = [start];
|
|
30
|
+
while (stack.length > 0) {
|
|
31
|
+
const name = stack.pop();
|
|
32
|
+
if (name === undefined || visited.has(name))
|
|
33
|
+
continue;
|
|
34
|
+
visited.add(name);
|
|
35
|
+
const node = byName.get(name);
|
|
36
|
+
if (node === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
for (const next of node.delegatesTo) {
|
|
39
|
+
if (!visited.has(next))
|
|
40
|
+
stack.push(next);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return visited;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Public API
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Find units whose EFFECTIVE (own + delegated) capability set forms a full lethal
|
|
50
|
+
* trifecta that their OWN set does not — an emergent, cross-delegation exfil path.
|
|
51
|
+
*
|
|
52
|
+
* Returns findings in stable order (by node name). See the module header for the
|
|
53
|
+
* skip rules (own-set already a trifecta → owned by the per-unit detector; an
|
|
54
|
+
* effective wildcard → owned by the per-unit advisory).
|
|
55
|
+
*/
|
|
56
|
+
function delegationTrifectaIssues(nodes, dialect) {
|
|
57
|
+
const byName = new Map();
|
|
58
|
+
for (const node of nodes)
|
|
59
|
+
byName.set(node.name, node);
|
|
60
|
+
const findings = [];
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
// (b) If the unit's OWN tools already form a full trifecta, the per-unit
|
|
63
|
+
// detector owns it — never double-report.
|
|
64
|
+
const ownLegs = (0, lethal_trifecta_js_1.classifyTrifectaLegs)(node.tools, dialect);
|
|
65
|
+
if (isFullTrifecta(ownLegs))
|
|
66
|
+
continue;
|
|
67
|
+
// (c) Effective tools = the de-duplicated union across the reachable set.
|
|
68
|
+
const reach = effectiveReach(node.name, byName);
|
|
69
|
+
const effSet = new Set();
|
|
70
|
+
for (const name of reach) {
|
|
71
|
+
const reached = byName.get(name);
|
|
72
|
+
if (reached === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
for (const tool of reached.tools)
|
|
75
|
+
effSet.add(tool);
|
|
76
|
+
}
|
|
77
|
+
const effectiveTools = [...effSet];
|
|
78
|
+
// (d) FP-safe wildcard guard: an inherits-all unit in the reachable set
|
|
79
|
+
// would always "trifecta" — that's the per-unit advisory's job.
|
|
80
|
+
if (effectiveTools.some((t) => isWildcard(baseTool(t))))
|
|
81
|
+
continue;
|
|
82
|
+
// (e) Classify the effective set.
|
|
83
|
+
const effLegs = (0, lethal_trifecta_js_1.classifyTrifectaLegs)(effectiveTools, dialect);
|
|
84
|
+
// (f) Emit ONLY when the effective set is a full trifecta (own set wasn't).
|
|
85
|
+
if (!isFullTrifecta(effLegs))
|
|
86
|
+
continue;
|
|
87
|
+
// The tools that supplied any leg in the effective set.
|
|
88
|
+
const legTools = new Set([
|
|
89
|
+
...effLegs.private,
|
|
90
|
+
...effLegs.untrusted,
|
|
91
|
+
...effLegs.exfil,
|
|
92
|
+
]);
|
|
93
|
+
// `via` = reachable units (excluding this node) that contribute a leg tool.
|
|
94
|
+
const via = [];
|
|
95
|
+
for (const name of reach) {
|
|
96
|
+
if (name === node.name)
|
|
97
|
+
continue;
|
|
98
|
+
const reached = byName.get(name);
|
|
99
|
+
if (reached === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
const contributes = reached.tools.some((t) => legTools.has(baseTool(t)));
|
|
102
|
+
if (contributes && !via.includes(name))
|
|
103
|
+
via.push(name);
|
|
104
|
+
}
|
|
105
|
+
via.sort();
|
|
106
|
+
const message = `Subagent "${node.name}" is not a data-leak risk on its own, but combined ` +
|
|
107
|
+
`with what it delegates to (${via.join(", ")}), the chain can read private ` +
|
|
108
|
+
`data (${effLegs.private.join(", ")}), ingest untrusted content ` +
|
|
109
|
+
`(${effLegs.untrusted.join(", ")}), AND exfiltrate ` +
|
|
110
|
+
`(${effLegs.exfil.join(", ")}) — a prompt injection in the untrusted input ` +
|
|
111
|
+
`could pivot through the delegation to leak data. Break the delegation or ` +
|
|
112
|
+
`drop one leg.`;
|
|
113
|
+
findings.push({
|
|
114
|
+
name: node.name,
|
|
115
|
+
kind: node.kind,
|
|
116
|
+
via,
|
|
117
|
+
legs: effLegs,
|
|
118
|
+
message,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
findings.sort((a, b) => a.name.localeCompare(b.name));
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=delegation-trifecta.js.map
|