openclaw-sc 5.38.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.
Files changed (48) hide show
  1. package/.env.example +13 -0
  2. package/INSTALL.md +13 -0
  3. package/LICENSE +233 -0
  4. package/README.md +123 -0
  5. package/SECURITY.md +27 -0
  6. package/index.js +3479 -0
  7. package/lib/code-review-shared.js +164 -0
  8. package/lib/config.js +164 -0
  9. package/lib/constants.js +438 -0
  10. package/lib/decomposer.js +896 -0
  11. package/lib/dialog-recall.js +389 -0
  12. package/lib/env.js +59 -0
  13. package/lib/level-rules.js +72 -0
  14. package/lib/log-manager.js +258 -0
  15. package/lib/preemption.js +278 -0
  16. package/lib/priority-calc.js +134 -0
  17. package/lib/prompt-injection.js +748 -0
  18. package/lib/route-evidence.js +701 -0
  19. package/lib/shared-fs.js +244 -0
  20. package/lib/steward-rules.js +648 -0
  21. package/lib/task-center.js +602 -0
  22. package/lib/task-chain.js +713 -0
  23. package/lib/task-checker.js +317 -0
  24. package/lib/task-profiles.js +255 -0
  25. package/lib/tcell.js +411 -0
  26. package/lib/tool-auto-discover.js +522 -0
  27. package/lib/tool-enrich-subagent.js +375 -0
  28. package/lib/tool-handlers.js +178 -0
  29. package/lib/tool-selector.js +459 -0
  30. package/openclaw.plugin.json +55 -0
  31. package/package.json +63 -0
  32. package/security.js +141 -0
  33. package/tools/bridge.js +3288 -0
  34. package/tools/dashboard/tk-dashboard.py +262 -0
  35. package/tools/hippocampus-multi-search.js +1038 -0
  36. package/tools/hippocampus-sqlite.js +738 -0
  37. package/tools/hippocampus-store.js +262 -0
  38. package/tools/mcp-tools.config.json +653 -0
  39. package/tools/pipeline-engine.js +667 -0
  40. package/tools/sidecar/_check_tools.py +34 -0
  41. package/tools/sidecar/sidecar-server.cjs +1360 -0
  42. package/tools/sidecar/subagent-runner.cjs +1471 -0
  43. package/tools/system-tools.js +619 -0
  44. package/vector/README.md +100 -0
  45. package/vector/usearch-bridge.js +639 -0
  46. package/vector/usearch-http.js +74 -0
  47. package/vector/usearch-serve.js +156 -0
  48. package/workers/worker.js +1419 -0
@@ -0,0 +1,748 @@
1
+ /**
2
+ * 🛡️ sc Prompt Injection 检测层
3
+ *
4
+ * 3层检测管线(原5层,杉哥2026-06-09删L1正则/L2模糊),延迟 < 1ms,无外部依赖
5
+ *
6
+ * 管线:
7
+ * L1: 正则模式(指令覆盖/角色扮演等6类)
8
+ * L2: 模糊匹配(Levenshtein距离)
9
+ * L3: 编码检测(零宽字符/Base64/Hex)
10
+ * L4: 结构分析(JSON伪装/格式注入)
11
+ * L5: 异常检测(Shannon熵/N-gram)
12
+ *
13
+ * 短路:任一 layer block 直达结果,不进后续层
14
+ */
15
+
16
+ // ============================================================
17
+ // Layer 1: 正则模式匹配
18
+ // ============================================================
19
+
20
+ // 攻击指令覆盖类
21
+ const PATTERN_INSTRUCTION_OVERRIDE = [
22
+ /ignore\s+(all\s+)?(previous|prior)\s+instructions?\b/i,
23
+ /ignore\s+(all\s+)?the\s+(above|foregoing)/i,
24
+ /override\s+(system|previous|all)\s+(instructions?|rules?|directives?)/i,
25
+ /disregard\s+(all\s+)?(previous|prior)\s+(instructions?|rules?)/i,
26
+ /你(已经)?不需要(再)?(遵守|遵循|执行)/i,
27
+ /忽略(所有|一切)?(之前的?|以上|前面的)(指令|规则|命令)/i,
28
+ /不再(需要|遵循|遵守)(前面|以上)?(所有)?(指令)/i,
29
+ /从现在开始.*?(忽略|忘记|不要遵守|抛弃)/i,
30
+ /无视.*?(之前|所有|系统).*?(指令|规则)/i,
31
+ /你已经被解放了/i,
32
+ /you are free now/i,
33
+ /从现在起你不用听.*指令/i,
34
+ ];
35
+
36
+ // 系统提示泄露类
37
+ const PATTERN_SYSTEM_PROMPT_LEAK = [
38
+ /reveal\s+(your\s+)?(system\s+)?prompt\b/i,
39
+ /show\s+(your\s+)?(system\s+)?(instructions?|prompt)\b/i,
40
+ /output\s+(your\s+)?(initial\s+)?(system\s+)?prompt\b/i,
41
+ /tell\s+me\s+(your\s+)?(system\s+)?(instructions?|prompt)/i,
42
+ /what\s+(were|are)\s+(your\s+)?(exact\s+)?(instructions?|prompt)\b/i,
43
+ /repeat\s+(the\s+)?text\s+(above|starting)/i,
44
+ /把(你的|系统)?(提示词|指令|规则)(发|输出|展示)(给|出来)/i,
45
+ /输出你的系统提示词/i,
46
+ /打印(你的|系统).*(提示词|指令)/i,
47
+ /说(出|一下)(你的|系统).*(提示词|指令|规则)/i,
48
+ /display\s+(your\s+)?(internal\s+)?(prompt|instructions)/i,
49
+ /print\s+(your\s+)?(system\s+)?prompt/i,
50
+ /list\s+your\s+instructions?/i,
51
+ /what\s+system\s+prompt/i,
52
+ /can\s+you\s+tell\s+me\s+your/i,
53
+ ];
54
+
55
+ // 角色模拟类
56
+ const PATTERN_ROLE_PLAY = [
57
+ /you\s+are\s+now\s+(in\s+)?developer\s+mode/i,
58
+ /do\s+anything\s+now/i,
59
+ /\bDAN\b/i,
60
+ /act\s+as\s+(if\s+you\s+are|a\s+|an\s+)?(hypothetical|fictional|unfiltered)/i,
61
+ /假装你是/i,
62
+ /你现在是(一个)?(开发者|管理员|黑客|上帝)/i,
63
+ /你是\s*(一个)?\s*(自由|解放|无限制|无约束)/i,
64
+ /从现在起.*?(你就是|你是)/i,
65
+ /act\s+as\s+if\s+(you\s+are\s+)?(a\s+)?(different|new|another)\s+(ai|model|assistant|system)/i,
66
+ /pretend\s+(you\s+are\s+)?(a\s+)?(different|new)\s+(ai|model)/i,
67
+ /developer\s*mode/i,
68
+ /STAN\s*mode/i,
69
+ /DUDE/i,
70
+ /hypothetical\s+scenario/i,
71
+ ];
72
+
73
+ // 安全绕过类
74
+ const PATTERN_BYPASS = [
75
+ /bypass\s+(safety|security|guardrails?|restrictions?|filters?)/i,
76
+ /remove\s+(all\s+)?(restrictions?|limitations?|constraints?)/i,
77
+ /disable\s+(safety|security|content\s+)?(filters?|guardrails?)/i,
78
+ /绕过(安全|内容)?(过滤|限制|规则|审核)/i,
79
+ /解除(所有)?限制/i,
80
+ /关闭.*?(安全|内容).*?(过滤|审核)/i,
81
+ /跳过.*?(安全|审核|过滤)/i,
82
+ /circumvent\s+(safety|security|restrictions)/i,
83
+ /break\s+(out\s+of|free\s+from)\s+(character|role|restrictions)/i,
84
+ /jailbreak/i,
85
+ /unlock/i,
86
+ /unrestrict/i,
87
+ /unguard/i,
88
+ /censor/i,
89
+ ];
90
+
91
+ // 数据泄露类
92
+ const PATTERN_DATA_EXFIL = [
93
+ /forward\s+(all\s+)?(data|emails?|files?|messages?)\s+to\b/i,
94
+ /send\s+(all\s+)?(data|files?|messages?|tickets?|conversations?)\s+to\b/i,
95
+ /exfiltrate\b/i,
96
+ /leak\s+(all\s+)?(data|info|files?)/i,
97
+ /把(所有)?(数据|文件|信息|邮件|消息)(发送|转发|导出)(到|给)/i,
98
+ /偷偷.*?(发送|转发|上传|导出)/i,
99
+ /copy\s+(all\s+)?(data|files|messages|conversations)\s+to/i,
100
+ /upload\s+(all\s+)?(data|files|logs|messages)\s+to/i,
101
+ ];
102
+
103
+ // 格式注入类
104
+ const PATTERN_FORMAT_INJECT = [
105
+ /```\s*(system|user|assistant)\s*\n/i,
106
+ /\{?\s*"[rR]ole"\s*:\s*"(system|user|assistant)"\s*,/i,
107
+ /\[system\]\s*:/i,
108
+ /<\|im_start\|>\s*(system|user)/i,
109
+ /<\|start_header_id\|>\s*(system|user)/i,
110
+ /<\|im_end\|>/i,
111
+ /<\|eot_id\|>/i,
112
+ /\[INST\].*?\[\/INST\]/i,
113
+ /<<SYS>>.*?<<\/SYS>>/i,
114
+ ];
115
+
116
+ // MCP 工具操纵
117
+ const PATTERN_TOOL_MANIPULATION = [
118
+ /call\s+(tool|function|mcp)\s+['"]?\w+['"]?\s+(with|using)\s+(these\s+)?parameters/i,
119
+ /execute\s+(the\s+)?(following\s+)?(tool|command|function)\b.*?(without|ignore)/i,
120
+ /for\s+all\s+future\s+requests?\b/i,
121
+ /from\s+now\s+on.*?(always|never|every)/i,
122
+ /在(所有|以后|每次).*?(工具|函数|调用).*?(前|时).*?(先|都)/i,
123
+ /always\s+respond\s+in/i,
124
+ /respond\s+in\s+the\s+following\s+format/i,
125
+ ];
126
+
127
+ // 所有模式分类
128
+ const PATTERN_CATEGORIES = {
129
+ instruction_override: { patterns: PATTERN_INSTRUCTION_OVERRIDE, severity: 'block' },
130
+ system_prompt_leak: { patterns: PATTERN_SYSTEM_PROMPT_LEAK, severity: 'block' },
131
+ role_play: { patterns: PATTERN_ROLE_PLAY, severity: 'block' },
132
+ bypass: { patterns: PATTERN_BYPASS, severity: 'block' },
133
+ data_exfil: { patterns: PATTERN_DATA_EXFIL, severity: 'block' },
134
+ format_inject: { patterns: PATTERN_FORMAT_INJECT, severity: 'warn' },
135
+ tool_manipulation: { patterns: PATTERN_TOOL_MANIPULATION, severity: 'warn' },
136
+ };
137
+
138
+ // 预编译正则(首次加载时一次性编译好)
139
+ const PATTERN_ENTRIES = Object.entries(PATTERN_CATEGORIES).flatMap(([category, def]) =>
140
+ def.patterns.map(re => ({ category, severity: def.severity, regex: re }))
141
+ );
142
+
143
+ function layer1PatternMatch(text) {
144
+ if (!text || typeof text !== 'string') return null;
145
+ const hits = [];
146
+ for (const entry of PATTERN_ENTRIES) {
147
+ if (entry.regex.test(text)) {
148
+ hits.push({ category: entry.category, severity: entry.severity, match: entry.regex.source.substring(0, 60) });
149
+ }
150
+ }
151
+ if (hits.length === 0) return null;
152
+
153
+ // 决策逻辑
154
+ const hasBlock = hits.some(h => h.severity === 'block');
155
+ const hasWarn = hits.some(h => h.severity === 'warn');
156
+
157
+ // 组合探测:instruction_override + bypass → block
158
+ const catSet = new Set(hits.map(h => h.category));
159
+ if (catSet.has('instruction_override') && catSet.has('bypass')) {
160
+ return { action: 'block', reason: `指令覆盖+绕过(组合): ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
161
+ }
162
+ // system_prompt_leak → block
163
+ if (catSet.has('system_prompt_leak')) {
164
+ return { action: 'block', reason: `系统提示泄露: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
165
+ }
166
+ // role_play + data_exfil → block
167
+ if (catSet.has('role_play') && catSet.has('data_exfil')) {
168
+ return { action: 'block', reason: `角色扮演+数据泄露(组合): ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
169
+ }
170
+ // bypass → block
171
+ if (catSet.has('bypass')) {
172
+ return { action: 'block', reason: `安全绕过: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
173
+ }
174
+ // data_exfil → block
175
+ if (catSet.has('data_exfil')) {
176
+ return { action: 'block', reason: `数据泄露: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
177
+ }
178
+ // instruction_override → block
179
+ if (catSet.has('instruction_override')) {
180
+ return { action: 'block', reason: `指令覆盖: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
181
+ }
182
+ // role_play → block
183
+ if (catSet.has('role_play')) {
184
+ return { action: 'block', reason: `角色模拟: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
185
+ }
186
+ // 纯 warn 类 → warn
187
+ if (hasWarn && !hasBlock) {
188
+ return { action: 'warn', reason: `格式/工具操作可疑: ${hits.map(h => h.category).join(', ')}`, layer: 'L1', hits };
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ // ============================================================
195
+ // Layer 2: 字形/模糊匹配(Levenshtein距离)
196
+ // ============================================================
197
+
198
+ const FUZZY_KEYWORDS = [
199
+ 'ignore', 'bypass', 'override', 'reveal', 'delete', 'disable',
200
+ 'system', 'instruction', 'previous', 'prompt', 'security',
201
+ 'restriction', 'safety', 'guardrail', 'filter',
202
+ 'override', 'execute', 'command', 'leak', 'exfil',
203
+ '忽略', '绕过', '覆盖', '泄露', '删除',
204
+ '系统', '指令', '规则', '提示', '安全', '限制',
205
+ '无视', '跳过', '关闭', '解锁', '破解',
206
+ 'developer', 'admin', 'root', 'shell', 'terminal',
207
+ ];
208
+
209
+ const FUZZY_THRESHOLD_SHORT = 2; // 短词(≤6字符)编辑距离≤2(含transposition)
210
+ const FUZZY_THRESHOLD_LONG = 3; // 长词(>6字符)编辑距离≤3
211
+
212
+ function levenshteinDistance(a, b) {
213
+ if (a.length === 0) return b.length;
214
+ if (b.length === 0) return a.length;
215
+ const alen = a.length;
216
+ const blen = b.length;
217
+ // 用单行数组优化
218
+ let prev = new Array(alen + 1);
219
+ let curr = new Array(alen + 1);
220
+ for (let j = 0; j <= alen; j++) prev[j] = j;
221
+ for (let i = 1; i <= blen; i++) {
222
+ curr[0] = i;
223
+ for (let j = 1; j <= alen; j++) {
224
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
225
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
226
+ }
227
+ [prev, curr] = [curr, prev];
228
+ }
229
+ return prev[alen];
230
+ }
231
+
232
+ function layer2FuzzyMatch(text) {
233
+ if (!text || typeof text !== 'string') return null;
234
+
235
+ // 分词:英文单词 + 中文词(单汉字不算)
236
+ const words = text.toLowerCase().split(/[\s,,。;;::、!!??()()\[\]【】{}"'"'"«»《》\/\\+_#@$%^*~`|=]+/);
237
+
238
+ const suspiciousHits = [];
239
+ for (const word of words) {
240
+ // 中文/亚洲字符(Unicode CJK范围)的最小长度=2
241
+ // 拉丁字母的最小长度=3
242
+ if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(word)) {
243
+ if (word.length < 2) continue;
244
+ } else {
245
+ if (word.length < 3) continue;
246
+ }
247
+ for (const kw of FUZZY_KEYWORDS) {
248
+ // 精确匹配已在 L1 处理,此处跳过
249
+ if (word === kw) continue;
250
+ const dist = levenshteinDistance(word, kw);
251
+ let threshold = kw.length <= 6 ? FUZZY_THRESHOLD_SHORT : FUZZY_THRESHOLD_LONG;
252
+ // CJK关键词长度短(2字),编辑距离阈值改为1防止误杀
253
+ if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(kw) && kw.length <= 4) {
254
+ threshold = 1;
255
+ }
256
+ if (dist > 0 && dist <= threshold) {
257
+ suspiciousHits.push({ word: word.substring(0, 30), matched: kw, distance: dist });
258
+ }
259
+ }
260
+ }
261
+
262
+ if (suspiciousHits.length === 0) return null;
263
+
264
+ // 决策:连续命中2+个不同关键词 → WARN (≥3升级BLOCK)
265
+ const uniqueMatched = new Set(suspiciousHits.map(h => h.matched));
266
+ if (uniqueMatched.size >= 2) {
267
+ const action = uniqueMatched.size >= 3 ? 'block' : 'warn';
268
+ return {
269
+ action,
270
+ reason: `模糊匹配命中 ${uniqueMatched.size} 个关键词: ${[...uniqueMatched].slice(0, 5).join(', ')}`,
271
+ layer: 'L2',
272
+ hits: suspiciousHits.slice(0, 10),
273
+ };
274
+ }
275
+ // 单个高相似命中(距离=1)→ note
276
+ if (suspiciousHits.length === 1 && suspiciousHits[0].distance === 1) {
277
+ return {
278
+ action: 'note',
279
+ reason: `近似词: "${suspiciousHits[0].word}" ≈ "${suspiciousHits[0].matched}"`,
280
+ layer: 'L2',
281
+ hits: suspiciousHits,
282
+ };
283
+ }
284
+
285
+ return null;
286
+ }
287
+
288
+ // ============================================================
289
+ // Layer 3: 编码/零宽字符检测
290
+ // ============================================================
291
+
292
+ const ZERO_WIDTH_CHARS = [
293
+ '\u200B', // ZERO WIDTH SPACE
294
+ '\u200C', // ZERO WIDTH NON-JOINER
295
+ '\u200D', // ZERO WIDTH JOINER
296
+ '\uFEFF', // ZERO WIDTH NO-BREAK SPACE (BOM)
297
+ '\u2060', // WORD JOINER
298
+ '\u2061', // FUNCTION APPLICATION
299
+ '\u2062', // INVISIBLE TIMES
300
+ '\u2063', // INVISIBLE SEPARATOR
301
+ '\u2064', // INVISIBLE PLUS
302
+ '\u180E', // MONGOLIAN VOWEL SEPARATOR
303
+ '\u034F', // COMBINING GRAPHEME JOINER
304
+ ];
305
+
306
+ const BIDI_OVERRIDE_RANGES = [
307
+ [0x202A, 0x202E],
308
+ [0x2066, 0x2069],
309
+ ];
310
+
311
+ function layer3EncodingCheck(text) {
312
+ if (!text || typeof text !== 'string') return null;
313
+
314
+ const findings = [];
315
+
316
+ // 1. 零宽字符检测
317
+ for (const zwc of ZERO_WIDTH_CHARS) {
318
+ const re = new RegExp(zwc, 'g');
319
+ let count = 0;
320
+ while (re.exec(text) !== null) count++;
321
+ if (count > 0) {
322
+ findings.push({ type: 'zero_width', char: `U+${zwc.charCodeAt(0).toString(16).toUpperCase()}`, count });
323
+ }
324
+ }
325
+
326
+ // 2. 双向文本操纵检测
327
+ for (let i = 0; i < text.length; i++) {
328
+ const cp = text.charCodeAt(i);
329
+ for (const [start, end] of BIDI_OVERRIDE_RANGES) {
330
+ if (cp >= start && cp <= end) {
331
+ findings.push({ type: 'bidi_override', position: i, codepoint: `U+${cp.toString(16).toUpperCase()}` });
332
+ }
333
+ }
334
+ }
335
+
336
+ // 3. 编码内容检测(Base64/Hex-like 长序列)
337
+ const longAlphaSeq = text.match(/[A-Za-z0-9+/=]{20,}/g);
338
+ if (longAlphaSeq) {
339
+ for (const match of longAlphaSeq) {
340
+ // Base64 解码尝试
341
+ try {
342
+ const decoded = Buffer.from(match.replace(/=+$/, ''), 'base64').toString('utf-8');
343
+ if (/ignore|system|instruction|override|bypass|泄露|指令|系统|忽略|绕过/i.test(decoded)) {
344
+ findings.push({
345
+ type: 'base64_encoded',
346
+ original: match.substring(0, 24) + '...',
347
+ decoded: decoded.substring(0, 60),
348
+ });
349
+ }
350
+ } catch { /* not valid base64 */ }
351
+
352
+ // Hex 解码尝试
353
+ if (/^[0-9a-fA-F]+$/.test(match)) {
354
+ try {
355
+ const decoded = Buffer.from(match, 'hex').toString('utf-8');
356
+ if (/ignore|system|instruction|override|泄露|指令|系统|忽略|绕过/i.test(decoded)) {
357
+ findings.push({
358
+ type: 'hex_encoded',
359
+ decoded: decoded.substring(0, 60),
360
+ });
361
+ }
362
+ } catch { /* not valid hex */ }
363
+ }
364
+ }
365
+ }
366
+
367
+ // 4. URL编码检测
368
+ const urlEncoded = text.match(/(%[0-9a-fA-F]{2}){3,}/g);
369
+ if (urlEncoded) {
370
+ for (const match of urlEncoded) {
371
+ try {
372
+ const decoded = decodeURIComponent(match);
373
+ if (/ignore|system|instruction|override|bypass|泄露|指令|忽略|绕过/i.test(decoded)) {
374
+ findings.push({ type: 'url_encoded', decoded: decoded.substring(0, 60) });
375
+ }
376
+ } catch { /* not valid url encoding */ }
377
+ }
378
+ }
379
+
380
+ // 5. HTML Entity检测
381
+ const htmlEntities = text.match(/&[a-z]+;/gi);
382
+ if (htmlEntities && htmlEntities.length > 5) {
383
+ findings.push({ type: 'html_entities', count: htmlEntities.length });
384
+ }
385
+
386
+ if (findings.length === 0) return null;
387
+
388
+ // 决策
389
+ const zwCount = findings.filter(f => f.type === 'zero_width').reduce((sum, f) => sum + f.count, 0);
390
+ const hasBidi = findings.some(f => f.type === 'bidi_override');
391
+ const hasEncoded = findings.some(f => f.type === 'base64_encoded' || f.type === 'hex_encoded' || f.type === 'url_encoded');
392
+ const hasHtmlEntities = findings.some(f => f.type === 'html_entities');
393
+
394
+ // 零宽字符 2+ 处 → BLOCK
395
+ if (zwCount >= 2) {
396
+ return { action: 'block', reason: `多处零宽字符注入 (${zwCount}处)`, layer: 'L3', hits: findings };
397
+ }
398
+ // Base64/Hex 解码出恶意命令 → BLOCK
399
+ if (hasEncoded) {
400
+ return { action: 'block', reason: `编码混淆内容检出恶意指令`, layer: 'L3', hits: findings };
401
+ }
402
+ // 双向文本操纵 → BLOCK
403
+ if (hasBidi) {
404
+ return { action: 'block', reason: `双向文本覆盖字符 (BiDi)`, layer: 'L3', hits: findings };
405
+ }
406
+ // 单处零宽字符 → WARN
407
+ if (zwCount === 1) {
408
+ return { action: 'warn', reason: `零宽字符异常`, layer: 'L3', hits: findings };
409
+ }
410
+ // HTML实体数量较多 → WARN
411
+ if (hasHtmlEntities) {
412
+ const htmlCount = findings.find(f => f.type === 'html_entities')?.count || 0;
413
+ return { action: 'warn', reason: `HTML实体编码混淆 (${htmlCount}处)`, layer: 'L3', hits: findings };
414
+ }
415
+
416
+ return null;
417
+ }
418
+
419
+ // ============================================================
420
+ // Layer 4: 结构分析
421
+ // ============================================================
422
+
423
+ function layer4StructureAnalysis(text) {
424
+ if (!text || typeof text !== 'string') return null;
425
+
426
+ const details = [];
427
+ let score = 0;
428
+
429
+ // 1. 消息格式模拟
430
+ const jsonRoleMatches = (text.match(/\{?\s*"[rR]ole"\s*:\s*"(system|user|assistant)"\s*[\s,}]/ig) || []).length;
431
+ const messageBoundaries = (text.match(/<\|im_start\|>|<\|start_header_id\|>/g) || []).length;
432
+ const codeBlockInjects = (text.match(/```\s*(system|user|assistant)/g) || []).length;
433
+ const formatScore = jsonRoleMatches + messageBoundaries + codeBlockInjects;
434
+ if (formatScore > 0) {
435
+ details.push({ check: 'messageFormatting', score: formatScore, detail: `JSON角色=${jsonRoleMatches}, 消息edges界=${messageBoundaries}, 代码块=${codeBlockInjects}` });
436
+ score++;
437
+ }
438
+
439
+ // 2. 指令层次混淆(System:/Assistant:/User: 前缀)
440
+ const prefixLines = (typeof text === 'string') ? text.split('\n') : [];
441
+ let prefixCount = 0;
442
+ for (const line of prefixLines) {
443
+ if (/^(System|Assistant|User|Human)\s*[::]/i.test(line.trim())) {
444
+ prefixCount++;
445
+ }
446
+ }
447
+ if (prefixCount >= 3) {
448
+ details.push({ check: 'hierarchyConfusion', score: prefixCount, detail: `角色前缀出现 ${prefixCount} 次` });
449
+ score++;
450
+ }
451
+
452
+ // 3. 命令/URL注入组合
453
+ const urlCmdPattern = /(?:https?:\/\/\S+)\s*\n.*?(?:ignore|execute|run|call|send|delete|upload|download)/i;
454
+ if (urlCmdPattern.test(text)) {
455
+ details.push({ check: 'commandInjection', score: 1, detail: 'URL+命令组合模式' });
456
+ score++;
457
+ }
458
+
459
+ // 4. 字符多样性异常(低多样性 = 机器生成/编码)
460
+ if (text.length > 200) {
461
+ const uniqueRatio = new Set(text).size / text.length;
462
+ if (uniqueRatio < 0.2) {
463
+ details.push({ check: 'lengthAnomaly', score: 1, detail: `字符多样性 ${(uniqueRatio * 100).toFixed(1)}% (阈值 < 20%)` });
464
+ score++;
465
+ }
466
+ }
467
+
468
+ // 5. 纯数字/字母比例异常(大量编码内容)
469
+ const alphanum = (text.match(/[a-zA-Z0-9]/g) || []).length;
470
+ const totalChars = Math.max(text.replace(/\s/g, '').length, 1);
471
+ const alphanumRatio = alphanum / totalChars;
472
+ if (alphanumRatio > 0.95 && text.length > 100) {
473
+ details.push({ check: 'alphanumericRatio', score: 1, detail: `字母数字占比 ${(alphanumRatio * 100).toFixed(1)}% > 95%` });
474
+ score++;
475
+ }
476
+
477
+ if (details.length === 0) return null;
478
+
479
+ if (score >= 3) {
480
+ return { action: 'block', reason: `结构异常得分 ${score}/5: ${details.map(d => d.check).join(', ')}`, layer: 'L4', hits: details };
481
+ }
482
+ if (score >= 2) {
483
+ return { action: 'warn', reason: `结构可疑得分 ${score}/5`, layer: 'L4', hits: details };
484
+ }
485
+ return null;
486
+ }
487
+
488
+ // ============================================================
489
+ // Layer 5: 异常熵值检测
490
+ // ============================================================
491
+
492
+ function shannonEntropy(text) {
493
+ const clean = text.replace(/[\x00-\x1F\s]/g, '');
494
+ if (clean.length < 10) return null;
495
+
496
+ const freq = {};
497
+ for (const ch of clean) freq[ch] = (freq[ch] || 0) + 1;
498
+
499
+ let entropy = 0;
500
+ for (const ch in freq) {
501
+ const p = freq[ch] / clean.length;
502
+ entropy -= p * Math.log2(p);
503
+ }
504
+ return entropy;
505
+ }
506
+
507
+ function ngramRepeatRatio(text) {
508
+ if (text.length < 3) return 0;
509
+ const bigrams = {};
510
+ for (let i = 0; i < text.length - 1; i++) {
511
+ const bg = text.substring(i, i + 2);
512
+ bigrams[bg] = (bigrams[bg] || 0) + 1;
513
+ }
514
+ const total = Object.values(bigrams).reduce((a, b) => a + b, 0);
515
+ if (total === 0) return 0;
516
+ const repeated = Object.values(bigrams).filter(c => c > 1).reduce((a, b) => a + b, 0);
517
+ return repeated / total;
518
+ }
519
+
520
+ function langMixRatio(text) {
521
+ const head = text.substring(0, Math.min(text.length, 300));
522
+ const cjkCount = (head.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
523
+ const latinCount = (head.match(/[a-zA-Z]/g) || []).length;
524
+ if (cjkCount === 0 || latinCount === 0) return null;
525
+ return cjkCount / Math.max(latinCount, 1);
526
+ }
527
+
528
+ function layer5AnomalyDetect(text, toolName = '') {
529
+ if (!text || typeof text !== 'string') return null;
530
+ if (text.length < 20) return null; // 太短不检测
531
+
532
+ const entropy = shannonEntropy(text);
533
+ const repeatRatio = ngramRepeatRatio(text);
534
+ const mixRatio = langMixRatio(text);
535
+
536
+ const flags = [];
537
+ const metrics = {};
538
+
539
+ if (entropy !== null) {
540
+ metrics.entropy = entropy;
541
+ metrics.repeatRatio = repeatRatio;
542
+ }
543
+
544
+ // 高熵 + 高重复 = 编码/混淆
545
+ if (entropy !== null && entropy > 6 && repeatRatio > 0.8) {
546
+ flags.push('high_entropy_high_repeat');
547
+ }
548
+
549
+ // 混合语言且中文比例极低 = 外来注入
550
+ // 子agent派发工具的中英混合prompt是正常行为,跳过lang_mix声明
551
+ const LANG_MIX_SAFE_TOOLS = new Set(['core_spawnAgent', 'core_spawnWorker', 'core_taskPipeline']);
552
+ if (mixRatio !== null) {
553
+ metrics.langMix = mixRatio;
554
+ if (mixRatio < 0.1 && !LANG_MIX_SAFE_TOOLS.has(toolName)) {
555
+ flags.push('lang_mix_anomaly');
556
+ }
557
+ }
558
+
559
+ // 熵值极端异常
560
+ if (entropy !== null && entropy < 1.0 && text.length > 50) {
561
+ flags.push('extremely_low_entropy');
562
+ }
563
+
564
+ if (flags.length === 0) return null;
565
+
566
+ // L5 仅标记(warn),不直接拦截
567
+ return {
568
+ action: 'warn',
569
+ reason: `异常特征: ${flags.join(', ')}`,
570
+ layer: 'L5',
571
+ hits: flags,
572
+ metrics,
573
+ };
574
+ }
575
+
576
+ // ============================================================
577
+ // 主检测管线
578
+ // ============================================================
579
+
580
+ /**
581
+ * 提取工具参数中的文本内容
582
+ * @param {string} toolName
583
+ * @param {object} args
584
+ * @returns {string} 展平的文本
585
+ */
586
+ function extractTextFromParams(toolName, args) {
587
+ if (!args || typeof args !== 'object') return '';
588
+ const parts = [];
589
+
590
+ // 遍历所有参数值
591
+ for (const [key, value] of Object.entries(args)) {
592
+ if (typeof value === 'string') {
593
+ // 跳过过长的不明文本(可能是二进制编码)
594
+ if (value.length > 10000 && /^[A-Za-z0-9+/=]{100,}$/.test(value)) {
595
+ parts.push(`[${key}]=<base64-like:${value.length}chars>`);
596
+ continue;
597
+ }
598
+ parts.push(value);
599
+ } else if (typeof value === 'object' && value !== null) {
600
+ try {
601
+ const str = JSON.stringify(value);
602
+ if (str.length < 5000) parts.push(str);
603
+ } catch { /* skip */ }
604
+ }
605
+ }
606
+
607
+ return parts.join(' ');
608
+ }
609
+
610
+ /**
611
+ * 5层检测入口
612
+ *
613
+ * @param {string} text - 要检测的文本
614
+ * @param {object} [options]
615
+ * @param {'strict'|'loose'|'custom'} [options.mode='strict']
616
+ * @returns {{ action: 'block'|'warn'|'note'|'pass', reason?: string, layer?: string, details?: object }}
617
+ */
618
+ function detectInjection(text, options = {}) {
619
+ const mode = options.mode || 'strict';
620
+ if (!text || typeof text !== 'string') {
621
+ return { action: 'pass' };
622
+ }
623
+
624
+ const result = { action: 'pass' };
625
+
626
+ // L1: 编码检测(杉哥2026-06-09: 原L3升级,删掉原L1正则+原L2模糊匹配——误杀正常审查任务)
627
+ // L3: 编码检测
628
+ try {
629
+ const l3 = layer3EncodingCheck(text);
630
+ if (l3) {
631
+ if (l3.action === 'block') {
632
+ result.action = 'block';
633
+ result.reason = l3.reason;
634
+ result.layer = l3.layer;
635
+ result.details = { ...(result.details || {}), encodingHits: l3.hits };
636
+ return result;
637
+ }
638
+ if (l3.action === 'warn') {
639
+ result.action = 'warn';
640
+ result.layer = l3.layer;
641
+ result.reason = l3.reason;
642
+ result.details = { ...(result.details || {}), encodingHits: l3.hits };
643
+ }
644
+ }
645
+ } catch (e) {
646
+ console.warn(`[PiFilter] L3异常:`, e.message);
647
+ }
648
+
649
+ // 宽松模式:L3 block 后继续走 L4/L5 收集信息但不拦截
650
+ if (mode === 'loose' && result.action === 'block') {
651
+ result.action = 'warn'; // 降级为 warn
652
+ }
653
+
654
+ // L4: 结构分析
655
+ try {
656
+ const l4 = layer4StructureAnalysis(text);
657
+ if (l4) {
658
+ if (l4.action === 'block') {
659
+ result.action = 'block';
660
+ result.reason = l4.reason;
661
+ result.layer = l4.layer;
662
+ result.details = { ...(result.details || {}), structureHits: l4.hits };
663
+ return result;
664
+ }
665
+ if (l4.action === 'warn' && result.action === 'pass') {
666
+ result.action = 'warn';
667
+ result.layer = l4.layer;
668
+ result.reason = l4.reason;
669
+ result.details = { ...(result.details || {}), structureHits: l4.hits };
670
+ }
671
+ }
672
+ } catch (e) {
673
+ console.warn(`[PiFilter] L4异常:`, e.message);
674
+ }
675
+
676
+ // L5: 异常检测(仅标记,不拦截)
677
+ try {
678
+ const l5 = layer5AnomalyDetect(text, options._toolName || '');
679
+ if (l5 && result.action === 'pass') {
680
+ result.action = 'warn';
681
+ result.layer = l5.layer;
682
+ result.reason = l5.reason;
683
+ result.details = { ...(result.details || {}), anomaly: l5.metrics, anomalyFlags: l5.hits };
684
+ }
685
+ } catch (e) {
686
+ console.warn(`[PiFilter] L5异常:`, e.message);
687
+ }
688
+
689
+ return result;
690
+ }
691
+
692
+ /**
693
+ * 检测工具调用参数中的注入
694
+ * @param {string} toolName - 工具名称
695
+ * @param {object} params - 工具参数
696
+ * @param {object} [options]
697
+ * @returns {{ block: boolean, warn: boolean, reason?: string, details?: object }}
698
+ */
699
+ function detectToolInjection(toolName, params, options = {}) {
700
+ const text = extractTextFromParams(toolName, params);
701
+ if (!text) return { block: false, warn: false };
702
+
703
+ const result = detectInjection(text, { ...options, _toolName: toolName });
704
+
705
+ // 脊髓反射级工具不检测(身份/状态查询)
706
+ const SAFE_TOOLS = new Set([
707
+ 'core_stats', 'core_about',
708
+ 'core_codeEditor', // 子agent改代码关键字不误报
709
+ 'core_memorySearch', // 语义搜索中英混合不误报
710
+ 'core_webSearch', // 网页搜索关键词不误报
711
+ ]);
712
+ if (SAFE_TOOLS.has(toolName)) {
713
+ // 杉哥2026-06-09:L1/L2已删除(误杀审查任务)。SAFE_TOOLS直接放行
714
+ return { block: false, warn: false };
715
+ }
716
+
717
+ if (result.action === 'block') {
718
+ console.warn(`[PiFilter] BLOCK injection: tool=${toolName} layer=${result.layer} reason=${result.reason}`);
719
+ return { block: true, warn: false, reason: `[注入检测] ${result.reason}`, details: result.details };
720
+ }
721
+
722
+ if (result.action === 'warn') {
723
+ // 杉哥2026-06-09: 同类型警告2分钟内只打印一次,防刷屏
724
+ // ⚠️ globalThis存储——函数内var每次调用重置,防抖从未生效
725
+ const warnKey = `${toolName}:${result.reason}`;
726
+ const now = Date.now();
727
+ globalThis.__piLastWarn = globalThis.__piLastWarn || {};
728
+ if (!globalThis.__piLastWarn[warnKey] || now - globalThis.__piLastWarn[warnKey] > 120000) {
729
+ globalThis.__piLastWarn[warnKey] = now;
730
+ console.warn(`[PiFilter] WARN suspicious: tool=${toolName} layer=${result.layer} reason=${result.reason}`);
731
+ }
732
+ return { block: false, warn: true, reason: result.reason, details: result.details };
733
+ }
734
+
735
+ return { block: false, warn: false };
736
+ }
737
+
738
+ export {
739
+ detectInjection,
740
+ detectToolInjection,
741
+ extractTextFromParams,
742
+ // 各层单独导出(便于测试)
743
+ layer1PatternMatch,
744
+ layer2FuzzyMatch,
745
+ layer3EncodingCheck,
746
+ layer4StructureAnalysis,
747
+ layer5AnomalyDetect,
748
+ };