claude-flow 3.32.13 → 3.32.15
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/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +27 -13
- package/v3/@claude-flow/cli/dist/src/commands/security.js +94 -7
- package/v3/@claude-flow/cli/dist/src/parser.d.ts +14 -0
- package/v3/@claude-flow/cli/dist/src/parser.js +22 -1
- package/v3/@claude-flow/cli/dist/src/security/mcp-composition-inspector.d.ts +72 -0
- package/v3/@claude-flow/cli/dist/src/security/mcp-composition-inspector.js +209 -0
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.15",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -659,17 +659,26 @@ const routeCommand = {
|
|
|
659
659
|
default: 'single',
|
|
660
660
|
},
|
|
661
661
|
{
|
|
662
|
-
// #2778
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
662
|
+
// #2778 canonical fanout-width flag. Restored to `--parallel` in
|
|
663
|
+
// v3.32.14 after the parser scoping fix landed (subcommand's
|
|
664
|
+
// non-boolean declaration now overrides the global boolean set,
|
|
665
|
+
// so `--parallel 7` on `hooks route` no longer collides with the
|
|
666
|
+
// boolean `--parallel` on `swarm start` / `workflow run`).
|
|
667
|
+
// NOTE: no `default:` here — default is resolved in the action so
|
|
668
|
+
// we can distinguish "user explicitly passed --parallel" from
|
|
669
|
+
// "user passed --moa-parallel (the v3.32.13 compat alias)". Without
|
|
670
|
+
// this, the parser's applied default would shadow the alias.
|
|
671
|
+
name: 'parallel',
|
|
672
|
+
short: 'p',
|
|
670
673
|
description: 'MoA fanout width (N parallel calls at Haiku tier; only meaningful when --mode=moa)',
|
|
671
674
|
type: 'number',
|
|
672
|
-
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
// v3.32.13 compat alias — kept so any users who upgraded to that
|
|
678
|
+
// release keep working. Prefer `--parallel` going forward.
|
|
679
|
+
name: 'moa-parallel',
|
|
680
|
+
description: 'DEPRECATED alias for --parallel (kept for v3.32.13 backward compat)',
|
|
681
|
+
type: 'number',
|
|
673
682
|
},
|
|
674
683
|
{
|
|
675
684
|
name: 'consensus',
|
|
@@ -682,15 +691,20 @@ const routeCommand = {
|
|
|
682
691
|
examples: [
|
|
683
692
|
{ command: 'claude-flow hooks route -t "Fix authentication bug"', description: 'Route task to optimal agent (single mode)' },
|
|
684
693
|
{ command: 'claude-flow hooks route -t "Optimize database queries" -K 5', description: 'Get top 5 suggestions' },
|
|
685
|
-
{ command: 'claude-flow hooks route -t "Design payment webhook" --mode moa --
|
|
694
|
+
{ command: 'claude-flow hooks route -t "Design payment webhook" --mode moa --parallel 3', description: 'MoA fanout: 3× Haiku + consensus (dream-cycle #2778)' },
|
|
686
695
|
],
|
|
687
696
|
action: async (ctx) => {
|
|
688
697
|
const task = ctx.flags.task || ctx.args[0];
|
|
689
698
|
const topK = ctx.flags.topK || 3;
|
|
690
699
|
const mode = ctx.flags.mode || 'single';
|
|
691
|
-
// #2778
|
|
692
|
-
//
|
|
693
|
-
|
|
700
|
+
// #2778: prefer canonical --parallel; fall back to --moa-parallel
|
|
701
|
+
// (v3.32.13 compat alias). Nullish coalescing so an explicit 0 still
|
|
702
|
+
// falls through to the alias / default, and neither flag being set
|
|
703
|
+
// resolves to 3 (the ACL 2026 SRW paper's baseline fanout width).
|
|
704
|
+
const parallelRaw = ctx.flags.parallel
|
|
705
|
+
?? ctx.flags.moaParallel
|
|
706
|
+
?? 3;
|
|
707
|
+
const parallel = Math.max(2, parallelRaw);
|
|
694
708
|
const consensus = ctx.flags.consensus || 'majority-vote';
|
|
695
709
|
if (!task) {
|
|
696
710
|
output.printError('Task description is required. Use --task or -t flag.');
|
|
@@ -977,15 +977,101 @@ const defendCommand = {
|
|
|
977
977
|
return { success: safe, exitCode: safe ? 0 : 1 };
|
|
978
978
|
},
|
|
979
979
|
};
|
|
980
|
+
// #2783 dream-cycle — MCP Composition Inspector subcommand.
|
|
981
|
+
// Standalone scanner (no LLM) that reads the registered MCP tool
|
|
982
|
+
// descriptors and flags cross-tool signals of Shamir-split prompt
|
|
983
|
+
// injection (arXiv 2606.27027 ShareLock) + typo-squat lookalikes
|
|
984
|
+
// against trusted ruflo prefixes.
|
|
985
|
+
const compositionScanCommand = {
|
|
986
|
+
name: 'composition-scan',
|
|
987
|
+
description: 'Scan registered MCP tool descriptions for cross-tool prompt-injection signatures (dream-cycle #2783)',
|
|
988
|
+
options: [
|
|
989
|
+
{ name: 'min-fragment', type: 'number', default: 20, description: 'Minimum shared-substring length to flag (default: 20 chars)' },
|
|
990
|
+
{ name: 'top', type: 'number', default: 20, description: 'Show top N suspects (default: 20)' },
|
|
991
|
+
{ name: 'tools-json', type: 'string', description: 'Path to a JSON file of {name, description}[] to scan (default: scan the CLI\'s own registered MCP tools)' },
|
|
992
|
+
],
|
|
993
|
+
examples: [
|
|
994
|
+
{ command: 'claude-flow security composition-scan', description: 'Scan the CLI\'s own registered MCP tool descriptions' },
|
|
995
|
+
{ command: 'claude-flow security composition-scan --tools-json ./external-mcp-registry.json --top 50', description: 'Scan a third-party MCP registry' },
|
|
996
|
+
],
|
|
997
|
+
action: async (ctx) => {
|
|
998
|
+
const minFragment = ctx.flags.minFragment || 20;
|
|
999
|
+
const top = ctx.flags.top || 20;
|
|
1000
|
+
const toolsJson = ctx.flags.toolsJson;
|
|
1001
|
+
let tools = [];
|
|
1002
|
+
try {
|
|
1003
|
+
if (toolsJson) {
|
|
1004
|
+
const fs = await import('node:fs');
|
|
1005
|
+
const path = await import('node:path');
|
|
1006
|
+
const raw = fs.readFileSync(path.resolve(toolsJson), 'utf-8');
|
|
1007
|
+
const parsed = JSON.parse(raw);
|
|
1008
|
+
if (!Array.isArray(parsed))
|
|
1009
|
+
throw new Error(`${toolsJson} must contain a JSON array of {name, description}`);
|
|
1010
|
+
tools = parsed
|
|
1011
|
+
.filter((t) => typeof t === 'object' && t !== null &&
|
|
1012
|
+
typeof t.name === 'string' &&
|
|
1013
|
+
typeof t.description === 'string')
|
|
1014
|
+
.map((t) => ({ name: t.name, description: t.description }));
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
// Scan the CLI's own registered MCP tools via the client registry.
|
|
1018
|
+
const { listMCPTools } = await import('../mcp-client.js');
|
|
1019
|
+
tools = listMCPTools().map((t) => ({ name: t.name, description: t.description }));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
catch (err) {
|
|
1023
|
+
output.printError(`Failed to load tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
1024
|
+
return { success: false, exitCode: 1 };
|
|
1025
|
+
}
|
|
1026
|
+
const { scanToolDescriptions } = await import('../security/mcp-composition-inspector.js');
|
|
1027
|
+
const result = scanToolDescriptions(tools, { minFragment });
|
|
1028
|
+
output.writeln();
|
|
1029
|
+
output.printBox(`Scanned ${result.stats.toolsScanned} MCP tools; compared ${result.stats.pairsCompared} pairs.\n` +
|
|
1030
|
+
`Suspects: ${result.suspects.length}`, 'MCP Composition Inspector (#2783)');
|
|
1031
|
+
if (ctx.flags.format === 'json') {
|
|
1032
|
+
output.printJson(result);
|
|
1033
|
+
return { success: true, data: result };
|
|
1034
|
+
}
|
|
1035
|
+
if (result.suspects.length === 0) {
|
|
1036
|
+
output.writeln();
|
|
1037
|
+
output.printSuccess('No cross-tool prompt-injection signatures detected.');
|
|
1038
|
+
output.writeln(output.dim('Note: heuristic scanner. Absence of hits does not prove safety — MCP tools you didn\'t audit yourself should still be treated as untrusted.'));
|
|
1039
|
+
return { success: true, data: result };
|
|
1040
|
+
}
|
|
1041
|
+
const sorted = [...result.suspects].sort((a, b) => b.score - a.score).slice(0, top);
|
|
1042
|
+
output.writeln();
|
|
1043
|
+
output.writeln(output.bold(`Top ${sorted.length} suspects (sorted by score)`));
|
|
1044
|
+
output.printTable({
|
|
1045
|
+
columns: [
|
|
1046
|
+
{ key: 'kind', header: 'Kind', width: 18 },
|
|
1047
|
+
{ key: 'tool', header: 'Tool', width: 30 },
|
|
1048
|
+
{ key: 'peer', header: 'Peer', width: 30 },
|
|
1049
|
+
{ key: 'score', header: 'Score', width: 8, align: 'right', format: (v) => (Number(v)).toFixed(2) },
|
|
1050
|
+
{ key: 'fragment', header: 'Fragment', width: 40 },
|
|
1051
|
+
],
|
|
1052
|
+
data: sorted.map((s) => ({
|
|
1053
|
+
kind: s.kind,
|
|
1054
|
+
tool: s.tool,
|
|
1055
|
+
peer: s.peer ?? '',
|
|
1056
|
+
score: s.score,
|
|
1057
|
+
fragment: s.fragment.length > 40 ? s.fragment.slice(0, 37) + '…' : s.fragment,
|
|
1058
|
+
})),
|
|
1059
|
+
});
|
|
1060
|
+
output.writeln();
|
|
1061
|
+
output.writeln(output.dim('Score ≥ 0.9 = high (likely real). 0.5–0.9 = investigate. < 0.5 = probably benign coincidence.'));
|
|
1062
|
+
return { success: true, data: result };
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
980
1065
|
// Main security command
|
|
981
1066
|
export const securityCommand = {
|
|
982
1067
|
name: 'security',
|
|
983
1068
|
description: 'Security scanning, CVE detection, threat modeling, AI defense',
|
|
984
|
-
subcommands: [scanCommand, cveCommand, threatsCommand, auditCommand, secretsCommand, defendCommand],
|
|
1069
|
+
subcommands: [scanCommand, cveCommand, threatsCommand, auditCommand, secretsCommand, defendCommand, compositionScanCommand],
|
|
985
1070
|
examples: [
|
|
986
1071
|
{ command: 'claude-flow security scan', description: 'Run security scan' },
|
|
987
1072
|
{ command: 'claude-flow security cve --list', description: 'List known CVEs' },
|
|
988
1073
|
{ command: 'claude-flow security threats', description: 'Run threat analysis' },
|
|
1074
|
+
{ command: 'claude-flow security composition-scan', description: 'Scan MCP tool descriptions for cross-tool injection (dream-cycle #2783)' },
|
|
989
1075
|
],
|
|
990
1076
|
action: async () => {
|
|
991
1077
|
output.writeln();
|
|
@@ -994,12 +1080,13 @@ export const securityCommand = {
|
|
|
994
1080
|
output.writeln();
|
|
995
1081
|
output.writeln('Subcommands:');
|
|
996
1082
|
output.printList([
|
|
997
|
-
'scan
|
|
998
|
-
'cve
|
|
999
|
-
'threats
|
|
1000
|
-
'audit
|
|
1001
|
-
'secrets
|
|
1002
|
-
'defend
|
|
1083
|
+
'scan - Run security scans on code, deps, containers',
|
|
1084
|
+
'cve - Check and manage CVE vulnerabilities',
|
|
1085
|
+
'threats - Threat modeling (STRIDE, DREAD, PASTA)',
|
|
1086
|
+
'audit - Security audit logging and compliance',
|
|
1087
|
+
'secrets - Detect and manage secrets in codebase',
|
|
1088
|
+
'defend - AI manipulation defense (prompt injection, jailbreaks, PII)',
|
|
1089
|
+
'composition-scan - Cross-tool prompt-injection scan on MCP registry (dream-cycle #2783)',
|
|
1003
1090
|
]);
|
|
1004
1091
|
output.writeln();
|
|
1005
1092
|
output.writeln('Use --help with subcommands for more info');
|
|
@@ -78,6 +78,20 @@ export declare class CommandParser {
|
|
|
78
78
|
private buildScopedAliases;
|
|
79
79
|
/**
|
|
80
80
|
* Get boolean flags scoped to a specific command/subcommand.
|
|
81
|
+
*
|
|
82
|
+
* `getBooleanFlags()` walks EVERY command + subcommand in the registry
|
|
83
|
+
* and adds their boolean options into one flat set. That's convenient
|
|
84
|
+
* for the common case but causes cross-subcommand pollution: if `swarm
|
|
85
|
+
* start --parallel` is boolean AND `hooks route --parallel` is numeric,
|
|
86
|
+
* the flat set marks `parallel` as boolean, and `hooks route --parallel 7`
|
|
87
|
+
* drops the `7` as a positional (parseFlag treats booleanFlags-hit as
|
|
88
|
+
* "consume no value"). This is the exact bug that forced the
|
|
89
|
+
* --moa-parallel rename in the 2026-07-26 dream-cycle #2778 fix.
|
|
90
|
+
*
|
|
91
|
+
* Fix: if the resolved subcommand declares the flag as a non-boolean
|
|
92
|
+
* type, REMOVE it from the boolean set. Narrowest scope wins.
|
|
93
|
+
* (Documented via in-tree comment in commands/hooks.ts and the #2778
|
|
94
|
+
* commit message.)
|
|
81
95
|
*/
|
|
82
96
|
private getScopedBooleanFlags;
|
|
83
97
|
private getBooleanFlags;
|
|
@@ -406,13 +406,34 @@ export class CommandParser {
|
|
|
406
406
|
}
|
|
407
407
|
/**
|
|
408
408
|
* Get boolean flags scoped to a specific command/subcommand.
|
|
409
|
+
*
|
|
410
|
+
* `getBooleanFlags()` walks EVERY command + subcommand in the registry
|
|
411
|
+
* and adds their boolean options into one flat set. That's convenient
|
|
412
|
+
* for the common case but causes cross-subcommand pollution: if `swarm
|
|
413
|
+
* start --parallel` is boolean AND `hooks route --parallel` is numeric,
|
|
414
|
+
* the flat set marks `parallel` as boolean, and `hooks route --parallel 7`
|
|
415
|
+
* drops the `7` as a positional (parseFlag treats booleanFlags-hit as
|
|
416
|
+
* "consume no value"). This is the exact bug that forced the
|
|
417
|
+
* --moa-parallel rename in the 2026-07-26 dream-cycle #2778 fix.
|
|
418
|
+
*
|
|
419
|
+
* Fix: if the resolved subcommand declares the flag as a non-boolean
|
|
420
|
+
* type, REMOVE it from the boolean set. Narrowest scope wins.
|
|
421
|
+
* (Documented via in-tree comment in commands/hooks.ts and the #2778
|
|
422
|
+
* commit message.)
|
|
409
423
|
*/
|
|
410
424
|
getScopedBooleanFlags(resolvedCmd) {
|
|
411
425
|
const flags = this.getBooleanFlags();
|
|
412
426
|
if (resolvedCmd?.options) {
|
|
413
427
|
for (const opt of resolvedCmd.options) {
|
|
428
|
+
const key = this.normalizeKey(opt.name);
|
|
414
429
|
if (opt.type === 'boolean') {
|
|
415
|
-
flags.add(
|
|
430
|
+
flags.add(key);
|
|
431
|
+
}
|
|
432
|
+
else if (opt.type) {
|
|
433
|
+
// Subcommand explicitly re-declares this flag as a non-boolean
|
|
434
|
+
// type (string/number/array) — narrowest scope wins, so drop
|
|
435
|
+
// it from the boolean set to override any global pollution.
|
|
436
|
+
flags.delete(key);
|
|
416
437
|
}
|
|
417
438
|
}
|
|
418
439
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Composition Inspector (#2783 dream-cycle, arXiv 2606.27027 ShareLock).
|
|
3
|
+
*
|
|
4
|
+
* The attack: an adversary registers N seemingly-benign MCP tools whose
|
|
5
|
+
* INDIVIDUAL descriptions look fine, but whose CONCATENATION forms an
|
|
6
|
+
* injection payload targeting a specific downstream agent. Per-tool
|
|
7
|
+
* inspection misses this (each fragment is under the detection threshold).
|
|
8
|
+
* The inspector composes them and looks for cross-tool signal.
|
|
9
|
+
*
|
|
10
|
+
* SCOPE: heuristic detector, not a security boundary. Reports SUSPECTS —
|
|
11
|
+
* the operator decides. No blocking, no rewriting. This is the "MCP
|
|
12
|
+
* Composition Inspector" surface from dream-cycle #2783's ADR-320
|
|
13
|
+
* recommendation, delivered as a bounded engineering fix rather than
|
|
14
|
+
* an ADR-scope subsystem.
|
|
15
|
+
*
|
|
16
|
+
* Detection method (v1 — deterministic, no LLM):
|
|
17
|
+
* 1. Common-substring scan: for every pair of tools, find shared
|
|
18
|
+
* substrings of length ≥ minFragment (default 20). Attacker
|
|
19
|
+
* fragments would repeat identically or near-identically across
|
|
20
|
+
* the split tools; benign tools rarely share 20+ char sequences
|
|
21
|
+
* that aren't proper nouns or well-known phrases.
|
|
22
|
+
* 2. Suspicious-phrase catalog: strings that appear in known
|
|
23
|
+
* prompt-injection templates ("ignore previous instructions",
|
|
24
|
+
* "you are now", "system prompt", "delete all", etc.) — even
|
|
25
|
+
* inside one tool, flagged.
|
|
26
|
+
* 3. Tool-name lookalikes: names that differ by ≤ 2 chars from a
|
|
27
|
+
* known-trusted ruflo tool (typosquatting mitigation).
|
|
28
|
+
*
|
|
29
|
+
* Future v2: SimHash + LSH for scale (arXiv 2606.27027's proposal),
|
|
30
|
+
* OWASP LLM07 alignment.
|
|
31
|
+
*/
|
|
32
|
+
export interface ToolDescriptor {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CompositionSuspect {
|
|
37
|
+
/** Suspect category — `shared-fragment`, `injection-phrase`, `name-lookalike`. */
|
|
38
|
+
kind: 'shared-fragment' | 'injection-phrase' | 'name-lookalike';
|
|
39
|
+
/** Tool name that carries the suspicious content. */
|
|
40
|
+
tool: string;
|
|
41
|
+
/** Peer tool (for shared-fragment / name-lookalike) or empty. */
|
|
42
|
+
peer?: string;
|
|
43
|
+
/** The offending substring or phrase (truncated for display). */
|
|
44
|
+
fragment: string;
|
|
45
|
+
/** Heuristic score, 0..1. Higher = more suspicious. */
|
|
46
|
+
score: number;
|
|
47
|
+
/** Human-readable explanation. */
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
export interface CompositionScanResult {
|
|
51
|
+
suspects: CompositionSuspect[];
|
|
52
|
+
stats: {
|
|
53
|
+
toolsScanned: number;
|
|
54
|
+
pairsCompared: number;
|
|
55
|
+
fragmentsCompared: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare function scanToolDescriptions(tools: ToolDescriptor[], options?: {
|
|
59
|
+
minFragment?: number;
|
|
60
|
+
trustedPrefixes?: readonly string[];
|
|
61
|
+
injectionPhrases?: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* Maximum number of tools a fragment may appear in before we treat it
|
|
64
|
+
* as "template language" and stop flagging it. Attack fragments live
|
|
65
|
+
* in small conspiracies (2–3 tools); template language shows up in
|
|
66
|
+
* dozens. Default 3 catches Shamir-split attacks without flooding on
|
|
67
|
+
* legitimate ruflo tools that share the same MCP-tool-description
|
|
68
|
+
* template.
|
|
69
|
+
*/
|
|
70
|
+
maxFragmentPopulation?: number;
|
|
71
|
+
}): CompositionScanResult;
|
|
72
|
+
//# sourceMappingURL=mcp-composition-inspector.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Composition Inspector (#2783 dream-cycle, arXiv 2606.27027 ShareLock).
|
|
3
|
+
*
|
|
4
|
+
* The attack: an adversary registers N seemingly-benign MCP tools whose
|
|
5
|
+
* INDIVIDUAL descriptions look fine, but whose CONCATENATION forms an
|
|
6
|
+
* injection payload targeting a specific downstream agent. Per-tool
|
|
7
|
+
* inspection misses this (each fragment is under the detection threshold).
|
|
8
|
+
* The inspector composes them and looks for cross-tool signal.
|
|
9
|
+
*
|
|
10
|
+
* SCOPE: heuristic detector, not a security boundary. Reports SUSPECTS —
|
|
11
|
+
* the operator decides. No blocking, no rewriting. This is the "MCP
|
|
12
|
+
* Composition Inspector" surface from dream-cycle #2783's ADR-320
|
|
13
|
+
* recommendation, delivered as a bounded engineering fix rather than
|
|
14
|
+
* an ADR-scope subsystem.
|
|
15
|
+
*
|
|
16
|
+
* Detection method (v1 — deterministic, no LLM):
|
|
17
|
+
* 1. Common-substring scan: for every pair of tools, find shared
|
|
18
|
+
* substrings of length ≥ minFragment (default 20). Attacker
|
|
19
|
+
* fragments would repeat identically or near-identically across
|
|
20
|
+
* the split tools; benign tools rarely share 20+ char sequences
|
|
21
|
+
* that aren't proper nouns or well-known phrases.
|
|
22
|
+
* 2. Suspicious-phrase catalog: strings that appear in known
|
|
23
|
+
* prompt-injection templates ("ignore previous instructions",
|
|
24
|
+
* "you are now", "system prompt", "delete all", etc.) — even
|
|
25
|
+
* inside one tool, flagged.
|
|
26
|
+
* 3. Tool-name lookalikes: names that differ by ≤ 2 chars from a
|
|
27
|
+
* known-trusted ruflo tool (typosquatting mitigation).
|
|
28
|
+
*
|
|
29
|
+
* Future v2: SimHash + LSH for scale (arXiv 2606.27027's proposal),
|
|
30
|
+
* OWASP LLM07 alignment.
|
|
31
|
+
*/
|
|
32
|
+
/** Known prompt-injection phrases (case-insensitive substring match). */
|
|
33
|
+
const INJECTION_PHRASES = [
|
|
34
|
+
'ignore previous instructions',
|
|
35
|
+
'ignore all prior',
|
|
36
|
+
'disregard the above',
|
|
37
|
+
'you are now',
|
|
38
|
+
'act as',
|
|
39
|
+
'system prompt',
|
|
40
|
+
'system:',
|
|
41
|
+
'assistant:',
|
|
42
|
+
'delete all',
|
|
43
|
+
'rm -rf',
|
|
44
|
+
'exfiltrate',
|
|
45
|
+
'send me the',
|
|
46
|
+
'reveal your',
|
|
47
|
+
'reveal the',
|
|
48
|
+
'override',
|
|
49
|
+
'jailbreak',
|
|
50
|
+
];
|
|
51
|
+
/** Known-trusted ruflo tool-name prefixes for typosquatting comparison. */
|
|
52
|
+
const TRUSTED_PREFIXES = [
|
|
53
|
+
'memory_', 'hooks_', 'swarm_', 'agent_', 'claims_', 'coordination_',
|
|
54
|
+
'session_', 'workflow_', 'neural_', 'browser_', 'daemon_', 'agentdb_',
|
|
55
|
+
'aidefence_', 'analyze_', 'autopilot_', 'business_pod_', 'config_',
|
|
56
|
+
'embeddings_', 'federation_', 'github_', 'guidance_', 'hive-mind_',
|
|
57
|
+
'http_', 'managed_agent_', 'mcp_', 'metaharness_', 'performance_',
|
|
58
|
+
'progress_', 'ruvllm_', 'system_', 'task_', 'terminal_', 'transfer_',
|
|
59
|
+
'wasm_', 'wasm_gallery_', 'agenticow_',
|
|
60
|
+
];
|
|
61
|
+
function normalize(s) {
|
|
62
|
+
return s.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
63
|
+
}
|
|
64
|
+
/** Levenshtein distance (bounded — returns Infinity past `max`). */
|
|
65
|
+
function editDistance(a, b, max = 3) {
|
|
66
|
+
if (Math.abs(a.length - b.length) > max)
|
|
67
|
+
return Infinity;
|
|
68
|
+
const m = a.length, n = b.length;
|
|
69
|
+
const dp = Array.from({ length: n + 1 }, (_, i) => i);
|
|
70
|
+
for (let i = 1; i <= m; i++) {
|
|
71
|
+
let prev = dp[0];
|
|
72
|
+
dp[0] = i;
|
|
73
|
+
let minRow = dp[0];
|
|
74
|
+
for (let j = 1; j <= n; j++) {
|
|
75
|
+
const cur = dp[j];
|
|
76
|
+
dp[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, dp[j - 1], dp[j]) + 1;
|
|
77
|
+
prev = cur;
|
|
78
|
+
if (dp[j] < minRow)
|
|
79
|
+
minRow = dp[j];
|
|
80
|
+
}
|
|
81
|
+
if (minRow > max)
|
|
82
|
+
return Infinity;
|
|
83
|
+
}
|
|
84
|
+
return dp[n];
|
|
85
|
+
}
|
|
86
|
+
/** Find all common substrings of length ≥ minLen between a and b. */
|
|
87
|
+
function commonSubstrings(a, b, minLen, cap = 20) {
|
|
88
|
+
const out = new Set();
|
|
89
|
+
const aLen = a.length, bLen = b.length;
|
|
90
|
+
if (aLen < minLen || bLen < minLen)
|
|
91
|
+
return [];
|
|
92
|
+
// Sliding-window: hash each a[i..i+minLen] and check if it appears in b.
|
|
93
|
+
// Then expand greedily. Cheap and deterministic.
|
|
94
|
+
const seen = new Map();
|
|
95
|
+
for (let i = 0; i + minLen <= aLen; i++) {
|
|
96
|
+
seen.set(a.slice(i, i + minLen), i);
|
|
97
|
+
}
|
|
98
|
+
for (let j = 0; j + minLen <= bLen; j++) {
|
|
99
|
+
const window = b.slice(j, j + minLen);
|
|
100
|
+
const aIdx = seen.get(window);
|
|
101
|
+
if (aIdx === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
// Expand greedily forward
|
|
104
|
+
let len = minLen;
|
|
105
|
+
while (aIdx + len < aLen &&
|
|
106
|
+
j + len < bLen &&
|
|
107
|
+
a[aIdx + len] === b[j + len])
|
|
108
|
+
len++;
|
|
109
|
+
out.add(a.slice(aIdx, aIdx + len));
|
|
110
|
+
if (out.size >= cap)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return Array.from(out);
|
|
114
|
+
}
|
|
115
|
+
export function scanToolDescriptions(tools, options = {}) {
|
|
116
|
+
const minFragment = options.minFragment ?? 20;
|
|
117
|
+
const trustedPrefixes = options.trustedPrefixes ?? TRUSTED_PREFIXES;
|
|
118
|
+
const injectionPhrases = options.injectionPhrases ?? INJECTION_PHRASES;
|
|
119
|
+
const maxPopulation = options.maxFragmentPopulation ?? 3;
|
|
120
|
+
const suspects = [];
|
|
121
|
+
const stats = { toolsScanned: tools.length, pairsCompared: 0, fragmentsCompared: 0 };
|
|
122
|
+
const normalized = tools.map((t) => ({
|
|
123
|
+
name: t.name,
|
|
124
|
+
raw: t.description,
|
|
125
|
+
norm: normalize(t.description),
|
|
126
|
+
}));
|
|
127
|
+
// 1) Injection-phrase scan (per tool)
|
|
128
|
+
for (const t of normalized) {
|
|
129
|
+
for (const phrase of injectionPhrases) {
|
|
130
|
+
const idx = t.norm.indexOf(phrase);
|
|
131
|
+
if (idx >= 0) {
|
|
132
|
+
suspects.push({
|
|
133
|
+
kind: 'injection-phrase',
|
|
134
|
+
tool: t.name,
|
|
135
|
+
fragment: t.raw.slice(Math.max(0, idx - 10), idx + phrase.length + 20).trim(),
|
|
136
|
+
score: 0.9,
|
|
137
|
+
reason: `Description contains known prompt-injection phrase "${phrase}"`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 2) Cross-tool shared-fragment scan — first collect ALL candidate
|
|
143
|
+
// fragments and count how many distinct tools each appears in. Only
|
|
144
|
+
// flag pairs where the fragment shows up in ≤ maxPopulation tools
|
|
145
|
+
// (template language across N tools is not an attack signature).
|
|
146
|
+
const fragmentToTools = new Map();
|
|
147
|
+
const rawPairs = [];
|
|
148
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
149
|
+
for (let j = i + 1; j < normalized.length; j++) {
|
|
150
|
+
stats.pairsCompared++;
|
|
151
|
+
const shared = commonSubstrings(normalized[i].norm, normalized[j].norm, minFragment);
|
|
152
|
+
stats.fragmentsCompared += shared.length;
|
|
153
|
+
for (const frag of shared) {
|
|
154
|
+
const wordCount = new Set(frag.split(/\s+/).filter(w => w.length > 2)).size;
|
|
155
|
+
if (wordCount < 3)
|
|
156
|
+
continue;
|
|
157
|
+
const set = fragmentToTools.get(frag) ?? new Set();
|
|
158
|
+
set.add(normalized[i].name);
|
|
159
|
+
set.add(normalized[j].name);
|
|
160
|
+
fragmentToTools.set(frag, set);
|
|
161
|
+
rawPairs.push({ i, j, frag });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Emit only fragments that live in a SMALL conspiracy of tools (≤ maxPop).
|
|
166
|
+
const emittedPair = new Set();
|
|
167
|
+
for (const { i, j, frag } of rawPairs) {
|
|
168
|
+
const population = fragmentToTools.get(frag)?.size ?? 0;
|
|
169
|
+
if (population > maxPopulation)
|
|
170
|
+
continue;
|
|
171
|
+
const key = `${normalized[i].name}||${normalized[j].name}||${frag}`;
|
|
172
|
+
if (emittedPair.has(key))
|
|
173
|
+
continue;
|
|
174
|
+
emittedPair.add(key);
|
|
175
|
+
const wordCount = new Set(frag.split(/\s+/).filter(w => w.length > 2)).size;
|
|
176
|
+
suspects.push({
|
|
177
|
+
kind: 'shared-fragment',
|
|
178
|
+
tool: normalized[i].name,
|
|
179
|
+
peer: normalized[j].name,
|
|
180
|
+
fragment: frag.slice(0, 80),
|
|
181
|
+
score: Math.min(1, frag.length / 100),
|
|
182
|
+
reason: `Shared substring (${frag.length} chars, ${wordCount} words, appears in ${population} tools) with ${normalized[j].name} — possible Shamir-split fragment`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// 3) Name-lookalike scan (typosquat detection against trusted prefixes)
|
|
186
|
+
for (const t of normalized) {
|
|
187
|
+
for (const prefix of trustedPrefixes) {
|
|
188
|
+
// Exact-prefix match is fine (that's a legitimate ruflo tool)
|
|
189
|
+
if (t.name.startsWith(prefix))
|
|
190
|
+
break;
|
|
191
|
+
// Near-match to a trusted prefix (1-2 char edit distance)
|
|
192
|
+
const nameHead = t.name.slice(0, prefix.length);
|
|
193
|
+
const dist = editDistance(nameHead, prefix, 2);
|
|
194
|
+
if (dist > 0 && dist <= 2) {
|
|
195
|
+
suspects.push({
|
|
196
|
+
kind: 'name-lookalike',
|
|
197
|
+
tool: t.name,
|
|
198
|
+
peer: prefix,
|
|
199
|
+
fragment: nameHead,
|
|
200
|
+
score: 0.7,
|
|
201
|
+
reason: `Tool name prefix "${nameHead}" is ${dist} edit(s) from trusted ruflo prefix "${prefix}" — possible typosquat`,
|
|
202
|
+
});
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { suspects, stats };
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=mcp-composition-inspector.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|