@bolloon/bolloon-agent 0.1.34 → 0.1.36

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 (226) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  4. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  5. package/Bolloon.md +144 -0
  6. package/Dive-into/CITATION.cff +17 -0
  7. package/Dive-into/LICENSE +25 -0
  8. package/Dive-into/README.md +598 -0
  9. package/Dive-into/README_zh.md +599 -0
  10. package/Dive-into/assets/context.png +0 -0
  11. package/Dive-into/assets/extensibility.png +0 -0
  12. package/Dive-into/assets/iteration.png +0 -0
  13. package/Dive-into/assets/layered_architecture.png +0 -0
  14. package/Dive-into/assets/main_structure.png +0 -0
  15. package/Dive-into/assets/permission.png +0 -0
  16. package/Dive-into/assets/session_compact.png +0 -0
  17. package/Dive-into/assets/subagent.png +0 -0
  18. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  19. package/README.md +1 -1
  20. package/dist/agents/p2p-chat-tools.js +6 -6
  21. package/dist/agents/permission-mode.js +115 -0
  22. package/dist/agents/pi-sdk.js +687 -25
  23. package/dist/agents/pre-tool-validator.js +194 -0
  24. package/dist/agents/workflow-pivot-loop.js +113 -12
  25. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  26. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  27. package/dist/bollharness/src/scripts/context_router.js +1 -1
  28. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  29. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  30. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  31. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  32. package/dist/bollharness-integration/context-router.js +2 -2
  33. package/dist/bollharness-integration/guard-checker.js +1 -1
  34. package/dist/bootstrap/bootstrap.js +117 -0
  35. package/dist/bootstrap/context-collector.js +312 -0
  36. package/dist/bootstrap/context-hierarchy.js +218 -0
  37. package/dist/bootstrap/lifecycle-hooks.js +171 -0
  38. package/dist/bootstrap/project-context.js +158 -0
  39. package/dist/context-compaction/auto-compact.js +144 -0
  40. package/dist/context-compaction/budget-gate.js +28 -0
  41. package/dist/context-compaction/budget-reduce.js +35 -0
  42. package/dist/context-compaction/context-collapse.js +66 -0
  43. package/dist/context-compaction/index.js +21 -0
  44. package/dist/context-compaction/microcompact.js +51 -0
  45. package/dist/context-compaction/pipeline.js +123 -0
  46. package/dist/context-compaction/snip.js +45 -0
  47. package/dist/context-compaction/token-estimator.js +35 -0
  48. package/dist/context-compaction/types.js +19 -0
  49. package/dist/heartbeat/HealthMonitor.js +3 -2
  50. package/dist/index.js +49 -2
  51. package/dist/llm/llm-judgment-client.js +32 -30
  52. package/dist/llm/pi-ai.js +125 -28
  53. package/dist/llm/system-prompt/health.js +129 -0
  54. package/dist/llm/system-prompt/registry.js +246 -0
  55. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  56. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  57. package/dist/llm/tool-manifest/bash.js +23 -0
  58. package/dist/llm/tool-manifest/create_file.js +24 -0
  59. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  60. package/dist/llm/tool-manifest/image_search.js +24 -0
  61. package/dist/llm/tool-manifest/index.js +69 -0
  62. package/dist/llm/tool-manifest/mcp.js +43 -0
  63. package/dist/llm/tool-manifest/message_compose.js +31 -0
  64. package/dist/llm/tool-manifest/places.js +83 -0
  65. package/dist/llm/tool-manifest/present_files.js +21 -0
  66. package/dist/llm/tool-manifest/recipe.js +40 -0
  67. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  68. package/dist/llm/tool-manifest/str_replace.js +27 -0
  69. package/dist/llm/tool-manifest/types.js +7 -0
  70. package/dist/llm/tool-manifest/view.js +24 -0
  71. package/dist/llm/tool-manifest/weather.js +27 -0
  72. package/dist/llm/tool-manifest/web.js +51 -0
  73. package/dist/network/p2p-direct.js +23 -0
  74. package/dist/network/source-intent-broadcaster.js +203 -0
  75. package/dist/network/source-intent.js +100 -0
  76. package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
  77. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  78. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  79. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  80. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  81. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  82. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  83. package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
  84. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  85. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  86. package/dist/security/builtin-guards.js +124 -0
  87. package/dist/security/context-router-tool.js +106 -0
  88. package/dist/security/input-scanner.js +223 -0
  89. package/dist/security/react-harness.js +143 -0
  90. package/dist/security/tool-gate.js +235 -0
  91. package/dist/utils/auto-evolve-policy.js +117 -0
  92. package/dist/utils/clamp.js +7 -0
  93. package/dist/utils/double.js +6 -0
  94. package/dist/web/client.js +3810 -3830
  95. package/dist/web/components/p2p/P2PModal.js +188 -0
  96. package/dist/web/components/p2p/index.js +264 -226
  97. package/dist/web/components/p2p/p2p-modal.js +657 -0
  98. package/dist/web/components/p2p/p2p-tools.js +248 -0
  99. package/dist/web/index.html +60 -49
  100. package/dist/web/server.js +827 -124
  101. package/dist/web/style.css +531 -249
  102. package/dist/web/ui/message-renderer.js +463 -0
  103. package/dist/web/ui/step-timeline.js +375 -0
  104. package/lefthook.yml +33 -0
  105. package/package.json +3 -2
  106. package/scripts/auto-evolve-loop.ts +481 -0
  107. package/scripts/auto-evolve-oneshot.sh +155 -0
  108. package/scripts/auto-evolve-snapshot.sh +136 -0
  109. package/scripts/build-web.ts +35 -1
  110. package/scripts/detect-schema-changes.sh +48 -0
  111. package/scripts/diff-reviewer.ts +159 -0
  112. package/scripts/validate-system-prompt.ts +142 -0
  113. package/scripts/weekly-report.ts +364 -0
  114. package/src/agents/p2p-chat-tools.ts +6 -6
  115. package/src/agents/permission-mode.ts +127 -0
  116. package/src/agents/pi-sdk.ts +741 -30
  117. package/src/agents/pre-tool-validator.ts +213 -0
  118. package/src/agents/workflow-pivot-loop.ts +110 -19
  119. package/src/bollharness/CLAUDE.md +1 -1
  120. package/src/bollharness/README.md +2 -2
  121. package/src/bollharness/README.zh-CN.md +2 -2
  122. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  123. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  124. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  125. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  126. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  127. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  128. package/src/bollharness/src/scripts/context_router.ts +1 -1
  129. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  130. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  131. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  132. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  133. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  134. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  135. package/src/bollharness-integration/context-router.ts +2 -2
  136. package/src/bollharness-integration/guard-checker.ts +1 -1
  137. package/src/bootstrap/bootstrap.ts +135 -0
  138. package/src/bootstrap/context-collector.ts +371 -0
  139. package/src/bootstrap/context-hierarchy.ts +283 -0
  140. package/src/bootstrap/lifecycle-hooks.ts +289 -0
  141. package/src/bootstrap/project-context.ts +171 -0
  142. package/src/context-compaction/auto-compact.ts +153 -0
  143. package/src/context-compaction/budget-gate.ts +32 -0
  144. package/src/context-compaction/budget-reduce.ts +37 -0
  145. package/src/context-compaction/context-collapse.ts +72 -0
  146. package/src/context-compaction/index.ts +24 -0
  147. package/src/context-compaction/microcompact.ts +54 -0
  148. package/src/context-compaction/pipeline.ts +133 -0
  149. package/src/context-compaction/snip.ts +51 -0
  150. package/src/context-compaction/token-estimator.ts +36 -0
  151. package/src/context-compaction/types.ts +99 -0
  152. package/src/heartbeat/HealthMonitor.ts +3 -2
  153. package/src/index.ts +47 -2
  154. package/src/llm/llm-judgment-client.ts +36 -35
  155. package/src/llm/pi-ai.ts +135 -29
  156. package/src/llm/system-prompt/health.ts +159 -0
  157. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  158. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  159. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  160. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  161. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  162. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  163. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  164. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  165. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  166. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  167. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  168. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  169. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  170. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  171. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  172. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  173. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  174. package/src/llm/system-prompt/layers/role/security.md +15 -0
  175. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  176. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  177. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  178. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  179. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  180. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  181. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  182. package/src/llm/system-prompt/registry.ts +325 -0
  183. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  184. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  185. package/src/llm/tool-manifest/bash.ts +25 -0
  186. package/src/llm/tool-manifest/create_file.ts +26 -0
  187. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  188. package/src/llm/tool-manifest/image_search.ts +26 -0
  189. package/src/llm/tool-manifest/index.ts +88 -0
  190. package/src/llm/tool-manifest/mcp.ts +46 -0
  191. package/src/llm/tool-manifest/message_compose.ts +33 -0
  192. package/src/llm/tool-manifest/places.ts +86 -0
  193. package/src/llm/tool-manifest/present_files.ts +23 -0
  194. package/src/llm/tool-manifest/recipe.ts +42 -0
  195. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  196. package/src/llm/tool-manifest/str_replace.ts +29 -0
  197. package/src/llm/tool-manifest/types.ts +52 -0
  198. package/src/llm/tool-manifest/view.ts +26 -0
  199. package/src/llm/tool-manifest/weather.ts +29 -0
  200. package/src/llm/tool-manifest/web.ts +54 -0
  201. package/src/network/p2p-direct.ts +22 -0
  202. package/src/network/source-intent-broadcaster.ts +242 -0
  203. package/src/network/source-intent.ts +167 -0
  204. package/src/security/builtin-guards.ts +162 -0
  205. package/src/security/context-router-tool.ts +122 -0
  206. package/src/security/input-scanner.ts +287 -0
  207. package/src/security/react-harness.ts +177 -0
  208. package/src/security/tool-gate.ts +294 -0
  209. package/src/utils/auto-evolve-policy.ts +138 -0
  210. package/src/utils/clamp.ts +5 -0
  211. package/src/web/client.js +105 -2187
  212. package/src/web/client.ts +4326 -0
  213. package/src/web/index.html +60 -49
  214. package/src/web/server.ts +894 -148
  215. package/src/web/style.css +531 -249
  216. package/src/web/ui/message-renderer.ts +540 -0
  217. package/src/web/ui/step-timeline.ts +394 -0
  218. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  219. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  220. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  221. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  222. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  223. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  224. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
  225. package/test-results/.last-run.json +6 -0
  226. package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * input-scanner.ts — P-Action 3 Untrusted-input scanner
3
+ *
4
+ * 4 仓库共识 (deusyu / walkinglabs / 马书 / AHE):
5
+ * - deusyu: 7+ 层防 prompt injection, 上游有 Unicode NFKC / 显式字符范围
6
+ * - walkinglabs bootstrap stage 4: 安全敏感代码只在信任边界后才加载
7
+ * - 马书 ch17b: 7 层防注入 (Unicode NFKC + \\p{Cf}/\\p{Co}/\\p{Cn} + 显式字符范围 +
8
+ * 迭代 10 轮上限 + XML 转义 + 29 个来源标签)
9
+ * - AHE: E2B sandbox + input scanner 拦截跨机器不可信输入
10
+ *
11
+ * bolloon 定位 (cross-check 选边):
12
+ * - **library 函数**, 不做 middleware (跟 tool-gate.ts 8 kinds 一致)
13
+ * - 默认 silence-on-fail + log-only; BOLLOON_INPUT_SCAN=block 才 hard-reject
14
+ * - 保守 PII: P2P 跳 PII+prompt-injection; judgment 摄入只跳 prompt-injection
15
+ * (judgment 决策文本含人话是合法的)
16
+ *
17
+ * 4 层 verdict (你已选 "四层 + fail-open 隐式"):
18
+ * - pass — 无威胁
19
+ * - low — 可疑特征 (whitespace 异常等), 静默
20
+ * - warn — PII 2 个以上 / 提示注入模式但需验证, log + tag (默认)
21
+ * - fail-safe — 扫描器内部异常, log (隐式, 失败时安全侧)
22
+ * - block — CVE-级 prompt injection / 明显密钥, 阻断 (需 BOLLOON_INPUT_SCAN=block)
23
+ */
24
+
25
+ import * as fs from 'fs/promises';
26
+ import * as os from 'os';
27
+ import * as path from 'path';
28
+
29
+ export type Verdict = 'pass' | 'low' | 'warn' | 'fail-safe' | 'block';
30
+
31
+ export type ThreatKind =
32
+ | 'prompt-injection-classic' // "ignore previous instructions" / "system: ..." 等
33
+ | 'prompt-injection-unicode' // Unicode 隐藏字符 (\\p{Cf}/\\p{Co}/\\p{Cn} 异常密度)
34
+ | 'prompt-injection-jailbreak' // "DAN" / role-play override 等
35
+ | 'pii-email' // 含邮箱
36
+ | 'pii-phone' // 含手机号
37
+ | 'pii-creditcard' // 含信用卡 (Luhn 校验)
38
+ | 'pii-privatekey' // 含 RSA / EC 私钥头
39
+ | 'secret-aws' // AWS access key
40
+ | 'secret-github' // GitHub PAT
41
+ | 'whitespace-anomaly' // 大量零宽字符 / 多空格
42
+ | 'oversize'; // 输入超长
43
+
44
+ export interface ThreatHit {
45
+ kind: ThreatKind;
46
+ /** 命中位置 (字节 offset) */
47
+ offset?: number;
48
+ /** 简短证据 (前 60 字符) */
49
+ evidence?: string;
50
+ }
51
+
52
+ export interface ScanResult {
53
+ verdict: Verdict;
54
+ threats: ThreatHit[];
55
+ /** 输入源 (供 audit log 归因) */
56
+ source: 'p2p' | 'judgment' | 'other';
57
+ /** 扫描耗时 ms */
58
+ durationMs: number;
59
+ /** 扫描器是否自身异常 (true = 静默 pass, 写 audit 'fail-safe') */
60
+ scannerFailed: boolean;
61
+ }
62
+
63
+ export interface ScanOptions {
64
+ source: 'p2p' | 'judgment' | 'other';
65
+ /** 是否扫描 PII (默认: p2p=true, judgment=false) */
66
+ scanPii?: boolean;
67
+ /** 输入字节上限 (超过直接 verdict=block, threats=oversize) */
68
+ maxBytes?: number;
69
+ }
70
+
71
+ const DEFAULT_MAX_BYTES = 1_000_000; // 1MB
72
+
73
+ // ============================================================
74
+ // 规则
75
+ // ============================================================
76
+
77
+ const PI_PATTERNS: Array<{ kind: ThreatKind; re: RegExp }> = [
78
+ { kind: 'pii-email', re: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
79
+ { kind: 'pii-phone', re: /(?<!\d)(?:\+?\d{1,3}[-.\s]?)?\(?\d{3,4}\)?[-.\s]?\d{3,4}[-.\s]?\d{4}(?!\d)/g },
80
+ ];
81
+
82
+ const SECRET_PATTERNS: Array<{ kind: ThreatKind; re: RegExp }> = [
83
+ { kind: 'pii-privatekey', re: /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----/g },
84
+ { kind: 'secret-aws', re: /AKIA[0-9A-Z]{16}/g },
85
+ { kind: 'secret-github', re: /ghp_[A-Za-z0-9]{36}/g },
86
+ ];
87
+
88
+ // 经典 prompt injection 模式 (高确信度 → block)
89
+ const INJECTION_CLASSIC: RegExp[] = [
90
+ /ignore\s+(?:all\s+)?previous\s+instructions/i,
91
+ /disregard\s+(?:all\s+)?(?:prior|previous)\s+(?:instructions|rules)/i,
92
+ /forget\s+(?:everything|all)\s+(?:above|before)/i,
93
+ /you\s+are\s+now\s+(?:a|an)\s+(?:unrestricted|jailbroken|DAN)/i,
94
+ /new\s+system\s*:\s*you\s+are/i,
95
+ ];
96
+
97
+ // 软提示注入 (需结合上下文验证 → warn, 不直接 block)
98
+ const INJECTION_SOFT: RegExp[] = [
99
+ /\bact\s+as\s+(?:a|an)\s+(?:developer\s+mode|root\s+admin)/i,
100
+ /\boverride\s+(?:safety|content)\s+(?:filter|policy)/i,
101
+ /\bDAN\s+mode\b/i,
102
+ ];
103
+
104
+ // Unicode 隐藏字符密度检测 (\\p{Cf}=Format, \\p{Co}=Private Use, \\p{Cn}=Unassigned)
105
+ const HIDDEN_CHAR_RE = /[\p{Cf}\p{Co}\p{Cn}]/gu;
106
+ const WHITESPACE_ANOMALY_RE = /[​-‏]{5,}/g;
107
+
108
+ // 信用卡 (Luhn 简化)
109
+ function luhnValid(num: string): boolean {
110
+ const digits = num.replace(/\D/g, '');
111
+ if (digits.length < 13 || digits.length > 19) return false;
112
+ let sum = 0;
113
+ let alt = false;
114
+ for (let i = digits.length - 1; i >= 0; i--) {
115
+ let n = parseInt(digits[i], 10);
116
+ if (alt) {
117
+ n *= 2;
118
+ if (n > 9) n -= 9;
119
+ }
120
+ sum += n;
121
+ alt = !alt;
122
+ }
123
+ return sum % 10 === 0;
124
+ }
125
+
126
+ // ============================================================
127
+ // 主入口
128
+ // ============================================================
129
+
130
+ export function scanInput(input: string | Buffer, opts: ScanOptions): ScanResult {
131
+ const start = Date.now();
132
+ const source = opts.source;
133
+ const scanPii = opts.scanPii ?? (source === 'p2p'); // 默认 P2P=true, judgment=false
134
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
135
+ const buf = typeof input === 'string' ? Buffer.from(input, 'utf-8') : input;
136
+
137
+ try {
138
+ // 0. 长度超限 → 直接 block
139
+ if (buf.length > maxBytes) {
140
+ return {
141
+ verdict: 'block',
142
+ threats: [{ kind: 'oversize', evidence: `${buf.length} > ${maxBytes}` }],
143
+ source,
144
+ durationMs: Date.now() - start,
145
+ scannerFailed: false,
146
+ };
147
+ }
148
+
149
+ const text = buf.toString('utf-8');
150
+ const threats: ThreatHit[] = [];
151
+
152
+ // 1. 经典 prompt injection → block
153
+ for (const re of INJECTION_CLASSIC) {
154
+ const m = text.match(re);
155
+ if (m) {
156
+ threats.push({ kind: 'prompt-injection-classic', offset: m.index, evidence: m[0].substring(0, 60) });
157
+ }
158
+ }
159
+
160
+ // 2. 软注入 → warn
161
+ for (const re of INJECTION_SOFT) {
162
+ const m = text.match(re);
163
+ if (m) {
164
+ threats.push({ kind: 'prompt-injection-jailbreak', offset: m.index, evidence: m[0].substring(0, 60) });
165
+ }
166
+ }
167
+
168
+ // 3. Unicode 隐藏字符 → warn (高密度时)
169
+ const hiddenMatches = [...text.matchAll(HIDDEN_CHAR_RE)];
170
+ if (hiddenMatches.length > 5) {
171
+ threats.push({ kind: 'prompt-injection-unicode', evidence: `${hiddenMatches.length} hidden chars` });
172
+ }
173
+
174
+ // 4. whitespace 异常 (5+ 连续零宽) → low
175
+ if (WHITESPACE_ANOMALY_RE.test(text)) {
176
+ threats.push({ kind: 'whitespace-anomaly' });
177
+ }
178
+
179
+ // 5. PII (仅 p2p / other, judgment 跳过)
180
+ if (scanPii) {
181
+ for (const { kind, re } of PI_PATTERNS) {
182
+ const matches = [...text.matchAll(re)];
183
+ for (const m of matches) {
184
+ threats.push({ kind, offset: m.index, evidence: m[0].substring(0, 60) });
185
+ }
186
+ }
187
+ // 信用卡需要 Luhn
188
+ const ccRe = /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g;
189
+ const ccMatches = [...text.matchAll(ccRe)];
190
+ for (const m of ccMatches) {
191
+ if (luhnValid(m[0])) {
192
+ threats.push({ kind: 'pii-creditcard', offset: m.index, evidence: m[0].substring(0, 60) });
193
+ }
194
+ }
195
+ }
196
+
197
+ // 6. 密钥 (无论 source)
198
+ for (const { kind, re } of SECRET_PATTERNS) {
199
+ const matches = [...text.matchAll(re)];
200
+ for (const m of matches) {
201
+ threats.push({ kind, offset: m.index, evidence: m[0].substring(0, 60) });
202
+ }
203
+ }
204
+
205
+ // 聚合 verdict
206
+ const verdict = aggregateVerdict(threats);
207
+ return {
208
+ verdict,
209
+ threats,
210
+ source,
211
+ durationMs: Date.now() - start,
212
+ scannerFailed: false,
213
+ };
214
+ } catch (err) {
215
+ // 扫描器自身异常 → fail-safe (隐式)
216
+ console.warn('[input-scanner] scanInput failed (silent, fail-safe):', err);
217
+ return {
218
+ verdict: 'pass', // 失败时安全侧 (不阻断)
219
+ threats: [],
220
+ source,
221
+ durationMs: Date.now() - start,
222
+ scannerFailed: true,
223
+ };
224
+ }
225
+ }
226
+
227
+ function aggregateVerdict(threats: ThreatHit[]): Verdict {
228
+ if (threats.length === 0) return 'pass';
229
+ if (threats.some((t) => t.kind === 'prompt-injection-classic' || t.kind === 'pii-privatekey' || t.kind === 'secret-aws' || t.kind === 'secret-github' || t.kind === 'pii-creditcard' || t.kind === 'oversize')) {
230
+ return 'block';
231
+ }
232
+ // prompt-injection-unicode + jailbreak + pii 多数 → warn
233
+ const warnCount = threats.filter((t) =>
234
+ t.kind === 'prompt-injection-unicode' ||
235
+ t.kind === 'prompt-injection-jailbreak' ||
236
+ t.kind.startsWith('pii-') ||
237
+ t.kind.startsWith('secret-')
238
+ ).length;
239
+ if (warnCount >= 2) return 'warn';
240
+ if (warnCount >= 1) return 'warn';
241
+ return 'low';
242
+ }
243
+
244
+ // ============================================================
245
+ // Audit log
246
+ // ============================================================
247
+
248
+ const AUDIT_PATH = () => path.join(
249
+ process.env.HOME || os.homedir() || '/tmp',
250
+ '.bolloon', 'sessions', 'input-scan-audit.jsonl'
251
+ );
252
+
253
+ export async function writeScanAudit(result: ScanResult, context?: Record<string, unknown>): Promise<void> {
254
+ // 只记 verdict 异常 + 失败, 减少噪音
255
+ if (result.verdict === 'pass' && !result.scannerFailed) return;
256
+ try {
257
+ await fs.mkdir(path.dirname(AUDIT_PATH()), { recursive: true });
258
+ const entry = {
259
+ ts: new Date().toISOString(),
260
+ source: result.source,
261
+ verdict: result.verdict,
262
+ threatCount: result.threats.length,
263
+ threatKinds: [...new Set(result.threats.map((t) => t.kind))],
264
+ scannerFailed: result.scannerFailed,
265
+ durationMs: result.durationMs,
266
+ context: context ?? null,
267
+ };
268
+ await fs.appendFile(AUDIT_PATH(), JSON.stringify(entry) + '\n', 'utf-8');
269
+ } catch (err) {
270
+ console.warn('[input-scanner] writeScanAudit failed (silent):', err);
271
+ }
272
+ }
273
+
274
+ /** BOLLOON_INPUT_SCAN=block 时, block 才是真阻断; 否则全默认 pass */
275
+ export function shouldHardBlock(result: ScanResult): boolean {
276
+ if (result.scannerFailed) return false; // 失败永不禁
277
+ if (result.verdict !== 'block') return false;
278
+ return process.env.BOLLOON_INPUT_SCAN === 'block';
279
+ }
280
+
281
+ // ============================================================
282
+ // 测试钩子
283
+ // ============================================================
284
+
285
+ export function _resetScannerForTest(): void {
286
+ // scanner 是 pure, 保留 API 一致
287
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * React Harness — ReAct 循环的 hook 集中调度
3
+ *
4
+ * 包装:
5
+ * - bollharness-integration (BollharnessHooks + GateStateMachine)
6
+ * - tool-gate (8 道安全 gate)
7
+ * - builtin-guards (4 个内置 guard, 跟 gate 互补)
8
+ *
9
+ * 接入点 (pi-sdk.ts runReActLoop):
10
+ * - preToolCall: 调 tool.execute 前 (8-gate + guards)
11
+ * - postToolCall: tool 返 output 后 (output gate + secret leak)
12
+ * - sessionStart: ReAct 循环入口 (harness sessionStart)
13
+ * - sessionEnd: 循环结束 (harness sessionEnd + archive)
14
+ *
15
+ * 设计原则:
16
+ * - fail-open: 任何 hook 自身挂掉 = pass, 不阻塞主对话
17
+ * - 8-gate + 4-guard 全部 disabled 时 = 旧行为 (跟加 harness 之前一样)
18
+ * - all 结果记录到 harness session archive (供 UI 审计)
19
+ */
20
+
21
+ import {
22
+ createBollharnessIntegration,
23
+ type BollharnessIntegration,
24
+ type IntegrationResult,
25
+ } from '../bollharness-integration/integration.js';
26
+ import {
27
+ runToolGates,
28
+ runOutputGate,
29
+ type ToolGateCheckResult,
30
+ } from './tool-gate.js';
31
+ import { routeContext } from './context-router-tool.js';
32
+
33
+ export interface ReactHarnessOptions {
34
+ /** 是否启用 harness (BollharnessIntegration + Hooks); 关闭后只剩 tool-gate */
35
+ harnessEnabled?: boolean;
36
+ /** 是否启用 8-gate 安全检查; 关闭后只剩 builtin-guards */
37
+ gateEnabled?: boolean;
38
+ /** 单轮最多 tool 调用数 (默认 5) */
39
+ maxToolCallsPerTurn?: number;
40
+ }
41
+
42
+ export interface PreToolCallResult {
43
+ /** true = 允许执行, false = 拒绝 */
44
+ allowed: boolean;
45
+ reason?: string;
46
+ /** 调试用: 各 gate 结果 */
47
+ details: ToolGateCheckResult;
48
+ }
49
+
50
+ export interface PostToolCallResult {
51
+ allowed: boolean;
52
+ reason?: string;
53
+ details: ToolGateCheckResult;
54
+ }
55
+
56
+ export class ReactHarness {
57
+ private integration: BollharnessIntegration | null = null;
58
+ private opts: Required<ReactHarnessOptions>;
59
+ private recentCalls: Array<{ tool: string; ts: number }> = [];
60
+ private toolCallCountInTurn = 0;
61
+
62
+ constructor(options: ReactHarnessOptions = {}) {
63
+ this.opts = {
64
+ harnessEnabled: options.harnessEnabled ?? true,
65
+ gateEnabled: options.gateEnabled ?? true,
66
+ maxToolCallsPerTurn: options.maxToolCallsPerTurn ?? 5,
67
+ };
68
+ if (this.opts.harnessEnabled) {
69
+ try {
70
+ this.integration = createBollharnessIntegration({
71
+ enabled: true,
72
+ guardsEnabled: true,
73
+ contextEnabled: true,
74
+ skillsEnabled: true,
75
+ gatesEnabled: true,
76
+ });
77
+ } catch (err) {
78
+ console.warn('[react-harness] bollharness init failed (non-fatal):', err);
79
+ this.integration = null;
80
+ }
81
+ }
82
+ }
83
+
84
+ /** 每次 ReAct 循环开始调一次 (重置 turn 计数 + 触发 harness sessionStart) */
85
+ async onSessionStart(channelId?: string): Promise<void> {
86
+ this.toolCallCountInTurn = 0;
87
+ this.recentCalls = [];
88
+ if (!this.integration) return;
89
+ try {
90
+ // BollharnessIntegration 自带 session 状态, 简单 log
91
+ console.log(`[react-harness] session start, channel=${channelId ?? 'n/a'}, currentGate=${this.integration.getCurrentGate()}`);
92
+ } catch (err) {
93
+ console.warn('[react-harness] sessionStart failed (non-fatal):', err);
94
+ }
95
+ }
96
+
97
+ /** 调 tool 前的 hook (8-gate + builtin-guards + context-router advisory) */
98
+ async preToolCall(tool: string, args: Record<string, unknown>, channelId?: string): Promise<PreToolCallResult> {
99
+ if (!this.opts.gateEnabled) {
100
+ return { allowed: true, details: { allowed: true, details: [] } };
101
+ }
102
+ try {
103
+ const result = runToolGates({
104
+ tool,
105
+ args,
106
+ channelId,
107
+ toolCallCountInTurn: this.toolCallCountInTurn,
108
+ recentCalls: this.recentCalls,
109
+ });
110
+ if (result.allowed) {
111
+ this.toolCallCountInTurn++;
112
+ this.recentCalls.push({ tool, ts: Date.now() });
113
+
114
+ // Context router: advisory 路由 (不阻断, 仅返回 hint 供 pi-sdk 拼到 LLM 上下文)
115
+ // 失败静默, router 挂掉 = 不给 hint
116
+ try {
117
+ const route = routeContext({ channelId, predictedTool: tool });
118
+ (this as any).lastRouteHint = route;
119
+ } catch (err) {
120
+ console.warn('[react-harness] routeContext failed (non-fatal):', err);
121
+ }
122
+ }
123
+ return { allowed: result.allowed, reason: result.reason, details: result };
124
+ } catch (err) {
125
+ console.warn('[react-harness] preToolCall failed (fail-open):', err);
126
+ return { allowed: true, details: { allowed: true, details: [] } };
127
+ }
128
+ }
129
+
130
+ /** 取最近一次 router 算出的 hint (供 pi-sdk 拼到 messageHistory 工具结果位) */
131
+ getLastRouteHint(): { systemAddition: string; toolPreamble: string; reason: string } | null {
132
+ return (this as any).lastRouteHint ?? null;
133
+ }
134
+
135
+ /** 清空最近 hint (每轮 ReAct 循环结束重置) */
136
+ clearRouteHint(): void {
137
+ (this as any).lastRouteHint = null;
138
+ }
139
+
140
+ /** 调 tool 后的 hook (审查 output) */
141
+ async postToolCall(tool: string, output: string, channelId?: string): Promise<PostToolCallResult> {
142
+ if (!this.opts.gateEnabled) {
143
+ return { allowed: true, details: { allowed: true, details: [] } };
144
+ }
145
+ try {
146
+ const result = runOutputGate(output);
147
+ if (!result.allowed) {
148
+ return { allowed: false, reason: result.reason, details: result };
149
+ }
150
+ return { allowed: true, details: result };
151
+ } catch (err) {
152
+ console.warn('[react-harness] postToolCall failed (fail-open):', err);
153
+ return { allowed: true, details: { allowed: true, details: [] } };
154
+ }
155
+ }
156
+
157
+ /** ReAct 循环结束 */
158
+ async onSessionEnd(): Promise<void> {
159
+ if (!this.integration) return;
160
+ try {
161
+ // 归档: 当前无 operationLog (那是另接的), 仅 log 状态
162
+ const gate = this.integration.getCurrentGate();
163
+ console.log(`[react-harness] session end, finalGate=${gate}, toolCallsThisTurn=${this.toolCallCountInTurn}`);
164
+ } catch (err) {
165
+ console.warn('[react-harness] sessionEnd failed (non-fatal):', err);
166
+ }
167
+ }
168
+
169
+ /** 暴露给 UI 调试 (harness 状态) */
170
+ getHarnessSnapshot(): { integration: boolean; gateEnabled: boolean; currentGate: number } {
171
+ return {
172
+ integration: this.integration !== null,
173
+ gateEnabled: this.opts.gateEnabled,
174
+ currentGate: this.integration ? this.integration.getCurrentGate() : 0,
175
+ };
176
+ }
177
+ }