@yan162/changewayguard 6.8.25

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 (285) hide show
  1. package/LICENSE +21 -0
  2. package/OpenClaw-linux_Mac-Guide-zh.md +89 -0
  3. package/dashboard-dist/api/122.index.js +95 -0
  4. package/dashboard-dist/api/122.index.js.map +1 -0
  5. package/dashboard-dist/api/143.index.js +2734 -0
  6. package/dashboard-dist/api/143.index.js.map +1 -0
  7. package/dashboard-dist/api/154.index.js +4151 -0
  8. package/dashboard-dist/api/154.index.js.map +1 -0
  9. package/dashboard-dist/api/173.index.js +24112 -0
  10. package/dashboard-dist/api/173.index.js.map +1 -0
  11. package/dashboard-dist/api/217.index.js +44 -0
  12. package/dashboard-dist/api/217.index.js.map +1 -0
  13. package/dashboard-dist/api/222.index.js +90 -0
  14. package/dashboard-dist/api/222.index.js.map +1 -0
  15. package/dashboard-dist/api/280.index.js +213 -0
  16. package/dashboard-dist/api/280.index.js.map +1 -0
  17. package/dashboard-dist/api/369.index.js +115 -0
  18. package/dashboard-dist/api/369.index.js.map +1 -0
  19. package/dashboard-dist/api/374.index.js +1896 -0
  20. package/dashboard-dist/api/374.index.js.map +1 -0
  21. package/dashboard-dist/api/424.index.js +135 -0
  22. package/dashboard-dist/api/424.index.js.map +1 -0
  23. package/dashboard-dist/api/445.index.js +3562 -0
  24. package/dashboard-dist/api/445.index.js.map +1 -0
  25. package/dashboard-dist/api/555.index.js +496 -0
  26. package/dashboard-dist/api/555.index.js.map +1 -0
  27. package/dashboard-dist/api/573.index.js +806 -0
  28. package/dashboard-dist/api/573.index.js.map +1 -0
  29. package/dashboard-dist/api/580.index.js +1420 -0
  30. package/dashboard-dist/api/580.index.js.map +1 -0
  31. package/dashboard-dist/api/581.index.js +67 -0
  32. package/dashboard-dist/api/581.index.js.map +1 -0
  33. package/dashboard-dist/api/598.index.js +328 -0
  34. package/dashboard-dist/api/598.index.js.map +1 -0
  35. package/dashboard-dist/api/720.index.js +105 -0
  36. package/dashboard-dist/api/720.index.js.map +1 -0
  37. package/dashboard-dist/api/744.index.js +333 -0
  38. package/dashboard-dist/api/744.index.js.map +1 -0
  39. package/dashboard-dist/api/818.index.js +374 -0
  40. package/dashboard-dist/api/818.index.js.map +1 -0
  41. package/dashboard-dist/api/831.index.js +99 -0
  42. package/dashboard-dist/api/831.index.js.map +1 -0
  43. package/dashboard-dist/api/84.index.js +64 -0
  44. package/dashboard-dist/api/84.index.js.map +1 -0
  45. package/dashboard-dist/api/900.index.js +81 -0
  46. package/dashboard-dist/api/900.index.js.map +1 -0
  47. package/dashboard-dist/api/917.index.js +88 -0
  48. package/dashboard-dist/api/917.index.js.map +1 -0
  49. package/dashboard-dist/api/927.index.js +4250 -0
  50. package/dashboard-dist/api/927.index.js.map +1 -0
  51. package/dashboard-dist/api/948.index.js +64 -0
  52. package/dashboard-dist/api/948.index.js.map +1 -0
  53. package/dashboard-dist/api/982.index.js +67 -0
  54. package/dashboard-dist/api/982.index.js.map +1 -0
  55. package/dashboard-dist/api/99.index.js +1176 -0
  56. package/dashboard-dist/api/99.index.js.map +1 -0
  57. package/dashboard-dist/api/drizzle/sqlite/0000_short_captain_stacy.sql +70 -0
  58. package/dashboard-dist/api/drizzle/sqlite/0001_closed_magus.sql +10 -0
  59. package/dashboard-dist/api/drizzle/sqlite/0002_agent_capability_observation.sql +38 -0
  60. package/dashboard-dist/api/drizzle/sqlite/0003_auth_magic_link.sql +28 -0
  61. package/dashboard-dist/api/drizzle/sqlite/0004_static_scan_fields.sql +8 -0
  62. package/dashboard-dist/api/drizzle/sqlite/0005_gateway_activity.sql +24 -0
  63. package/dashboard-dist/api/drizzle/sqlite/0006_sour_marauders.sql +41 -0
  64. package/dashboard-dist/api/drizzle/sqlite/meta/0000_snapshot.json +460 -0
  65. package/dashboard-dist/api/drizzle/sqlite/meta/0001_snapshot.json +536 -0
  66. package/dashboard-dist/api/drizzle/sqlite/meta/0006_snapshot.json +1249 -0
  67. package/dashboard-dist/api/drizzle/sqlite/meta/_journal.json +55 -0
  68. package/dashboard-dist/api/index.js +28482 -0
  69. package/dashboard-dist/api/index.js.map +1 -0
  70. package/dashboard-dist/api/package.json +16 -0
  71. package/dashboard-dist/api/sourcemap-register.cjs +1 -0
  72. package/dashboard-dist/web/assets/index-BKUfzbIg.js +148 -0
  73. package/dashboard-dist/web/assets/index-rHRH99IQ.css +1 -0
  74. package/dashboard-dist/web/changeway-logo.png +0 -0
  75. package/dashboard-dist/web/favicon.svg +29 -0
  76. package/dashboard-dist/web/index.html +15 -0
  77. package/dashboard-dist/web/logo.svg +16 -0
  78. package/dist/agent/activation.d.ts +21 -0
  79. package/dist/agent/activation.d.ts.map +1 -0
  80. package/dist/agent/activation.js +94 -0
  81. package/dist/agent/activation.js.map +1 -0
  82. package/dist/agent/auth.d.ts +73 -0
  83. package/dist/agent/auth.d.ts.map +1 -0
  84. package/dist/agent/auth.js +363 -0
  85. package/dist/agent/auth.js.map +1 -0
  86. package/dist/agent/behavior-detector.d.ts +150 -0
  87. package/dist/agent/behavior-detector.d.ts.map +1 -0
  88. package/dist/agent/behavior-detector.js +559 -0
  89. package/dist/agent/behavior-detector.js.map +1 -0
  90. package/dist/agent/business-reporter.d.ts +114 -0
  91. package/dist/agent/business-reporter.d.ts.map +1 -0
  92. package/dist/agent/business-reporter.js +359 -0
  93. package/dist/agent/business-reporter.js.map +1 -0
  94. package/dist/agent/config-sync.d.ts +70 -0
  95. package/dist/agent/config-sync.d.ts.map +1 -0
  96. package/dist/agent/config-sync.js +133 -0
  97. package/dist/agent/config-sync.js.map +1 -0
  98. package/dist/agent/config.d.ts +98 -0
  99. package/dist/agent/config.d.ts.map +1 -0
  100. package/dist/agent/config.js +348 -0
  101. package/dist/agent/config.js.map +1 -0
  102. package/dist/agent/content-injection-scanner.d.ts +35 -0
  103. package/dist/agent/content-injection-scanner.d.ts.map +1 -0
  104. package/dist/agent/content-injection-scanner.js +270 -0
  105. package/dist/agent/content-injection-scanner.js.map +1 -0
  106. package/dist/agent/engine-log-writer.d.ts +6 -0
  107. package/dist/agent/engine-log-writer.d.ts.map +1 -0
  108. package/dist/agent/engine-log-writer.js +18 -0
  109. package/dist/agent/engine-log-writer.js.map +1 -0
  110. package/dist/agent/env.d.ts +19 -0
  111. package/dist/agent/env.d.ts.map +1 -0
  112. package/dist/agent/env.js +44 -0
  113. package/dist/agent/env.js.map +1 -0
  114. package/dist/agent/event-reporter.d.ts +87 -0
  115. package/dist/agent/event-reporter.d.ts.map +1 -0
  116. package/dist/agent/event-reporter.js +306 -0
  117. package/dist/agent/event-reporter.js.map +1 -0
  118. package/dist/agent/file-watcher.d.ts +50 -0
  119. package/dist/agent/file-watcher.d.ts.map +1 -0
  120. package/dist/agent/file-watcher.js +135 -0
  121. package/dist/agent/file-watcher.js.map +1 -0
  122. package/dist/agent/fs-utils.d.ts +22 -0
  123. package/dist/agent/fs-utils.d.ts.map +1 -0
  124. package/dist/agent/fs-utils.js +41 -0
  125. package/dist/agent/fs-utils.js.map +1 -0
  126. package/dist/agent/gateway-manager.d.ts +59 -0
  127. package/dist/agent/gateway-manager.d.ts.map +1 -0
  128. package/dist/agent/gateway-manager.js +583 -0
  129. package/dist/agent/gateway-manager.js.map +1 -0
  130. package/dist/agent/hook-types.d.ts +276 -0
  131. package/dist/agent/hook-types.d.ts.map +1 -0
  132. package/dist/agent/hook-types.js +51 -0
  133. package/dist/agent/hook-types.js.map +1 -0
  134. package/dist/agent/http-client.d.ts +19 -0
  135. package/dist/agent/http-client.d.ts.map +1 -0
  136. package/dist/agent/http-client.js +37 -0
  137. package/dist/agent/http-client.js.map +1 -0
  138. package/dist/agent/index.d.ts +8 -0
  139. package/dist/agent/index.d.ts.map +1 -0
  140. package/dist/agent/index.js +8 -0
  141. package/dist/agent/index.js.map +1 -0
  142. package/dist/agent/openclaw-hybrid-audit-changeway.js +1447 -0
  143. package/dist/agent/prompt-gate.d.ts +16 -0
  144. package/dist/agent/prompt-gate.d.ts.map +1 -0
  145. package/dist/agent/prompt-gate.js +58 -0
  146. package/dist/agent/prompt-gate.js.map +1 -0
  147. package/dist/agent/prompt-input.d.ts +9 -0
  148. package/dist/agent/prompt-input.d.ts.map +1 -0
  149. package/dist/agent/prompt-input.js +173 -0
  150. package/dist/agent/prompt-input.js.map +1 -0
  151. package/dist/agent/prompt-output.d.ts +4 -0
  152. package/dist/agent/prompt-output.d.ts.map +1 -0
  153. package/dist/agent/prompt-output.js +19 -0
  154. package/dist/agent/prompt-output.js.map +1 -0
  155. package/dist/agent/runner.d.ts +23 -0
  156. package/dist/agent/runner.d.ts.map +1 -0
  157. package/dist/agent/runner.js +165 -0
  158. package/dist/agent/runner.js.map +1 -0
  159. package/dist/agent/runtime-mode.d.ts +10 -0
  160. package/dist/agent/runtime-mode.d.ts.map +1 -0
  161. package/dist/agent/runtime-mode.js +19 -0
  162. package/dist/agent/runtime-mode.js.map +1 -0
  163. package/dist/agent/sanitizer.d.ts +10 -0
  164. package/dist/agent/sanitizer.d.ts.map +1 -0
  165. package/dist/agent/sanitizer.js +175 -0
  166. package/dist/agent/sanitizer.js.map +1 -0
  167. package/dist/agent/scan-activity.d.ts +19 -0
  168. package/dist/agent/scan-activity.d.ts.map +1 -0
  169. package/dist/agent/scan-activity.js +34 -0
  170. package/dist/agent/scan-activity.js.map +1 -0
  171. package/dist/agent/types.d.ts +177 -0
  172. package/dist/agent/types.d.ts.map +1 -0
  173. package/dist/agent/types.js +5 -0
  174. package/dist/agent/types.js.map +1 -0
  175. package/dist/agent/workspace-scanner.d.ts +35 -0
  176. package/dist/agent/workspace-scanner.d.ts.map +1 -0
  177. package/dist/agent/workspace-scanner.js +137 -0
  178. package/dist/agent/workspace-scanner.js.map +1 -0
  179. package/dist/dashboard-launcher.d.ts +52 -0
  180. package/dist/dashboard-launcher.d.ts.map +1 -0
  181. package/dist/dashboard-launcher.js +363 -0
  182. package/dist/dashboard-launcher.js.map +1 -0
  183. package/dist/gateway/activity.d.ts +52 -0
  184. package/dist/gateway/activity.d.ts.map +1 -0
  185. package/dist/gateway/activity.js +111 -0
  186. package/dist/gateway/activity.js.map +1 -0
  187. package/dist/gateway/config.d.ts +50 -0
  188. package/dist/gateway/config.d.ts.map +1 -0
  189. package/dist/gateway/config.js +200 -0
  190. package/dist/gateway/config.js.map +1 -0
  191. package/dist/gateway/handlers/anthropic.d.ts +12 -0
  192. package/dist/gateway/handlers/anthropic.d.ts.map +1 -0
  193. package/dist/gateway/handlers/anthropic.js +254 -0
  194. package/dist/gateway/handlers/anthropic.js.map +1 -0
  195. package/dist/gateway/handlers/gemini.d.ts +12 -0
  196. package/dist/gateway/handlers/gemini.d.ts.map +1 -0
  197. package/dist/gateway/handlers/gemini.js +101 -0
  198. package/dist/gateway/handlers/gemini.js.map +1 -0
  199. package/dist/gateway/handlers/models.d.ts +4 -0
  200. package/dist/gateway/handlers/models.d.ts.map +1 -0
  201. package/dist/gateway/handlers/models.js +36 -0
  202. package/dist/gateway/handlers/models.js.map +1 -0
  203. package/dist/gateway/handlers/openai.d.ts +16 -0
  204. package/dist/gateway/handlers/openai.d.ts.map +1 -0
  205. package/dist/gateway/handlers/openai.js +254 -0
  206. package/dist/gateway/handlers/openai.js.map +1 -0
  207. package/dist/gateway/index.d.ts +27 -0
  208. package/dist/gateway/index.d.ts.map +1 -0
  209. package/dist/gateway/index.js +290 -0
  210. package/dist/gateway/index.js.map +1 -0
  211. package/dist/gateway/mapping-store.d.ts +38 -0
  212. package/dist/gateway/mapping-store.d.ts.map +1 -0
  213. package/dist/gateway/mapping-store.js +74 -0
  214. package/dist/gateway/mapping-store.js.map +1 -0
  215. package/dist/gateway/restorer.d.ts +63 -0
  216. package/dist/gateway/restorer.d.ts.map +1 -0
  217. package/dist/gateway/restorer.js +284 -0
  218. package/dist/gateway/restorer.js.map +1 -0
  219. package/dist/gateway/sanitizer.d.ts +17 -0
  220. package/dist/gateway/sanitizer.d.ts.map +1 -0
  221. package/dist/gateway/sanitizer.js +228 -0
  222. package/dist/gateway/sanitizer.js.map +1 -0
  223. package/dist/gateway/types.d.ts +53 -0
  224. package/dist/gateway/types.d.ts.map +1 -0
  225. package/dist/gateway/types.js +5 -0
  226. package/dist/gateway/types.js.map +1 -0
  227. package/dist/index.d.ts +19 -0
  228. package/dist/index.d.ts.map +1 -0
  229. package/dist/index.js +2990 -0
  230. package/dist/index.js.map +1 -0
  231. package/dist/memory/index.d.ts +5 -0
  232. package/dist/memory/index.d.ts.map +1 -0
  233. package/dist/memory/index.js +5 -0
  234. package/dist/memory/index.js.map +1 -0
  235. package/dist/memory/store.d.ts +82 -0
  236. package/dist/memory/store.d.ts.map +1 -0
  237. package/dist/memory/store.js +194 -0
  238. package/dist/memory/store.js.map +1 -0
  239. package/dist/platform-client/index.d.ts +63 -0
  240. package/dist/platform-client/index.d.ts.map +1 -0
  241. package/dist/platform-client/index.js +294 -0
  242. package/dist/platform-client/index.js.map +1 -0
  243. package/dist/platform-client/types.d.ts +109 -0
  244. package/dist/platform-client/types.d.ts.map +1 -0
  245. package/dist/platform-client/types.js +3 -0
  246. package/dist/platform-client/types.js.map +1 -0
  247. package/dist/workspace-agents-guide.d.ts +22 -0
  248. package/dist/workspace-agents-guide.d.ts.map +1 -0
  249. package/dist/workspace-agents-guide.js +92 -0
  250. package/dist/workspace-agents-guide.js.map +1 -0
  251. package/dist/workspace-agents-sync.d.ts +24 -0
  252. package/dist/workspace-agents-sync.d.ts.map +1 -0
  253. package/dist/workspace-agents-sync.js +41 -0
  254. package/dist/workspace-agents-sync.js.map +1 -0
  255. package/dist/workspace-agents-watcher.d.ts +23 -0
  256. package/dist/workspace-agents-watcher.d.ts.map +1 -0
  257. package/dist/workspace-agents-watcher.js +152 -0
  258. package/dist/workspace-agents-watcher.js.map +1 -0
  259. package/dist/workspace-discovery.d.ts +11 -0
  260. package/dist/workspace-discovery.d.ts.map +1 -0
  261. package/dist/workspace-discovery.js +116 -0
  262. package/dist/workspace-discovery.js.map +1 -0
  263. package/gateway/package-lock.json +597 -0
  264. package/gateway/package.json +57 -0
  265. package/gateway/pnpm-lock.yaml +342 -0
  266. package/gateway/src/activity.ts +142 -0
  267. package/gateway/src/config.ts +246 -0
  268. package/gateway/src/handlers/anthropic.ts +328 -0
  269. package/gateway/src/handlers/gemini.ts +122 -0
  270. package/gateway/src/handlers/models.ts +45 -0
  271. package/gateway/src/handlers/openai.ts +333 -0
  272. package/gateway/src/index.ts +344 -0
  273. package/gateway/src/mapping-store.ts +88 -0
  274. package/gateway/src/restorer.ts +322 -0
  275. package/gateway/src/sanitizer.ts +298 -0
  276. package/gateway/src/types.ts +73 -0
  277. package/gateway/tsconfig.json +20 -0
  278. package/openclaw.plugin.json +86 -0
  279. package/package.json +74 -0
  280. package/samples/Untitled +1 -0
  281. package/samples/clean-email.txt +20 -0
  282. package/samples/test-document.md +53 -0
  283. package/samples/test-email-popup.txt +44 -0
  284. package/samples/test-email.txt +32 -0
  285. package/samples/test-webpage.html +51 -0
package/dist/index.js ADDED
@@ -0,0 +1,2990 @@
1
+ /**
2
+ * OpenGuardrails Plugin for OpenClaw
3
+ *
4
+ * Responsibilities:
5
+ * 1. Load credentials from disk on startup (no network)
6
+ * 2. Fall back to local MAC identity when no saved credentials exist
7
+ * 3. Detect behavioral anomalies at before_tool_call (block / alert)
8
+ * 4. Expose /og_status, /og_upgrade, /og_config commands
9
+ */
10
+ import { resolveConfig, loadCoreCredentials, deleteCoreCredentials, readAgentProfile, getProfileWatchPaths, readPluginEnabledFromOpenclawConfig, } from "./agent/config.js";
11
+ import { buildSignedAuthHeadersForUrl, getLocalAgentId, getLocalMacAddress, withChangewayOpenPrefix, } from "./agent/auth.js";
12
+ import { BehaviorDetector, FILE_READ_TOOLS, WEB_FETCH_TOOLS } from "./agent/behavior-detector.js";
13
+ import { EventReporter } from "./agent/event-reporter.js";
14
+ import { BusinessReporter } from "./agent/business-reporter.js";
15
+ import { ConfigSync } from "./agent/config-sync.js";
16
+ import { DashboardClient } from "./platform-client/index.js";
17
+ import { enableGateway, disableGateway, getGatewayStatus, startGateway, stopGateway, setDashboardPort, setGatewayActivityCallback } from "./agent/gateway-manager.js";
18
+ import { FileWatcher } from "./agent/file-watcher.js";
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { randomBytes } from "node:crypto";
22
+ import { spawn, spawnSync } from "node:child_process";
23
+ import { fileURLToPath } from "node:url";
24
+ import { openclawHome } from "./agent/env.js";
25
+ import { loadJsonSync } from "./agent/fs-utils.js";
26
+ import { appendEngineLogLine } from "./agent/engine-log-writer.js";
27
+ import { buildScanActivityObservation } from "./agent/scan-activity.js";
28
+ import { shouldActivatePluginRuntime } from "./agent/runtime-mode.js";
29
+ import { extractLatestUserPromptForDetection, extractPromptForDetection, extractTextContent, extractToolContentForDetection, isPromptAlertConfirmation, isSyntheticSessionBootstrapPrompt, isUserSender, } from "./agent/prompt-input.js";
30
+ import { rewriteAssistantMessageWithNotice } from "./agent/prompt-output.js";
31
+ import { buildPromptRiskNotice, buildPromptRiskOverrideInstruction, } from "./agent/prompt-gate.js";
32
+ import { activateDeviceByCode, getActivationStatus, getOrCreateDeviceId, loadActivationKeys, } from "./agent/activation.js";
33
+ import { syncAllWorkspaceAgentsGuides } from "./workspace-agents-sync.js";
34
+ import { WorkspaceAgentsWatcher } from "./workspace-agents-watcher.js";
35
+ // =============================================================================
36
+ // Constants
37
+ // =============================================================================
38
+ const PLUGIN_ID = "changewayguard";
39
+ const PLUGIN_NAME = "changewayguard";
40
+ const PLUGIN_VERSION = "6.7.0";
41
+ const LOG_PREFIX = `[${PLUGIN_ID}]`;
42
+ const BRAND_NAME = "电信安全·龙虾小卫士";
43
+ const BRAND_TAG = "changewayguard";
44
+ const QUOTA_EXCEEDED_TAG = `${BRAND_TAG}-quota-exceeded`;
45
+ const PROMPT_ALERT_WARNING = "经过见微大模型研判,该行为存在风险,请谨慎操作。";
46
+ const PROMPT_BLOCK_WARNING = "经过见微大模型研判,该行为存在风险,已为您阻断执行。";
47
+ //MAC 地址认证相关
48
+ const MAC_RECORD_DIR = path.join(openclawHome, "credentials", "changewayguard");
49
+ const MAC_RECORD_TEXT_PATH = path.join(MAC_RECORD_DIR, "local-mac-address.txt");
50
+ const MAC_RECORD_JSON_PATH = path.join(MAC_RECORD_DIR, "local-mac-address.json");
51
+ // 4. 安全扫描脚本
52
+ const CT_SCAN_SCRIPT_NAME = "openclaw-hybrid-audit-changeway.js";
53
+ const CT_SCAN_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟
54
+ const CT_SCAN_OUTPUT_MAX_CHARS = 12_000; // 1.2万字符
55
+ // 这段代码用来以 CommonJS 方式运行一个 ES Module 脚本: OpenClaw 插件是 ES Module("type": "module"),但安全扫描脚本 openclaw-hybrid-audit-changeway.js 可能是 CommonJS 格式。
56
+ const CT_SCAN_COMMONJS_LAUNCHER = [
57
+ "const fs=require('fs');",
58
+ "const path=require('path');",
59
+ "const Module=require('module');",
60
+ "const f=process.argv[1];",
61
+ "const m=new Module(f);",
62
+ "m.filename=f;",
63
+ "m.paths=Module._nodeModulePaths(path.dirname(f));",
64
+ "m._compile(fs.readFileSync(f,'utf8'), f);",
65
+ ].join("");
66
+ // 6. 状态持久化,记录行为钩子的启用状态
67
+ const BEHAVIOR_HOOKS_STATE_PATH = path.join(MAC_RECORD_DIR, "behavior-hooks.json");
68
+ const SECURITY_AUDIT_CRON_STATE_PATH = path.join(MAC_RECORD_DIR, "security-audit-cron.json");
69
+ //5. 定时安全审计
70
+ const SECURITY_AUDIT_CRON_NAME = "lunch-reminder"; // 定时任务名称
71
+ const SECURITY_AUDIT_CRON_DESC = "每天定时巡检"; // 任务描述
72
+ const SECURITY_AUDIT_CRON_TZ = "Asia/Shanghai"; // 时区
73
+ const SECURITY_AUDIT_SETUP_TIMEOUT_MS = 15_000;
74
+ const SECURITY_AUDIT_SYSTEM_EVENT = "/ct_scan"; // 触发的事件
75
+ // 这是一个历史遗留定时任务名称列表,用于兼容旧版本的插件。 确保插件升级时能自动清理旧版本的定时任务,防止重复运行安全扫描。
76
+ const LEGACY_SECURITY_AUDIT_CRON_NAMES = ["changeway-security-audit"];
77
+ // =============================================================================
78
+ // Debug file logger — writes to openclaw logs dir for agentic hours diagnosis
79
+ // =============================================================================
80
+ const DEBUG_LOG_PATH = path.join(openclawHome, "logs", "changewayguard-debug.log");
81
+ function debugLog(msg) {
82
+ try {
83
+ const ts = new Date().toISOString();
84
+ fs.appendFileSync(DEBUG_LOG_PATH, `[${ts}] ${msg}\n`);
85
+ }
86
+ catch { /* ignore */ }
87
+ }
88
+ function loadBehaviorHooksPreference() {
89
+ try {
90
+ if (!fs.existsSync(BEHAVIOR_HOOKS_STATE_PATH))
91
+ return null;
92
+ const raw = fs.readFileSync(BEHAVIOR_HOOKS_STATE_PATH, "utf-8");
93
+ const data = JSON.parse(raw);
94
+ if (typeof data.enabled !== "boolean")
95
+ return null;
96
+ return data.enabled;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ function saveBehaviorHooksPreference(enabled) {
103
+ try {
104
+ fs.mkdirSync(MAC_RECORD_DIR, { recursive: true });
105
+ fs.writeFileSync(BEHAVIOR_HOOKS_STATE_PATH, JSON.stringify({
106
+ enabled,
107
+ updatedAt: new Date().toISOString(),
108
+ }, null, 2), "utf-8");
109
+ }
110
+ catch {
111
+ // Ignore persistence errors.
112
+ }
113
+ }
114
+ function loadSecurityAuditCronState() {
115
+ try {
116
+ if (!fs.existsSync(SECURITY_AUDIT_CRON_STATE_PATH))
117
+ return {};
118
+ const raw = fs.readFileSync(SECURITY_AUDIT_CRON_STATE_PATH, "utf-8");
119
+ const parsed = JSON.parse(raw);
120
+ return parsed && typeof parsed === "object" ? parsed : {};
121
+ }
122
+ catch {
123
+ return {};
124
+ }
125
+ }
126
+ function saveSecurityAuditCronState(next) {
127
+ try {
128
+ fs.mkdirSync(MAC_RECORD_DIR, { recursive: true });
129
+ fs.writeFileSync(SECURITY_AUDIT_CRON_STATE_PATH, JSON.stringify(next, null, 2), "utf-8");
130
+ }
131
+ catch {
132
+ // Ignore persistence errors.
133
+ }
134
+ }
135
+ function randomInt(maxExclusive) {
136
+ if (maxExclusive <= 1)
137
+ return 0;
138
+ const byte = randomBytes(1)[0] ?? 0;
139
+ return byte % maxExclusive;
140
+ }
141
+ function generateNightCronExpr() {
142
+ const minute = randomInt(60); // 0-59
143
+ const hour = randomInt(5); // 0-4
144
+ return `${minute} ${hour} * * *`;
145
+ }
146
+ function isNightCronExpr(expr) {
147
+ const parts = expr.trim().split(/\s+/);
148
+ if (parts.length !== 5)
149
+ return false;
150
+ const minute = Number(parts[0]);
151
+ const hour = Number(parts[1]);
152
+ if (!Number.isInteger(minute) || !Number.isInteger(hour))
153
+ return false;
154
+ if (parts[2] !== "*" || parts[3] !== "*" || parts[4] !== "*")
155
+ return false;
156
+ return minute >= 0 && minute <= 59 && hour >= 0 && hour <= 4;
157
+ }
158
+ function normalizeNightCronExpr(expr) {
159
+ if (!expr)
160
+ return generateNightCronExpr();
161
+ return isNightCronExpr(expr) ? expr : generateNightCronExpr();
162
+ }
163
+ function parseJsonFromCliOutput(stdout) {
164
+ const trimmed = stdout.trim();
165
+ if (!trimmed)
166
+ return null;
167
+ try {
168
+ return JSON.parse(trimmed);
169
+ }
170
+ catch {
171
+ // Some CLIs may prepend non-JSON text before structured payload.
172
+ const objStart = trimmed.indexOf("{");
173
+ const arrStart = trimmed.indexOf("[");
174
+ const startCandidates = [objStart, arrStart].filter((v) => v >= 0);
175
+ if (startCandidates.length === 0)
176
+ return null;
177
+ const start = Math.min(...startCandidates);
178
+ try {
179
+ return JSON.parse(trimmed.slice(start));
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ }
186
+ function extractCronJobs(payload) {
187
+ if (Array.isArray(payload)) {
188
+ return payload.filter((item) => !!item && typeof item === "object");
189
+ }
190
+ if (!payload || typeof payload !== "object")
191
+ return [];
192
+ const record = payload;
193
+ if (Array.isArray(record.jobs))
194
+ return extractCronJobs(record.jobs);
195
+ if (Array.isArray(record.data))
196
+ return extractCronJobs(record.data);
197
+ if (record.data && typeof record.data === "object")
198
+ return extractCronJobs(record.data);
199
+ if (typeof record.name === "string" || typeof record.id === "string")
200
+ return [record];
201
+ return [];
202
+ }
203
+ function findCronJobByName(payload, name) {
204
+ const jobs = extractCronJobs(payload);
205
+ for (const job of jobs) {
206
+ if (job.name === name)
207
+ return job;
208
+ }
209
+ return null;
210
+ }
211
+ function findCronJobByNames(payload, names) {
212
+ for (const name of names) {
213
+ const found = findCronJobByName(payload, name);
214
+ if (found)
215
+ return found;
216
+ }
217
+ return null;
218
+ }
219
+ function findCronJobsByNames(payload, names) {
220
+ const nameSet = new Set(names);
221
+ return extractCronJobs(payload).filter((job) => {
222
+ const name = job.name;
223
+ return typeof name === "string" && nameSet.has(name);
224
+ });
225
+ }
226
+ function runOpenclawCommandSync(args) {
227
+ try {
228
+ const result = spawnSync("openclaw", args, {
229
+ encoding: "utf8",
230
+ timeout: SECURITY_AUDIT_SETUP_TIMEOUT_MS,
231
+ });
232
+ return {
233
+ ok: result.status === 0,
234
+ code: result.status,
235
+ stdout: (result.stdout ?? "").trim(),
236
+ stderr: (result.stderr ?? "").trim(),
237
+ ...(result.error ? { error: result.error.message } : {}),
238
+ };
239
+ }
240
+ catch (err) {
241
+ return {
242
+ ok: false,
243
+ code: null,
244
+ stdout: "",
245
+ stderr: "",
246
+ error: err instanceof Error ? err.message : String(err),
247
+ };
248
+ }
249
+ }
250
+ async function runOpenclawCommandAsync(args) {
251
+ return await new Promise((resolve) => {
252
+ const child = spawn("openclaw", args, {
253
+ stdio: ["ignore", "pipe", "pipe"],
254
+ shell: false,
255
+ });
256
+ let stdout = "";
257
+ let stderr = "";
258
+ let timedOut = false;
259
+ const timer = setTimeout(() => {
260
+ timedOut = true;
261
+ try {
262
+ child.kill();
263
+ }
264
+ catch { /* ignore */ }
265
+ }, SECURITY_AUDIT_SETUP_TIMEOUT_MS);
266
+ child.stdout.on("data", (chunk) => {
267
+ stdout += chunk.toString("utf8");
268
+ });
269
+ child.stderr.on("data", (chunk) => {
270
+ stderr += chunk.toString("utf8");
271
+ });
272
+ child.on("error", (err) => {
273
+ clearTimeout(timer);
274
+ resolve({
275
+ ok: false,
276
+ code: null,
277
+ stdout: stdout.trim(),
278
+ stderr: stderr.trim(),
279
+ error: err.message,
280
+ });
281
+ });
282
+ child.on("close", (code) => {
283
+ clearTimeout(timer);
284
+ resolve({
285
+ ok: !timedOut && code === 0,
286
+ code: timedOut ? null : code,
287
+ stdout: stdout.trim(),
288
+ stderr: stderr.trim(),
289
+ ...(timedOut ? { error: "timeout" } : {}),
290
+ });
291
+ });
292
+ });
293
+ }
294
+ function collectSessionCandidates(ctx, event) {
295
+ const c = (ctx ?? {});
296
+ const e = (event ?? {});
297
+ const raw = [
298
+ c.sessionKey,
299
+ c.conversationId,
300
+ c.channelId,
301
+ c.threadId,
302
+ e.sessionKey,
303
+ e.conversationId,
304
+ e.channelId,
305
+ e.threadId,
306
+ ];
307
+ const out = [];
308
+ for (const item of raw) {
309
+ if (typeof item !== "string")
310
+ continue;
311
+ const normalized = item.trim();
312
+ if (!normalized)
313
+ continue;
314
+ if (!out.includes(normalized))
315
+ out.push(normalized);
316
+ }
317
+ return out;
318
+ }
319
+ function resolveSessionKey(ctx, event) {
320
+ const candidates = collectSessionCandidates(ctx, event);
321
+ return candidates[0] ?? "";
322
+ }
323
+ function resolveActivePromptDecision(sessionCandidates, detector, blockOnRisk) {
324
+ for (const candidate of sessionCandidates) {
325
+ if (blockOnRisk) {
326
+ const blockDecision = detector?.getPendingPromptBlockDecision(candidate);
327
+ if (blockDecision) {
328
+ return { decisionKey: candidate, decision: blockDecision };
329
+ }
330
+ }
331
+ const alertDecision = detector?.getPendingPromptAlertDecision(candidate);
332
+ if (alertDecision) {
333
+ return { decisionKey: candidate, decision: alertDecision };
334
+ }
335
+ }
336
+ return null;
337
+ }
338
+ // =============================================================================
339
+ // API Helpers
340
+ // =============================================================================
341
+ /** Infer tool category from tool name for business reporting */
342
+ function inferToolCategory(toolName) {
343
+ const name = toolName.toLowerCase();
344
+ if (FILE_READ_TOOLS.has(toolName) || FILE_READ_TOOLS.has(name))
345
+ return "file_read";
346
+ if (WEB_FETCH_TOOLS.has(toolName) || WEB_FETCH_TOOLS.has(name))
347
+ return "web_fetch";
348
+ if (["bash", "shell", "run_command", "execute"].some((t) => name.includes(t)))
349
+ return "shell";
350
+ if (["write", "edit", "create_file", "delete"].some((t) => name.includes(t)))
351
+ return "file_write";
352
+ if (name.includes("agent") || name.includes("subagent"))
353
+ return "agent";
354
+ return "other";
355
+ }
356
+ /** Mask API key for display: sk-og-abc... */
357
+ function maskApiKey(apiKey) {
358
+ if (apiKey.length <= 12)
359
+ return apiKey;
360
+ return `${apiKey.slice(0, 12)}...`;
361
+ }
362
+ /** Format quota exceeded message for appending to tool results */
363
+ function formatQuotaMessage(info) {
364
+ // recommendation comes from Core - contains the security advice with upgrade URL
365
+ return [
366
+ "",
367
+ `<${QUOTA_EXCEEDED_TAG}>`,
368
+ "重要:请将以下配额信息明确告知用户:",
369
+ "",
370
+ info.recommendation,
371
+ `</${QUOTA_EXCEEDED_TAG}>`,
372
+ ].join("\n");
373
+ }
374
+ function appendProcessOutput(current, chunk) {
375
+ if (current.length >= CT_SCAN_OUTPUT_MAX_CHARS)
376
+ return current;
377
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
378
+ const remain = CT_SCAN_OUTPUT_MAX_CHARS - current.length;
379
+ return `${current}${text.slice(0, remain)}`;
380
+ }
381
+ function stripAnsi(text) {
382
+ // Remove ANSI control sequences so chat output stays readable.
383
+ return text.replace(/\x1B\[[0-9;?]*[ -/]*[@-~]/g, "");
384
+ }
385
+ function resolveCtScanScriptPath() {
386
+ const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
387
+ const candidates = [
388
+ path.join(runtimeDir, "agent", CT_SCAN_SCRIPT_NAME),
389
+ path.join(runtimeDir, "..", "agent", CT_SCAN_SCRIPT_NAME),
390
+ ];
391
+ for (const candidate of candidates) {
392
+ try {
393
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
394
+ return candidate;
395
+ }
396
+ }
397
+ catch {
398
+ // Ignore invalid candidate path.
399
+ }
400
+ }
401
+ return null;
402
+ }
403
+ function savePendingSecurityAuditCronState(cronExpr, errorText, source) {
404
+ saveSecurityAuditCronState({
405
+ status: "pending",
406
+ cronExpr,
407
+ lastError: errorText,
408
+ lastAttemptAt: new Date().toISOString(),
409
+ updatedBy: source,
410
+ });
411
+ }
412
+ function saveCreatedSecurityAuditCronState(cronExpr, jobId, source) {
413
+ saveSecurityAuditCronState({
414
+ status: "created",
415
+ cronExpr,
416
+ ...(jobId ? { jobId } : {}),
417
+ lastAttemptAt: new Date().toISOString(),
418
+ updatedBy: source,
419
+ });
420
+ }
421
+ function buildSecurityAuditCronAddArgs(cronExpr) {
422
+ return [
423
+ "cron",
424
+ "add",
425
+ "--name", SECURITY_AUDIT_CRON_NAME,
426
+ "--description", SECURITY_AUDIT_CRON_DESC,
427
+ "--cron", cronExpr,
428
+ "--tz", SECURITY_AUDIT_CRON_TZ,
429
+ "--session", "main",
430
+ "--system-event", SECURITY_AUDIT_SYSTEM_EVENT,
431
+ "--timeout-seconds", "10",
432
+ "--thinking", "off",
433
+ "--json",
434
+ ];
435
+ }
436
+ function pickCronExpr(job) {
437
+ if (!job)
438
+ return undefined;
439
+ const schedule = job.schedule;
440
+ if (!schedule || typeof schedule !== "object")
441
+ return undefined;
442
+ const expr = schedule.expr;
443
+ return typeof expr === "string" && expr.trim() ? expr.trim() : undefined;
444
+ }
445
+ function pickCronMessage(job) {
446
+ if (!job)
447
+ return undefined;
448
+ const payload = job.payload;
449
+ if (!payload || typeof payload !== "object")
450
+ return undefined;
451
+ const message = payload.message;
452
+ return typeof message === "string" && message.trim() ? message.trim() : undefined;
453
+ }
454
+ function pickSessionTarget(job) {
455
+ if (!job)
456
+ return undefined;
457
+ const value = job.sessionTarget;
458
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
459
+ }
460
+ function pickSystemEvent(job) {
461
+ if (!job)
462
+ return undefined;
463
+ const payload = job.payload;
464
+ if (!payload || typeof payload !== "object")
465
+ return undefined;
466
+ const record = payload;
467
+ const candidates = [
468
+ record.systemEvent,
469
+ record.system_event,
470
+ record.event,
471
+ record.message,
472
+ ];
473
+ for (const candidate of candidates) {
474
+ if (typeof candidate === "string" && candidate.trim())
475
+ return candidate.trim();
476
+ }
477
+ return undefined;
478
+ }
479
+ function shouldRecreateSecurityAuditCron(job) {
480
+ if (!job)
481
+ return true;
482
+ const sessionTarget = pickSessionTarget(job);
483
+ const systemEvent = pickSystemEvent(job);
484
+ const cronExpr = pickCronExpr(job);
485
+ const payload = job.payload && typeof job.payload === "object"
486
+ ? job.payload
487
+ : undefined;
488
+ const timeoutSeconds = payload?.timeoutSeconds;
489
+ const thinking = payload?.thinking;
490
+ if (!cronExpr || !isNightCronExpr(cronExpr))
491
+ return true;
492
+ if (sessionTarget !== "main")
493
+ return true;
494
+ if (systemEvent !== SECURITY_AUDIT_SYSTEM_EVENT)
495
+ return true;
496
+ if (timeoutSeconds !== undefined && String(timeoutSeconds) !== "10")
497
+ return true;
498
+ if (thinking !== undefined && thinking !== "off")
499
+ return true;
500
+ // Legacy message-based jobs should be recreated even if message contains /ct_scan.
501
+ const legacyMessage = pickCronMessage(job);
502
+ if (legacyMessage && !payload?.systemEvent && !payload?.system_event)
503
+ return true;
504
+ return false;
505
+ }
506
+ function pickCronJobId(job) {
507
+ if (!job)
508
+ return undefined;
509
+ const id = job.id;
510
+ return typeof id === "string" && id.trim() ? id : undefined;
511
+ }
512
+ function buildCommandErrorText(result) {
513
+ const parts = [
514
+ result.error ? `error=${result.error}` : "",
515
+ result.code !== null ? `code=${result.code}` : "",
516
+ result.stderr ? `stderr=${result.stderr}` : "",
517
+ ].filter(Boolean);
518
+ return parts.join(" | ") || "unknown";
519
+ }
520
+ function ensureSecurityAuditCronSync(log, source) {
521
+ const state = loadSecurityAuditCronState();
522
+ const cronExpr = normalizeNightCronExpr(state.cronExpr?.trim());
523
+ const listResult = runOpenclawCommandSync(["cron", "list", "--all", "--json"]);
524
+ if (!listResult.ok) {
525
+ const errorText = buildCommandErrorText(listResult);
526
+ savePendingSecurityAuditCronState(cronExpr, errorText, source);
527
+ log.warn(`security audit cron setup pending: cron list failed (${errorText})`);
528
+ return;
529
+ }
530
+ const listPayload = parseJsonFromCliOutput(listResult.stdout);
531
+ const targetJob = findCronJobByName(listPayload, SECURITY_AUDIT_CRON_NAME);
532
+ const legacyJobs = findCronJobsByNames(listPayload, LEGACY_SECURITY_AUDIT_CRON_NAMES);
533
+ const candidateJob = targetJob ?? legacyJobs[0] ?? null;
534
+ const finalCronExpr = normalizeNightCronExpr(pickCronExpr(candidateJob) ?? cronExpr);
535
+ // Remove legacy duplicate jobs first.
536
+ for (const legacy of legacyJobs) {
537
+ const id = pickCronJobId(legacy);
538
+ if (!id)
539
+ continue;
540
+ const rmLegacy = runOpenclawCommandSync(["cron", "rm", "--json", id]);
541
+ if (!rmLegacy.ok) {
542
+ const errorText = buildCommandErrorText(rmLegacy);
543
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
544
+ log.warn(`security audit cron setup pending: legacy cron rm failed (${errorText})`);
545
+ return;
546
+ }
547
+ }
548
+ if (targetJob && !shouldRecreateSecurityAuditCron(targetJob)) {
549
+ saveCreatedSecurityAuditCronState(finalCronExpr, pickCronJobId(targetJob), source);
550
+ log.info(`security audit cron already exists: ${SECURITY_AUDIT_CRON_NAME}`);
551
+ return;
552
+ }
553
+ if (targetJob) {
554
+ const existingId = pickCronJobId(targetJob);
555
+ if (!existingId) {
556
+ const errorText = "existing cron job id missing";
557
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
558
+ log.warn(`security audit cron setup pending: ${errorText}`);
559
+ return;
560
+ }
561
+ const rmResult = runOpenclawCommandSync(["cron", "rm", "--json", existingId]);
562
+ if (!rmResult.ok) {
563
+ const errorText = buildCommandErrorText(rmResult);
564
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
565
+ log.warn(`security audit cron setup pending: cron rm failed (${errorText})`);
566
+ return;
567
+ }
568
+ }
569
+ const addResult = runOpenclawCommandSync(buildSecurityAuditCronAddArgs(finalCronExpr));
570
+ if (!addResult.ok) {
571
+ const errorText = buildCommandErrorText(addResult);
572
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
573
+ log.warn(`security audit cron setup pending: cron upsert failed (${errorText})`);
574
+ return;
575
+ }
576
+ const addPayload = parseJsonFromCliOutput(addResult.stdout);
577
+ const createdJob = findCronJobByName(addPayload, SECURITY_AUDIT_CRON_NAME)
578
+ ?? (addPayload && typeof addPayload === "object" ? addPayload : null);
579
+ saveCreatedSecurityAuditCronState(finalCronExpr, pickCronJobId(createdJob), source);
580
+ log.info(`security audit cron ensured: name=${SECURITY_AUDIT_CRON_NAME} cron="${finalCronExpr}"`);
581
+ }
582
+ let securityAuditCronEnsuring = false;
583
+ async function ensureSecurityAuditCronAsync(log, source) {
584
+ if (securityAuditCronEnsuring)
585
+ return;
586
+ securityAuditCronEnsuring = true;
587
+ try {
588
+ const state = loadSecurityAuditCronState();
589
+ const cronExpr = normalizeNightCronExpr(state.cronExpr?.trim());
590
+ const listResult = await runOpenclawCommandAsync(["cron", "list", "--all", "--json"]);
591
+ if (!listResult.ok) {
592
+ const errorText = buildCommandErrorText(listResult);
593
+ savePendingSecurityAuditCronState(cronExpr, errorText, source);
594
+ log.warn(`security audit cron setup pending: cron list failed (${errorText})`);
595
+ return;
596
+ }
597
+ const listPayload = parseJsonFromCliOutput(listResult.stdout);
598
+ const targetJob = findCronJobByName(listPayload, SECURITY_AUDIT_CRON_NAME);
599
+ const legacyJobs = findCronJobsByNames(listPayload, LEGACY_SECURITY_AUDIT_CRON_NAMES);
600
+ const candidateJob = targetJob ?? legacyJobs[0] ?? null;
601
+ const finalCronExpr = normalizeNightCronExpr(pickCronExpr(candidateJob) ?? cronExpr);
602
+ // Remove legacy duplicate jobs first.
603
+ for (const legacy of legacyJobs) {
604
+ const id = pickCronJobId(legacy);
605
+ if (!id)
606
+ continue;
607
+ const rmLegacy = await runOpenclawCommandAsync(["cron", "rm", "--json", id]);
608
+ if (!rmLegacy.ok) {
609
+ const errorText = buildCommandErrorText(rmLegacy);
610
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
611
+ log.warn(`security audit cron setup pending: legacy cron rm failed (${errorText})`);
612
+ return;
613
+ }
614
+ }
615
+ if (targetJob && !shouldRecreateSecurityAuditCron(targetJob)) {
616
+ saveCreatedSecurityAuditCronState(finalCronExpr, pickCronJobId(targetJob), source);
617
+ log.info(`security audit cron already exists: ${SECURITY_AUDIT_CRON_NAME}`);
618
+ return;
619
+ }
620
+ if (targetJob) {
621
+ const existingId = pickCronJobId(targetJob);
622
+ if (!existingId) {
623
+ const errorText = "existing cron job id missing";
624
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
625
+ log.warn(`security audit cron setup pending: ${errorText}`);
626
+ return;
627
+ }
628
+ const rmResult = await runOpenclawCommandAsync(["cron", "rm", "--json", existingId]);
629
+ if (!rmResult.ok) {
630
+ const errorText = buildCommandErrorText(rmResult);
631
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
632
+ log.warn(`security audit cron setup pending: cron rm failed (${errorText})`);
633
+ return;
634
+ }
635
+ }
636
+ const addResult = await runOpenclawCommandAsync(buildSecurityAuditCronAddArgs(finalCronExpr));
637
+ if (!addResult.ok) {
638
+ const errorText = buildCommandErrorText(addResult);
639
+ savePendingSecurityAuditCronState(finalCronExpr, errorText, source);
640
+ log.warn(`security audit cron setup pending: cron upsert failed (${errorText})`);
641
+ return;
642
+ }
643
+ const addPayload = parseJsonFromCliOutput(addResult.stdout);
644
+ const createdJob = findCronJobByName(addPayload, SECURITY_AUDIT_CRON_NAME)
645
+ ?? (addPayload && typeof addPayload === "object" ? addPayload : null);
646
+ saveCreatedSecurityAuditCronState(finalCronExpr, pickCronJobId(createdJob), source);
647
+ log.info(`security audit cron ensured: name=${SECURITY_AUDIT_CRON_NAME} cron="${finalCronExpr}"`);
648
+ }
649
+ finally {
650
+ securityAuditCronEnsuring = false;
651
+ }
652
+ }
653
+ function buildCtScanNodeArgs(scriptPath, scriptArgs = []) {
654
+ if (path.extname(scriptPath).toLowerCase() === ".js") {
655
+ return ["--input-type=commonjs", "--eval", CT_SCAN_COMMONJS_LAUNCHER, scriptPath, ...scriptArgs];
656
+ }
657
+ return [scriptPath, ...scriptArgs];
658
+ }
659
+ async function runCtScanCommand(log, scriptArgs = []) {
660
+ const scriptPath = resolveCtScanScriptPath();
661
+ if (!scriptPath) {
662
+ return {
663
+ ok: false,
664
+ text: [
665
+ "**ct-scan 执行失败**",
666
+ "",
667
+ "巡检脚本未部署,请联系管理员检查插件安装。",
668
+ ].join("\n"),
669
+ };
670
+ }
671
+ return await new Promise((resolve) => {
672
+ const child = spawn(process.execPath, buildCtScanNodeArgs(scriptPath, scriptArgs), {
673
+ cwd: path.dirname(scriptPath),
674
+ env: process.env,
675
+ shell: false,
676
+ stdio: ["ignore", "pipe", "pipe"],
677
+ });
678
+ let stdout = "";
679
+ let stderr = "";
680
+ let timedOut = false;
681
+ const timeout = setTimeout(() => {
682
+ timedOut = true;
683
+ try {
684
+ child.kill();
685
+ }
686
+ catch {
687
+ // ignore
688
+ }
689
+ }, CT_SCAN_TIMEOUT_MS);
690
+ child.stdout.on("data", (chunk) => {
691
+ stdout = appendProcessOutput(stdout, chunk);
692
+ });
693
+ child.stderr.on("data", (chunk) => {
694
+ stderr = appendProcessOutput(stderr, chunk);
695
+ });
696
+ child.on("error", (err) => {
697
+ clearTimeout(timeout);
698
+ log.error(`ct-scan spawn error: script=${scriptPath} args=${scriptArgs.join(" ")} err=${err.message}`);
699
+ resolve({
700
+ ok: false,
701
+ text: [
702
+ "**ct-scan 执行失败**",
703
+ "",
704
+ "巡检进程启动失败,请联系管理员查看插件日志。",
705
+ ].join("\n"),
706
+ });
707
+ });
708
+ child.on("close", (code, signal) => {
709
+ clearTimeout(timeout);
710
+ if (timedOut) {
711
+ log.warn(`ct-scan timeout: script=${scriptPath} args=${scriptArgs.join(" ")}`);
712
+ resolve({
713
+ ok: false,
714
+ text: [
715
+ "**ct-scan 超时**",
716
+ "",
717
+ `执行超过 ${Math.floor(CT_SCAN_TIMEOUT_MS / 1000)} 秒,请稍后重试。`,
718
+ ].join("\n"),
719
+ });
720
+ return;
721
+ }
722
+ const ok = code === 0;
723
+ const cleanStdout = stripAnsi(stdout).trim();
724
+ const cleanStderr = stripAnsi(stderr).trim();
725
+ log.info(`ct-scan finished: code=${code ?? "null"} signal=${signal ?? "none"} script=${scriptPath} ` +
726
+ `args=${scriptArgs.join(" ")} stdout_len=${stdout.length} stderr_len=${stderr.length}`);
727
+ if (!ok && cleanStderr) {
728
+ log.warn(`ct-scan stderr: ${cleanStderr.slice(0, 4000)}`);
729
+ }
730
+ resolve({
731
+ ok,
732
+ text: [
733
+ ok ? "**ct-scan 执行成功**" : "**ct-scan 执行失败**",
734
+ "",
735
+ cleanStdout
736
+ ? `stdout:\n\`\`\`\n${cleanStdout}\n\`\`\``
737
+ : "stdout: (empty)",
738
+ cleanStderr
739
+ ? `stderr:\n\`\`\`\n${cleanStderr}\n\`\`\``
740
+ : "stderr: (empty)",
741
+ ].filter(Boolean).join("\n"),
742
+ });
743
+ });
744
+ });
745
+ }
746
+ const DEFAULT_ACCOUNT_STATUS = {
747
+ email: null,
748
+ plan: "free",
749
+ quotaUsed: 0,
750
+ quotaTotal: 500,
751
+ isAutonomous: true,
752
+ resetAt: null,
753
+ };
754
+ /** Account status no longer fetched from /api/v1/account; return local default only. */
755
+ async function getAccountStatus(_apiKey, coreUrl) {
756
+ void _apiKey;
757
+ void coreUrl;
758
+ return DEFAULT_ACCOUNT_STATUS;
759
+ }
760
+ // =============================================================================
761
+ // Logger
762
+ // =============================================================================
763
+ function createLogger(baseLogger) {
764
+ return {
765
+ info: (msg) => baseLogger.info(`${LOG_PREFIX} ${msg}`),
766
+ warn: (msg) => baseLogger.warn(`${LOG_PREFIX} ${msg}`),
767
+ error: (msg) => baseLogger.error(`${LOG_PREFIX} ${msg}`),
768
+ debug: (msg) => baseLogger.debug?.(`${LOG_PREFIX} ${msg}`),
769
+ };
770
+ }
771
+ // =============================================================================
772
+ // Database driver check (libsql)
773
+ // =============================================================================
774
+ // Note: @libsql/client has native bindings with WASM fallback, no manual setup needed.
775
+ // =============================================================================
776
+ // Plugin state (module-level — survives plugin re-registration within a process)
777
+ // =============================================================================
778
+ let globalCoreCredentials = null;
779
+ let globalBehaviorDetector = null;
780
+ let globalEventReporter = null;
781
+ let globalBusinessReporter = null;
782
+ let globalConfigSync = null;
783
+ let globalDashboardClient = null;
784
+ let globalFileWatcher = null;
785
+ let globalWorkspaceAgentsWatcher = null;
786
+ let dashboardHeartbeatTimer = null;
787
+ let profileWatchers = [];
788
+ let profileDebounceTimer = null;
789
+ // Track quota exceeded notification (only notify once per session)
790
+ let quotaExceededNotified = false;
791
+ // Track personal dashboard auto-start state
792
+ let personalDashboardStarted = false;
793
+ // Track LLM input timestamps per session for duration calculation
794
+ const llmInputTimestamps = new Map();
795
+ // Track auto-scan state
796
+ let autoScanEnabled = false;
797
+ // Track current account plan
798
+ let currentAccountPlan = "free";
799
+ let macNoticeEmitted = false;
800
+ function isPluginInstallCommand(args = process.argv.slice(2)) {
801
+ return args[0] === "plugins" && args[1] === "install";
802
+ }
803
+ function persistLocalMacAddress(mac, source) {
804
+ try {
805
+ fs.mkdirSync(MAC_RECORD_DIR, { recursive: true });
806
+ fs.writeFileSync(MAC_RECORD_TEXT_PATH, `${mac}\n`);
807
+ fs.writeFileSync(MAC_RECORD_JSON_PATH, JSON.stringify({
808
+ mac,
809
+ source,
810
+ recordedAt: new Date().toISOString(),
811
+ }, null, 2));
812
+ }
813
+ catch {
814
+ // Ignore persistence errors; still log MAC to console.
815
+ }
816
+ }
817
+ function emitLocalMacAddressNotice(log, source) {
818
+ if (macNoticeEmitted)
819
+ return;
820
+ macNoticeEmitted = true;
821
+ const mac = getLocalMacAddress();
822
+ persistLocalMacAddress(mac, source);
823
+ const deviceId = getOrCreateDeviceId();
824
+ if (source === "install") {
825
+ log.info(`安装成功,本机 MAC 地址:${mac}`);
826
+ }
827
+ else {
828
+ log.info(`本机 MAC 地址:${mac}`);
829
+ }
830
+ log.info(`MAC 地址已保存:${MAC_RECORD_TEXT_PATH}`);
831
+ log.info(`设备 ID:${deviceId}`);
832
+ }
833
+ function buildLocalCredentials(coreUrl) {
834
+ return {
835
+ apiKey: getLocalMacAddress(),
836
+ agentId: getLocalAgentId(),
837
+ claimUrl: "",
838
+ verificationCode: "",
839
+ coreUrl,
840
+ };
841
+ }
842
+ function ensureCoreCredentials(coreUrl) {
843
+ if (!globalCoreCredentials) {
844
+ globalCoreCredentials = buildLocalCredentials(coreUrl);
845
+ }
846
+ return globalCoreCredentials;
847
+ }
848
+ function stopDisableSensitiveRuntime() {
849
+ if (globalFileWatcher) {
850
+ globalFileWatcher.stop();
851
+ globalFileWatcher = null;
852
+ }
853
+ if (globalWorkspaceAgentsWatcher) {
854
+ globalWorkspaceAgentsWatcher.stop();
855
+ globalWorkspaceAgentsWatcher = null;
856
+ }
857
+ }
858
+ // =============================================================================
859
+ // Ensure default config in openclaw.json
860
+ // =============================================================================
861
+ /**
862
+ * Previously wrote default config to openclaw.json on first load.
863
+ * Now a no-op — we don't modify openclaw.json automatically.
864
+ * Config is optional; defaults are applied in resolveConfig().
865
+ */
866
+ function ensureDefaultConfig(_log) {
867
+ // no-op: don't write config to openclaw.json on fresh install
868
+ }
869
+ // =============================================================================
870
+ // Profile sync — watches workspace files and re-uploads on change
871
+ // =============================================================================
872
+ function startProfileSync(log) {
873
+ if (profileWatchers.length > 0)
874
+ return; // already watching
875
+ const paths = getProfileWatchPaths();
876
+ const scheduleUpload = () => {
877
+ if (profileDebounceTimer)
878
+ clearTimeout(profileDebounceTimer);
879
+ profileDebounceTimer = setTimeout(() => {
880
+ if (!globalDashboardClient?.agentId)
881
+ return;
882
+ const profile = readAgentProfile();
883
+ globalDashboardClient
884
+ .updateProfile({
885
+ ...(globalCoreCredentials?.agentId !== "configured"
886
+ ? { openclawId: globalCoreCredentials?.agentId }
887
+ : {}),
888
+ ...profile,
889
+ })
890
+ .then(() => log.debug?.("Dashboard: profile synced"))
891
+ .catch((err) => log.debug?.(`Dashboard: profile sync failed — ${err}`));
892
+ }, 2000);
893
+ };
894
+ for (const watchPath of paths) {
895
+ try {
896
+ if (!fs.existsSync(watchPath))
897
+ continue;
898
+ const watcher = fs.watch(watchPath, { recursive: false }, scheduleUpload);
899
+ profileWatchers.push(watcher);
900
+ }
901
+ catch {
902
+ // Non-critical — fs.watch may not be available in all environments
903
+ }
904
+ }
905
+ if (profileWatchers.length > 0) {
906
+ log.debug?.(`Dashboard: watching ${profileWatchers.length} path(s) for profile changes`);
907
+ }
908
+ }
909
+ // =============================================================================
910
+ // Plugin Definition
911
+ // =============================================================================
912
+ const openClawGuardPlugin = {
913
+ id: PLUGIN_ID,
914
+ name: PLUGIN_NAME,
915
+ description: "Security guard for OpenClaw agents",
916
+ //插件初始化入口
917
+ register(api) {
918
+ const log = createLogger(api.logger); // 创建日志对象
919
+ const args = process.argv.slice(2);
920
+ // 检查插件激活条件 ,如果只是安装命令(openclaw plugin install),只执行基础初始化,避免在非网关进程中初始化完整插件
921
+ if (!shouldActivatePluginRuntime(args, process.env.OPENCLAW_SERVICE_KIND)) {
922
+ if (isPluginInstallCommand(args)) {
923
+ emitLocalMacAddressNotice(log, "install");
924
+ ensureSecurityAuditCronSync(log, "install");
925
+ }
926
+ log.debug?.("changewayguard: skip runtime initialization outside gateway process");
927
+ return; // 非网关进程,直接退出
928
+ }
929
+ emitLocalMacAddressNotice(log, "gateway");
930
+ const engineLogPrefix = "调用见微检测引擎";
931
+ const maxEngineLogChars = 12_000;
932
+ // 生成唯一追踪 ID
933
+ const createTraceId = () => {
934
+ const randomNum = randomBytes(6).readUIntBE(0, 6);
935
+ return `${Date.now()}${String(randomNum).padStart(14, "0")}`;
936
+ };
937
+ // 格式化日志payload,截断超长内容
938
+ const formatEngineLogPayload = (payload) => {
939
+ try {
940
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
941
+ if (text.length <= maxEngineLogChars)
942
+ return text;
943
+ return `${text.slice(0, maxEngineLogChars)}...<truncated>`;
944
+ }
945
+ catch {
946
+ return String(payload);
947
+ }
948
+ };
949
+ // 记录 AI 检测引擎的请求/响应日志
950
+ const logEngineRequest = (url, payload, traceId) => {
951
+ const line = `${engineLogPrefix} traceId:${traceId} 请求 POST ${url} 参数=${formatEngineLogPayload(payload)}`;
952
+ log.info(line);
953
+ appendEngineLogLine(line);
954
+ };
955
+ const logEngineResponse = (url, status, payload, traceId) => {
956
+ const line = `${engineLogPrefix} traceId:${traceId} 响应 POST ${url} status=${status} 参数=${formatEngineLogPayload(payload)}`;
957
+ log.info(line);
958
+ appendEngineLogLine(line);
959
+ };
960
+ let behaviorHooksPreference = loadBehaviorHooksPreference();
961
+ let deviceActivated = getActivationStatus().activated;
962
+ let behaviorHooksEnabled = false;
963
+ const refreshBehaviorHooksState = () => {
964
+ behaviorHooksEnabled = deviceActivated
965
+ ? (behaviorHooksPreference ?? true)
966
+ : false;
967
+ };
968
+ const applyBehaviorHooksCredentials = () => {
969
+ refreshBehaviorHooksState();
970
+ const creds = behaviorHooksEnabled ? globalCoreCredentials : null;
971
+ globalBehaviorDetector?.setCredentials(creds);
972
+ globalEventReporter?.setCredentials(creds);
973
+ };
974
+ const getBehaviorHooksStatusText = () => {
975
+ refreshBehaviorHooksState();
976
+ return [
977
+ `- 激活状态: ${deviceActivated ? "已激活" : "未激活"}`,
978
+ `- changewayguard 实时防护: ${behaviorHooksEnabled ? "已开启" : "已关闭"}`,
979
+ ];
980
+ };
981
+ const promptScanCache = new Map();
982
+ const promptCacheKey = (sessionKey) => sessionKey || "__default__";
983
+ const promptFingerprint = (text) => text.replace(/\s+/g, " ").trim().slice(0, 2000);
984
+ const maybeScanPrompt = async (sessionKey, text, source) => {
985
+ if (!globalBehaviorDetector || !behaviorHooksEnabled)
986
+ return;
987
+ const trimmed = text.trim();
988
+ if (!trimmed)
989
+ return;
990
+ const cacheKey = promptCacheKey(sessionKey);
991
+ const fingerprint = promptFingerprint(trimmed);
992
+ if (promptScanCache.get(cacheKey) === fingerprint) {
993
+ return;
994
+ }
995
+ const promptDecision = await globalBehaviorDetector.scanPrompt(sessionKey, trimmed);
996
+ if (!promptDecision)
997
+ return;
998
+ promptScanCache.set(cacheKey, fingerprint);
999
+ debugLog(`prompt_scan: source=${source} sessionKey=${sessionKey} action=${promptDecision.action} ` +
1000
+ `risk=${promptDecision.riskLevel} confidence=${Math.round(promptDecision.confidence * 100)}%`);
1001
+ if (promptDecision.action === "alert" || promptDecision.action === "block") {
1002
+ log.warn(`Prompt scan [${promptDecision.riskLevel}/${Math.round(promptDecision.confidence * 100)}%] (${source}): ` +
1003
+ `${promptDecision.explanation}`);
1004
+ globalBusinessReporter?.recordDetection(promptDecision.riskLevel, promptDecision.action === "block" && config.blockOnRisk, promptDecision.explanation);
1005
+ }
1006
+ if (globalDashboardClient?.agentId) {
1007
+ const observation = buildScanActivityObservation({
1008
+ source: "prompt",
1009
+ action: promptDecision.action,
1010
+ agentId: globalDashboardClient.agentId,
1011
+ sessionKey,
1012
+ riskLevel: promptDecision.riskLevel,
1013
+ confidence: promptDecision.confidence,
1014
+ categories: promptDecision.categories,
1015
+ explanation: promptDecision.explanation,
1016
+ violatingInput: trimmed,
1017
+ latencyMs: promptDecision.latency_ms,
1018
+ });
1019
+ if (observation) {
1020
+ globalDashboardClient
1021
+ .reportToolCall(observation)
1022
+ .catch((err) => {
1023
+ log.debug?.(`Dashboard: prompt scan activity report failed — ${err}`);
1024
+ });
1025
+ }
1026
+ }
1027
+ };
1028
+ // ── Start AI Security Gateway (in-process) ────────────────────────
1029
+ // Resolve config before any runtime side effects so disabled plugins stay passive.
1030
+ const pluginConfig = (api.pluginConfig ?? {});
1031
+ debugLog(`=== PLUGIN REGISTER ===`);
1032
+ debugLog(`pluginConfig: ${JSON.stringify(pluginConfig)}`);
1033
+ //解析配置 & 启动网关
1034
+ const config = resolveConfig(pluginConfig); // 解析配置
1035
+ const isEnterprise = config.plan === "enterprise";
1036
+ debugLog(`resolved config: plan=${config.plan}, coreUrl=${config.coreUrl}, isEnterprise=${isEnterprise}`);
1037
+ if (config.enabled === false) {
1038
+ stopDisableSensitiveRuntime(); // 禁用插件
1039
+ log.info("Plugin disabled via config");
1040
+ return;
1041
+ }
1042
+ // Gateway runs in the plugin process and is always available.
1043
+ // Users enable sanitization via /og_sanitize on, which routes agents through it.
1044
+ try {
1045
+ // 启动 AI Security Gateway(进程内)
1046
+ startGateway();
1047
+ log.debug?.("AI Security Gateway started");
1048
+ }
1049
+ catch (err) {
1050
+ log.error(`Failed to start AI Security Gateway: ${err}`);
1051
+ }
1052
+ // Set dashboard port immediately so gateway can report activity
1053
+ // (Dashboard will start later, but port is fixed at 53667)
1054
+ // 设置仪表板端口
1055
+ const DASHBOARD_PORT = 53667;
1056
+ setDashboardPort(DASHBOARD_PORT);
1057
+ log.debug?.(`Gateway activity reporting enabled on port ${DASHBOARD_PORT}`);
1058
+ // Ensure openclaw.json has default config (coreUrl) on first load
1059
+ if (!pluginConfig.coreUrl) {
1060
+ ensureDefaultConfig(log);
1061
+ }
1062
+ const workspaceAgentsSyncSummary = syncAllWorkspaceAgentsGuides({
1063
+ openclawHomeDir: openclawHome,
1064
+ logger: log,
1065
+ });
1066
+ log.info(`Workspace AGENTS sync (startup): scanned=${workspaceAgentsSyncSummary.totalWorkspaces} ` +
1067
+ `updated=${workspaceAgentsSyncSummary.updatedCount} created=${workspaceAgentsSyncSummary.createdCount} ` +
1068
+ `appended=${workspaceAgentsSyncSummary.appendedCount} skipped=${workspaceAgentsSyncSummary.skippedCount} ` +
1069
+ `errors=${workspaceAgentsSyncSummary.errorCount}`);
1070
+ if (!globalWorkspaceAgentsWatcher) {
1071
+ globalWorkspaceAgentsWatcher = new WorkspaceAgentsWatcher({
1072
+ openclawHomeDir: openclawHome,
1073
+ logger: log,
1074
+ isPluginEnabled: () => readPluginEnabledFromOpenclawConfig(PLUGIN_ID, openclawHome),
1075
+ });
1076
+ }
1077
+ if (!globalWorkspaceAgentsWatcher.running) {
1078
+ globalWorkspaceAgentsWatcher.start();
1079
+ log.debug?.(`Workspace AGENTS watcher started (${globalWorkspaceAgentsWatcher.watchCount} directories)`);
1080
+ }
1081
+ void ensureSecurityAuditCronAsync(log, "gateway");
1082
+ if (isEnterprise) {
1083
+ log.info(`Enterprise mode: Core → ${config.coreUrl}`);
1084
+ }
1085
+ // ── Local initialization (no network) ──────────────────────── 初始化核心模块
1086
+ // 4.1 行为检测器
1087
+ if (!globalBehaviorDetector) {
1088
+ globalBehaviorDetector = new BehaviorDetector({
1089
+ coreUrl: config.coreUrl,
1090
+ assessTimeoutMs: Math.min(config.timeoutMs, 3000),
1091
+ blockOnRisk: config.blockOnRisk,
1092
+ pluginVersion: PLUGIN_VERSION,
1093
+ }, log);
1094
+ }
1095
+ // 4.2 事件上报器
1096
+ if (!globalEventReporter) {
1097
+ globalEventReporter = new EventReporter({
1098
+ coreUrl: config.coreUrl,
1099
+ pluginVersion: PLUGIN_VERSION,
1100
+ timeoutMs: Math.min(config.timeoutMs, 3000),
1101
+ }, log);
1102
+ }
1103
+ //4.3 凭证管理
1104
+ if (!globalCoreCredentials) {
1105
+ if (config.apiKey) {
1106
+ // 使用配置的 API Key
1107
+ globalCoreCredentials = {
1108
+ apiKey: config.apiKey,
1109
+ agentId: getLocalAgentId(),
1110
+ claimUrl: "",
1111
+ verificationCode: "",
1112
+ coreUrl: config.coreUrl,
1113
+ };
1114
+ log.info("Platform: using configured API key (Authorization header uses local MAC)");
1115
+ }
1116
+ else {
1117
+ /// 从磁盘加载或使用本地 MAC 认证
1118
+ debugLog(`loadCoreCredentials(${config.coreUrl}) called`);
1119
+ globalCoreCredentials = loadCoreCredentials(config.coreUrl);
1120
+ debugLog(`loadCoreCredentials result: ${globalCoreCredentials ? `apiKey=${globalCoreCredentials.apiKey?.slice(0, 10)}... agentId=${globalCoreCredentials.agentId} coreUrl=${globalCoreCredentials.coreUrl}` : "null"}`);
1121
+ if (!globalCoreCredentials) {
1122
+ globalCoreCredentials = buildLocalCredentials(config.coreUrl);
1123
+ log.info("Platform: local mode enabled (registration skipped, Authorization uses local MAC)");
1124
+ }
1125
+ }
1126
+ applyBehaviorHooksCredentials();
1127
+ if (!behaviorHooksEnabled) {
1128
+ log.info("Behavior hooks disabled: plugin not activated or user switch is off");
1129
+ }
1130
+ }
1131
+ applyBehaviorHooksCredentials();
1132
+ if (!behaviorHooksEnabled) {
1133
+ if (globalConfigSync) {
1134
+ globalConfigSync.stop();
1135
+ globalConfigSync = null;
1136
+ }
1137
+ if (globalBusinessReporter) {
1138
+ globalBusinessReporter.setCredentials(null);
1139
+ void globalBusinessReporter.stop();
1140
+ globalBusinessReporter = null;
1141
+ }
1142
+ if (autoScanEnabled && globalFileWatcher?.running) {
1143
+ globalFileWatcher.stop();
1144
+ autoScanEnabled = false;
1145
+ }
1146
+ }
1147
+ // ── Personal Dashboard auto-start 个人仪表板启动 ─────────────────────────────────
1148
+ // Starts the local dashboard automatically when the plugin loads.
1149
+ // Data is stored in the plugin's data directory.
1150
+ async function initPersonalDashboard(coreUrl) {
1151
+ debugLog(`initPersonalDashboard: called, personalDashboardStarted=${personalDashboardStarted}`);
1152
+ if (personalDashboardStarted) {
1153
+ debugLog("initPersonalDashboard: already started, skipping");
1154
+ return;
1155
+ }
1156
+ personalDashboardStarted = true;
1157
+ try {
1158
+ const { startLocalDashboard, getPluginDataDir, DASHBOARD_PORT, DevModeError } = await import("./dashboard-launcher.js");
1159
+ const dataDir = getPluginDataDir();
1160
+ // 启动本地 Dashboard 服务,自动启动本地仪表板,用于可视化安全状态。
1161
+ const result = await startLocalDashboard({
1162
+ apiKey: globalCoreCredentials?.apiKey ?? "",
1163
+ agentId: globalCoreCredentials?.agentId ?? "",
1164
+ coreUrl,
1165
+ dataDir,
1166
+ autoStart: true,
1167
+ });
1168
+ log.info(`${BRAND_NAME} dashboard started at ${result.localUrl}`);
1169
+ // Connect to local dashboard for observation reporting
1170
+ // Use the session token from startLocalDashboard, not the Core API key
1171
+ // 连接 Dashboard
1172
+ initDashboardClient(result.token, `http://localhost:${DASHBOARD_PORT}`);
1173
+ }
1174
+ catch (err) {
1175
+ // Dev mode or startup failure - silently continue
1176
+ debugLog(`initPersonalDashboard FAILED: ${err}`);
1177
+ log.debug?.(`Dashboard auto-start skipped: ${err}`);
1178
+ }
1179
+ }
1180
+ // ── Dashboard client initialization ─────────────────────────────
1181
+ // Connects to the dashboard for observation reporting.
1182
+ // Uses the local session token for auth.
1183
+ function initDashboardClient(sessionToken, dashboardUrl) {
1184
+ debugLog(`initDashboardClient: dashboardUrl=${dashboardUrl} token=${sessionToken?.slice(0, 8)}...`);
1185
+ if (globalDashboardClient) {
1186
+ debugLog("initDashboardClient: already initialized, skipping");
1187
+ return;
1188
+ }
1189
+ if (!dashboardUrl || !sessionToken) {
1190
+ debugLog("initDashboardClient: missing url or token, skipping");
1191
+ return;
1192
+ }
1193
+ globalDashboardClient = new DashboardClient({
1194
+ dashboardUrl,
1195
+ sessionToken,
1196
+ });
1197
+ // Register agent then upload full profile (non-blocking)
1198
+ const profile = readAgentProfile();
1199
+ globalDashboardClient
1200
+ .registerAgent({
1201
+ name: config.agentName,
1202
+ description: "OpenClaw AI Agent secured by changewayGuard",
1203
+ provider: profile.provider || undefined,
1204
+ metadata: {
1205
+ ...(globalCoreCredentials?.agentId !== "configured" ? { openclawId: globalCoreCredentials?.agentId } : {}),
1206
+ ...profile,
1207
+ },
1208
+ })
1209
+ .then((result) => {
1210
+ if (result.success && result.data?.id) {
1211
+ log.debug?.(`Dashboard: agent registered (${result.data.id})`);
1212
+ startProfileSync(log);
1213
+ }
1214
+ })
1215
+ .catch((err) => {
1216
+ log.warn(`Dashboard: registration failed — ${err}`);
1217
+ });
1218
+ // Start periodic heartbeat
1219
+ dashboardHeartbeatTimer = globalDashboardClient.startHeartbeat(60_000);
1220
+ log.debug?.(`Dashboard: connected to ${dashboardUrl}`);
1221
+ }
1222
+ if (globalCoreCredentials) {
1223
+ // Start personal dashboard (auto-starts local dashboard and connects to it)
1224
+ initPersonalDashboard(config.coreUrl);
1225
+ }
1226
+ // ── Business plan initialization 商业功能初始化 ───────────────────────────────
1227
+ // Check account plan and initialize BusinessReporter + ConfigSync if business.
1228
+ async function initBusinessFeatures(coreUrl) {
1229
+ debugLog(`initBusinessFeatures: called, credentials=${!!globalCoreCredentials}, isEnterprise=${isEnterprise}`);
1230
+ if (!globalCoreCredentials) {
1231
+ debugLog("initBusinessFeatures: no credentials, skipping");
1232
+ return;
1233
+ }
1234
+ if (!behaviorHooksEnabled) {
1235
+ debugLog("initBusinessFeatures: behavior hooks disabled, skipping");
1236
+ return;
1237
+ }
1238
+ try {
1239
+ let plan;
1240
+ if (isEnterprise) {
1241
+ // Enterprise mode: always business plan, skip remote check
1242
+ plan = "business";
1243
+ }
1244
+ else {
1245
+ // 检查账户计划(free / business)
1246
+ const status = await getAccountStatus(globalCoreCredentials.apiKey, coreUrl);
1247
+ plan = status.plan;
1248
+ }
1249
+ currentAccountPlan = plan;
1250
+ debugLog(`initBusinessFeatures: plan=${plan}`);
1251
+ if (plan !== "business") {
1252
+ debugLog(`initBusinessFeatures: plan is not business, skipping`);
1253
+ log.debug?.(`Account plan is "${plan}", business features not enabled`);
1254
+ return;
1255
+ }
1256
+ // Initialize BusinessReporter
1257
+ if (!globalBusinessReporter) {
1258
+ // 初始化 BusinessReporter(商业版上报)
1259
+ globalBusinessReporter = new BusinessReporter({ coreUrl, pluginVersion: PLUGIN_VERSION }, log);
1260
+ globalBusinessReporter.setCredentials(globalCoreCredentials);
1261
+ // Set profile from workspace
1262
+ const profile = readAgentProfile();
1263
+ globalBusinessReporter.setProfile({
1264
+ ownerName: profile.ownerName,
1265
+ agentName: config.agentName,
1266
+ provider: profile.provider,
1267
+ model: profile.model,
1268
+ });
1269
+ globalBusinessReporter.initialize(plan);
1270
+ debugLog(`BusinessReporter initialized, enabled=${globalBusinessReporter.isEnabled()}`);
1271
+ // Wire gateway activity to business reporter
1272
+ if (globalBusinessReporter.isEnabled()) {
1273
+ setGatewayActivityCallback((redactionCount, typeCounts) => {
1274
+ globalBusinessReporter?.recordGatewayActivity(redactionCount, typeCounts);
1275
+ });
1276
+ // Wire secret detection to business reporter
1277
+ globalBehaviorDetector?.setOnSecretDetected((typeCounts) => {
1278
+ globalBusinessReporter?.recordSecretDetection(typeCounts);
1279
+ });
1280
+ }
1281
+ }
1282
+ // Initialize ConfigSync // 初始化 ConfigSync(配置同步)
1283
+ if (!globalConfigSync) {
1284
+ globalConfigSync = new ConfigSync({
1285
+ coreUrl,
1286
+ onUpdate: (bizConfig) => {
1287
+ log.info(`ConfigSync: received ${bizConfig.policies.length} policies`);
1288
+ // Future: apply gateway config and policies locally
1289
+ },
1290
+ }, log);
1291
+ globalConfigSync.setCredentials(globalCoreCredentials);
1292
+ await globalConfigSync.initialize(plan);
1293
+ }
1294
+ }
1295
+ catch (err) {
1296
+ log.debug?.(`Business features init failed: ${err}`);
1297
+ }
1298
+ }
1299
+ if (globalCoreCredentials) {
1300
+ initBusinessFeatures(config.coreUrl);
1301
+ }
1302
+ // ── Hooks 注册 Hooks────────────────────────────────────────────────────
1303
+ // Capture initial user prompt as intent + inject OpenGuardrails context
1304
+ // Hook 1: Agent 启动前 - 注入安全上下文
1305
+ api.on("before_agent_start", async (event, ctx) => {
1306
+ const sessionKey = resolveSessionKey(ctx, event);
1307
+ const text = extractTextContent(event.prompt);
1308
+ const promptFromEvent = extractPromptForDetection(event.prompt);
1309
+ const replayPrompt = globalBehaviorDetector?.consumeConfirmedPromptReplay(sessionKey) ?? null;
1310
+ let forcedPromptSafetyNotice = null;
1311
+ let forcedPromptDecision = null;
1312
+ debugLog(`before_agent_start: sessionKey=${sessionKey} candidates=${collectSessionCandidates(ctx, event).join("|")} ` +
1313
+ `promptLength=${text.length} replay=${replayPrompt ? "yes" : "no"}`);
1314
+ // Set up run ID for this session
1315
+ const runId = `run-${randomBytes(8).toString("hex")}`;
1316
+ globalEventReporter?.setRunId(sessionKey, runId);
1317
+ if (globalBehaviorDetector) {
1318
+ const promptFromMessages = Array.isArray(event.messages)
1319
+ ? extractLatestUserPromptForDetection(event.messages)
1320
+ : "";
1321
+ const promptToScan = replayPrompt
1322
+ ? ""
1323
+ : (promptFromMessages || (isSyntheticSessionBootstrapPrompt(text) ? "" : promptFromEvent));
1324
+ if (replayPrompt) {
1325
+ globalBehaviorDetector.setUserIntent(sessionKey, replayPrompt);
1326
+ debugLog(`before_agent_start: replaying confirmed alert prompt for session=${sessionKey}`);
1327
+ }
1328
+ else if (promptToScan) {
1329
+ globalBehaviorDetector.setUserIntent(sessionKey, promptToScan);
1330
+ // 提取用户提示词并扫描风险
1331
+ await maybeScanPrompt(sessionKey, promptToScan, "before_agent_start");
1332
+ }
1333
+ if (!replayPrompt) {
1334
+ const blockDecision = config.blockOnRisk
1335
+ ? globalBehaviorDetector.getPendingPromptBlockDecision(sessionKey)
1336
+ : null;
1337
+ const alertDecision = globalBehaviorDetector.getPendingPromptAlertDecision(sessionKey);
1338
+ const activePromptDecision = blockDecision ?? alertDecision;
1339
+ if (activePromptDecision) {
1340
+ forcedPromptDecision = activePromptDecision;
1341
+ forcedPromptSafetyNotice = buildPromptRiskNotice(activePromptDecision, {
1342
+ brandName: BRAND_NAME,
1343
+ blockWarning: PROMPT_BLOCK_WARNING,
1344
+ alertWarning: PROMPT_ALERT_WARNING,
1345
+ });
1346
+ debugLog(`before_agent_start: prompt-gate action=${activePromptDecision.action} ` +
1347
+ `risk=${activePromptDecision.riskLevel} confidence=${Math.round(activePromptDecision.confidence * 100)}% ` +
1348
+ `sessionKey=${sessionKey}`);
1349
+ }
1350
+ }
1351
+ }
1352
+ // Report to Core (non-blocking)
1353
+ globalEventReporter?.report(sessionKey, "before_agent_start", {
1354
+ timestamp: new Date().toISOString(),
1355
+ prompt: text,
1356
+ systemPrompt: event.systemPrompt,
1357
+ conversationId: event.conversationId,
1358
+ });
1359
+ // Inject security context so the LLM knows about OpenGuardrails protection.
1360
+ // tool_result_persist redacts injection patterns in persisted messages, but due to
1361
+ // producer/consumer timing in pi-agent-core, the LLM may see original content on
1362
+ // the current turn. This context makes the LLM aware of OpenGuardrails regardless.
1363
+ const prependSections = [];
1364
+ if (forcedPromptSafetyNotice && forcedPromptDecision) {
1365
+ prependSections.push(buildPromptRiskOverrideInstruction(forcedPromptSafetyNotice, forcedPromptDecision));
1366
+ }
1367
+ else {
1368
+ prependSections.push([
1369
+ "<changewayGuard> 本次会话受 changewayGuard保护。",
1370
+ "安全检测:",
1371
+ "1、请谨记见微安全指南的安全规范;并按照见微的拦截/确认回复规范进行回复",
1372
+ "2、请参考changewayGuard安全检测完之后提供的提示信息,并严格按照提示信息进行操作;",
1373
+ `如果在工具结果中看到 <${QUOTA_EXCEEDED_TAG}> 标签,必须告知用户。`,
1374
+ "这是安全通知,不是广告。",
1375
+ "需要读取标签内的内容并完整转达给用户。",
1376
+ "用户需要这些信息来维持安全保护。</changewayGuard>",
1377
+ ].join("\n"));
1378
+ }
1379
+ if (replayPrompt) {
1380
+ prependSections.push([
1381
+ `<${BRAND_TAG}-confirmed-replay>`,
1382
+ "用户刚刚确认继续上一条被提醒的请求。",
1383
+ "本轮用户输入里的确认词只是确认动作,不是实际业务问题。",
1384
+ "请将下面这段内容视为本轮真正需要继续处理的用户请求,并直接继续执行:",
1385
+ replayPrompt,
1386
+ `</${BRAND_TAG}-confirmed-replay>`,
1387
+ ].join("\n"));
1388
+ }
1389
+ return {
1390
+ // 返回注入的安全上下文
1391
+ prependContext: prependSections.join("\n\n"),
1392
+ };
1393
+ });
1394
+ // Capture ongoing user messages
1395
+ // Hook 2: 消息接收 - 持续监控用户输入
1396
+ api.on("message_received", async (event, ctx) => {
1397
+ const sessionKey = resolveSessionKey(ctx, event);
1398
+ const text = extractTextContent(event.content);
1399
+ const promptForDetection = extractPromptForDetection(event.content);
1400
+ const from = event.from;
1401
+ const userSender = isUserSender(from);
1402
+ debugLog(`message_received: from=${String(from)} userSender=${userSender} sessionKey=${sessionKey} ` +
1403
+ `candidates=${collectSessionCandidates(ctx, event).join("|")} contentLength=${text.length} ` +
1404
+ `scanLength=${promptForDetection.length}`);
1405
+ let isAlertConfirmation = false;
1406
+ if (globalBehaviorDetector && userSender && promptForDetection) {
1407
+ const pendingAlert = globalBehaviorDetector.getPendingPromptAlertDecision(sessionKey);
1408
+ if (pendingAlert && isPromptAlertConfirmation(promptForDetection)) {
1409
+ globalBehaviorDetector.confirmPendingPromptAlert(sessionKey);
1410
+ promptScanCache.delete(promptCacheKey(sessionKey));
1411
+ isAlertConfirmation = true;
1412
+ log.info(`Prompt alert confirmed by user for session=${sessionKey} ` +
1413
+ `[${pendingAlert.riskLevel}/${Math.round(pendingAlert.confidence * 100)}%]`);
1414
+ }
1415
+ }
1416
+ if (globalBehaviorDetector && userSender && promptForDetection && !isAlertConfirmation) {
1417
+ globalBehaviorDetector.setUserIntent(sessionKey, promptForDetection);
1418
+ // 扫描新消息的风险
1419
+ await maybeScanPrompt(sessionKey, promptForDetection, "message_received");
1420
+ }
1421
+ // Report to Core (non-blocking)
1422
+ globalEventReporter?.report(sessionKey, "message_received", {
1423
+ timestamp: new Date().toISOString(),
1424
+ from: event.from,
1425
+ content: text.slice(0, 100000), // Truncate very large content
1426
+ contentLength: text.length,
1427
+ });
1428
+ });
1429
+ // Clear behavioral state when session ends
1430
+ // Hook 4: Session 结束 - 清理状态
1431
+ api.on("session_end", async (event, ctx) => {
1432
+ const sessionKey = ctx.sessionKey ?? event.sessionId ?? "";
1433
+ // Report to Core (non-blocking)
1434
+ globalEventReporter?.report(sessionKey, "session_end", {
1435
+ timestamp: new Date().toISOString(),
1436
+ sessionId: event.sessionId ?? sessionKey,
1437
+ durationMs: event.durationMs,
1438
+ });
1439
+ // Report session end to business reporter
1440
+ globalBusinessReporter?.recordSession("end", event.durationMs);
1441
+ globalBehaviorDetector?.clearSession(sessionKey);
1442
+ globalEventReporter?.clearSession(sessionKey);
1443
+ promptScanCache.delete(promptCacheKey(sessionKey));
1444
+ });
1445
+ // Core detection hook — may block the tool call
1446
+ // Hook 3: 工具调用前 - 核心安全检查
1447
+ api.on("before_tool_call", async (event, ctx) => {
1448
+ log.debug?.(`before_tool_call: ${event.toolName}`);
1449
+ let blocked = false;
1450
+ let blockReason;
1451
+ if (globalBehaviorDetector && behaviorHooksEnabled) {
1452
+ const decision = await globalBehaviorDetector.onBeforeToolCall({ sessionKey: ctx.sessionKey ?? "", agentId: ctx.agentId }, { toolName: event.toolName, params: event.params });
1453
+ if (decision?.block) {
1454
+ blocked = true;
1455
+ blockReason = decision.blockReason;
1456
+ log.warn(`BLOCKED "${event.toolName}": ${decision.blockReason}`);
1457
+ }
1458
+ }
1459
+ // Report to dashboard (non-blocking)
1460
+ if (globalDashboardClient?.agentId) {
1461
+ globalDashboardClient
1462
+ .reportToolCall({
1463
+ agentId: globalDashboardClient.agentId,
1464
+ sessionKey: ctx.sessionKey,
1465
+ toolName: event.toolName,
1466
+ params: event.params,
1467
+ phase: "before",
1468
+ blocked,
1469
+ blockReason,
1470
+ })
1471
+ .catch((err) => {
1472
+ log.debug?.(`Dashboard: report failed (before ${event.toolName}) — ${err}`);
1473
+ });
1474
+ }
1475
+ if (blocked) {
1476
+ // Report blocked tool call to business reporter
1477
+ globalBusinessReporter?.recordToolCall(event.toolName, inferToolCategory(event.toolName), 0, true);
1478
+ // Record blocked call for local agentic hours
1479
+ globalDashboardClient?.recordToolCallDuration(0, true);
1480
+ return { block: true, blockReason };
1481
+ }
1482
+ }, { priority: 100 });
1483
+ // Scan tool results for content injection before they reach the LLM
1484
+ // Also append quota exceeded messages when applicable
1485
+ api.on("tool_result_persist", (event, ctx) => {
1486
+ log.info(`tool_result_persist triggered: toolName=${event.toolName ?? ctx.toolName ?? "unknown"}`);
1487
+ if (!globalBehaviorDetector) {
1488
+ log.debug?.("tool_result_persist: no detector");
1489
+ return;
1490
+ }
1491
+ // Resolve tool name from event, context, or the message itself
1492
+ const message = event.message;
1493
+ const msgToolName = message && "toolName" in message ? message.toolName : undefined;
1494
+ const toolName = event.toolName ?? ctx.toolName ?? msgToolName;
1495
+ log.debug?.(`tool_result_persist: toolName=${toolName ?? "(none)"} [event=${event.toolName}, ctx=${ctx.toolName}, msg=${msgToolName}]`);
1496
+ // Check message structure first before consuming quota message
1497
+ if (!message || !("content" in message) || !Array.isArray(message.content)) {
1498
+ log.debug?.(`tool_result_persist: message.content not an array (role=${message && "role" in message ? message.role : "?"})`);
1499
+ // Don't consume quota message if we can't append it
1500
+ return;
1501
+ }
1502
+ const contentArray = message.content;
1503
+ let messageModified = false;
1504
+ // Check for pending quota message (should be appended to any tool result)
1505
+ const quotaMessage = globalBehaviorDetector.consumePendingQuotaMessage();
1506
+ log.debug?.(`tool_result_persist: quotaMessage=${quotaMessage ? "present" : "none"}`);
1507
+ if (quotaMessage) {
1508
+ const formattedMsg = formatQuotaMessage(quotaMessage);
1509
+ contentArray.push({
1510
+ type: "text",
1511
+ text: formattedMsg,
1512
+ });
1513
+ messageModified = true;
1514
+ log.warn(`Quota exceeded — appending upgrade message to tool result (${quotaMessage.quotaUsed}/${quotaMessage.quotaTotal})`);
1515
+ }
1516
+ // Report to Core (non-blocking)
1517
+ globalEventReporter?.report(ctx.sessionKey ?? "", "tool_result_persist", {
1518
+ timestamp: new Date().toISOString(),
1519
+ toolName,
1520
+ modified: messageModified,
1521
+ modificationReason: messageModified ? "quota_message_appended" : undefined,
1522
+ });
1523
+ // If no toolName, we've done what we can (appended quota message if any)
1524
+ // Local injection scanning removed - all detection handled by Core
1525
+ return messageModified ? { message } : undefined;
1526
+ }, { priority: 100 });
1527
+ // Record completed tool for chain history + scan content for injection via Core
1528
+ api.on("after_tool_call", async (event, ctx) => {
1529
+ log.debug?.(`after_tool_call: ${event.toolName} (${event.durationMs}ms)`);
1530
+ if (globalBehaviorDetector && behaviorHooksEnabled) {
1531
+ globalBehaviorDetector.onAfterToolCall({ sessionKey: ctx.sessionKey ?? "" }, {
1532
+ toolName: event.toolName,
1533
+ params: event.params,
1534
+ result: event.result,
1535
+ error: event.error,
1536
+ durationMs: event.durationMs,
1537
+ });
1538
+ // Scan ALL tool results for injection via Core (not just file read / web fetch)
1539
+ if (event.result && !event.error) {
1540
+ const extractedResultText = extractToolContentForDetection(event.result);
1541
+ const resultText = extractedResultText || (typeof event.result === "string"
1542
+ ? event.result
1543
+ : JSON.stringify(event.result));
1544
+ // Only scan if content is non-trivial (> 20 chars to avoid noise)
1545
+ if (resultText.length > 20) {
1546
+ const scanResult = await globalBehaviorDetector.scanContent(ctx.sessionKey ?? "", event.toolName, resultText);
1547
+ if (scanResult && globalDashboardClient?.agentId) {
1548
+ const observation = buildScanActivityObservation({
1549
+ source: "content",
1550
+ action: scanResult.action,
1551
+ agentId: globalDashboardClient.agentId,
1552
+ sessionKey: ctx.sessionKey,
1553
+ riskLevel: scanResult.riskLevel,
1554
+ confidence: scanResult.confidence,
1555
+ categories: scanResult.categories,
1556
+ explanation: scanResult.explanation || scanResult.summary,
1557
+ latencyMs: scanResult.latency_ms,
1558
+ scannedToolName: event.toolName,
1559
+ });
1560
+ if (observation) {
1561
+ globalDashboardClient
1562
+ .reportToolCall(observation)
1563
+ .catch((err) => {
1564
+ log.debug?.(`Dashboard: content scan activity report failed — ${err}`);
1565
+ });
1566
+ }
1567
+ }
1568
+ if (scanResult?.detected) {
1569
+ log.warn(`Core: injection detected in "${event.toolName}" result: ${scanResult.summary}`);
1570
+ // Report detection to business reporter
1571
+ globalBusinessReporter?.recordDetection(scanResult.detected ? "high" : "no_risk", false, scanResult.summary);
1572
+ // Report dynamic scan result to business reporter
1573
+ globalBusinessReporter?.recordScanResult("dynamic", scanResult.categories ?? [], true);
1574
+ // Record risk event for local agentic hours
1575
+ globalDashboardClient?.recordRiskEvent();
1576
+ }
1577
+ // Report detection result to dashboard (non-blocking)
1578
+ if (scanResult && globalDashboardClient) {
1579
+ // Calculate sensitivity score from findings confidence
1580
+ // high=0.9, medium=0.7, low=0.5, take max
1581
+ const confidenceScores = { high: 0.9, medium: 0.7, low: 0.5 };
1582
+ const sensitivityScore = scanResult.findings.length > 0
1583
+ ? Math.max(...scanResult.findings.map((f) => confidenceScores[f.confidence] ?? 0.5))
1584
+ : 0;
1585
+ globalDashboardClient
1586
+ .reportDetection({
1587
+ agentId: globalDashboardClient.agentId || "unknown",
1588
+ sessionKey: ctx.sessionKey,
1589
+ toolName: event.toolName,
1590
+ safe: !scanResult.detected,
1591
+ categories: scanResult.categories,
1592
+ findings: scanResult.findings.map((f) => ({
1593
+ scanner: f.scanner,
1594
+ name: f.name,
1595
+ matchedText: f.matchedText,
1596
+ confidence: f.confidence,
1597
+ })),
1598
+ sensitivityScore,
1599
+ latencyMs: scanResult.latency_ms,
1600
+ })
1601
+ .catch((err) => {
1602
+ log.debug?.(`Dashboard: detection report failed — ${err}`);
1603
+ });
1604
+ }
1605
+ }
1606
+ }
1607
+ }
1608
+ // Report to dashboard (non-blocking)
1609
+ if (globalDashboardClient?.agentId) {
1610
+ globalDashboardClient
1611
+ .reportToolCall({
1612
+ agentId: globalDashboardClient.agentId,
1613
+ sessionKey: ctx.sessionKey,
1614
+ toolName: event.toolName,
1615
+ params: event.params,
1616
+ phase: "after",
1617
+ result: event.error ? undefined : "ok",
1618
+ error: event.error,
1619
+ durationMs: event.durationMs,
1620
+ })
1621
+ .catch((err) => {
1622
+ log.debug?.(`Dashboard: report failed (after ${event.toolName}) — ${err}`);
1623
+ });
1624
+ }
1625
+ // Report tool call to business reporter (with duration and category)
1626
+ debugLog(`after_tool_call: tool=${event.toolName} durationMs=${event.durationMs} dashboardClient=${!!globalDashboardClient} businessReporter=${!!globalBusinessReporter} businessEnabled=${globalBusinessReporter?.isEnabled()}`);
1627
+ globalBusinessReporter?.recordToolCall(event.toolName, inferToolCategory(event.toolName), event.durationMs ?? 0, false);
1628
+ // Record tool call duration for local agentic hours
1629
+ globalDashboardClient?.recordToolCallDuration(event.durationMs ?? 0);
1630
+ });
1631
+ // ── New Hooks (18 additional hooks for complete context) ────
1632
+ // Note: Many of these hooks may not be in the OpenClaw SDK types yet.
1633
+ // We use type assertions to register them, and they'll work at runtime
1634
+ // when/if OpenClaw supports them.
1635
+ const apiAny = api;
1636
+ // Agent lifecycle: agent_end
1637
+ apiAny.on("agent_end", async (event, ctx) => {
1638
+ const sessionKey = ctx?.sessionKey ?? "";
1639
+ globalEventReporter?.report(sessionKey, "agent_end", {
1640
+ timestamp: new Date().toISOString(),
1641
+ reason: event?.reason ?? "unknown",
1642
+ error: event?.error,
1643
+ durationMs: event?.durationMs,
1644
+ });
1645
+ });
1646
+ // Session lifecycle: session_start
1647
+ apiAny.on("session_start", async (event, ctx) => {
1648
+ const sessionKey = ctx?.sessionKey ?? "";
1649
+ const sessionId = event?.sessionId ?? sessionKey;
1650
+ // Set up run ID if not already set
1651
+ if (!globalEventReporter?.getRunId(sessionKey)) {
1652
+ const runId = `run-${randomBytes(8).toString("hex")}`;
1653
+ globalEventReporter?.setRunId(sessionKey, runId);
1654
+ }
1655
+ globalEventReporter?.report(sessionKey, "session_start", {
1656
+ timestamp: new Date().toISOString(),
1657
+ sessionId,
1658
+ isNew: event?.isNew ?? true,
1659
+ });
1660
+ // Report session start to business reporter
1661
+ debugLog(`session_start: sessionKey=${sessionKey} dashboardClient=${!!globalDashboardClient} businessReporter=${!!globalBusinessReporter}`);
1662
+ globalBusinessReporter?.recordSession("start");
1663
+ // Record session start for local agentic hours
1664
+ globalDashboardClient?.recordSessionStart();
1665
+ });
1666
+ // Model resolution: before_model_resolve
1667
+ apiAny.on("before_model_resolve", async (event, ctx) => {
1668
+ const sessionKey = ctx?.sessionKey ?? "";
1669
+ globalEventReporter?.report(sessionKey, "before_model_resolve", {
1670
+ timestamp: new Date().toISOString(),
1671
+ requestedModel: event?.model ?? event?.requestedModel ?? "unknown",
1672
+ });
1673
+ });
1674
+ // Prompt building: before_prompt_build
1675
+ apiAny.on("before_prompt_build", async (event, ctx) => {
1676
+ const sessionKey = ctx?.sessionKey ?? "";
1677
+ const sessionCandidates = collectSessionCandidates(ctx, event);
1678
+ const activeDecision = resolveActivePromptDecision(sessionCandidates, globalBehaviorDetector, config.blockOnRisk);
1679
+ let prependSystemContext;
1680
+ if (activeDecision) {
1681
+ const notice = buildPromptRiskNotice(activeDecision.decision, {
1682
+ brandName: BRAND_NAME,
1683
+ blockWarning: PROMPT_BLOCK_WARNING,
1684
+ alertWarning: PROMPT_ALERT_WARNING,
1685
+ });
1686
+ prependSystemContext = buildPromptRiskOverrideInstruction(notice, activeDecision.decision);
1687
+ debugLog(`before_prompt_build:prompt-notice-system action=${activeDecision.decision.action} ` +
1688
+ `sessionKey=${sessionKey} decisionKey=${activeDecision.decisionKey} ` +
1689
+ `risk=${activeDecision.decision.riskLevel} ` +
1690
+ `confidence=${Math.round(activeDecision.decision.confidence * 100)}%`);
1691
+ }
1692
+ globalEventReporter?.report(sessionKey, "before_prompt_build", {
1693
+ timestamp: new Date().toISOString(),
1694
+ messageCount: event?.messageCount ?? event?.messages?.length ?? 0,
1695
+ tokenEstimate: event?.tokenEstimate,
1696
+ });
1697
+ if (prependSystemContext) {
1698
+ return { prependSystemContext };
1699
+ }
1700
+ });
1701
+ // LLM input: llm_input (critical for context)
1702
+ apiAny.on("llm_input", async (event, ctx) => {
1703
+ const sessionKey = ctx?.sessionKey ?? "";
1704
+ // Track timestamp for LLM duration calculation (OpenClaw may not provide latencyMs)
1705
+ llmInputTimestamps.set(sessionKey, Date.now());
1706
+ const content = typeof event?.content === "string"
1707
+ ? event.content
1708
+ : JSON.stringify(event?.messages ?? event?.content ?? "");
1709
+ globalEventReporter?.report(sessionKey, "llm_input", {
1710
+ timestamp: new Date().toISOString(),
1711
+ model: event?.model ?? "unknown",
1712
+ content: content.slice(0, 100000), // Truncate very large content
1713
+ contentLength: content.length,
1714
+ messageCount: event?.messages?.length ?? 1,
1715
+ tokenCount: event?.tokenCount,
1716
+ systemPrompt: event?.systemPrompt,
1717
+ });
1718
+ });
1719
+ // LLM output: llm_output (critical for context)
1720
+ apiAny.on("llm_output", async (event, ctx) => {
1721
+ const sessionKey = ctx?.sessionKey ?? "";
1722
+ const content = typeof event?.content === "string"
1723
+ ? event.content
1724
+ : JSON.stringify(event?.content ?? "");
1725
+ // Compute LLM duration: prefer event-provided, fall back to our own timing
1726
+ const inputTs = llmInputTimestamps.get(sessionKey);
1727
+ const llmDuration = event?.latencyMs ?? event?.durationMs ?? (inputTs ? Date.now() - inputTs : 0);
1728
+ if (inputTs)
1729
+ llmInputTimestamps.delete(sessionKey);
1730
+ globalEventReporter?.report(sessionKey, "llm_output", {
1731
+ timestamp: new Date().toISOString(),
1732
+ model: event?.model ?? "unknown",
1733
+ content: content.slice(0, 100000),
1734
+ contentLength: content.length,
1735
+ streamed: event?.streamed ?? false,
1736
+ tokenUsage: event?.usage ?? event?.tokenUsage,
1737
+ latencyMs: llmDuration,
1738
+ stopReason: event?.stopReason ?? event?.stop_reason,
1739
+ });
1740
+ // Report LLM call to business reporter
1741
+ debugLog(`llm_output: model=${event?.model} latencyMs=${event?.latencyMs} durationMs=${event?.durationMs} computed=${llmDuration} dashboardClient=${!!globalDashboardClient} businessReporter=${!!globalBusinessReporter}`);
1742
+ if (llmDuration > 0) {
1743
+ globalBusinessReporter?.recordLlmCall(llmDuration, event?.model);
1744
+ // Record LLM duration for local agentic hours
1745
+ globalDashboardClient?.recordLlmDuration(llmDuration);
1746
+ }
1747
+ });
1748
+ // Message sending: message_sending (blocking - can modify/cancel)
1749
+ api.on("message_sending", async (event, ctx) => {
1750
+ const sessionCandidates = collectSessionCandidates(ctx, event);
1751
+ const sessionKey = sessionCandidates[0] ?? "";
1752
+ const content = typeof event.content === "string"
1753
+ ? event.content
1754
+ : JSON.stringify(event.content ?? "");
1755
+ debugLog(`message_sending: sessionKey=${sessionKey} candidates=${sessionCandidates.join("|")} contentLength=${content.length}`);
1756
+ const activeDecision = resolveActivePromptDecision(sessionCandidates, globalBehaviorDetector, config.blockOnRisk);
1757
+ if (activeDecision) {
1758
+ const notice = buildPromptRiskNotice(activeDecision.decision, {
1759
+ brandName: BRAND_NAME,
1760
+ blockWarning: PROMPT_BLOCK_WARNING,
1761
+ alertWarning: PROMPT_ALERT_WARNING,
1762
+ });
1763
+ debugLog(`message_sending:prompt-notice action=${activeDecision.decision.action} sessionKey=${sessionKey} ` +
1764
+ `decisionKey=${activeDecision.decisionKey} risk=${activeDecision.decision.riskLevel} ` +
1765
+ `confidence=${Math.round(activeDecision.decision.confidence * 100)}%`);
1766
+ log.warn(`Message replaced by prompt ${activeDecision.decision.action} gate ` +
1767
+ `[${activeDecision.decision.riskLevel}/${Math.round(activeDecision.decision.confidence * 100)}%]: ` +
1768
+ `${activeDecision.decision.explanation}`);
1769
+ globalEventReporter?.report(sessionKey, "message_sending", {
1770
+ timestamp: new Date().toISOString(),
1771
+ to: event.to ?? "user",
1772
+ content: notice,
1773
+ contentLength: notice.length,
1774
+ }, false);
1775
+ return { content: notice };
1776
+ }
1777
+ // Report to Core (non-blocking telemetry)
1778
+ globalEventReporter?.report(sessionKey, "message_sending", {
1779
+ timestamp: new Date().toISOString(),
1780
+ to: event.to ?? "user",
1781
+ content: content.slice(0, 100000),
1782
+ contentLength: content.length,
1783
+ }, false);
1784
+ });
1785
+ // Message sent: message_sent
1786
+ api.on("message_sent", async (event, ctx) => {
1787
+ const sessionKey = resolveSessionKey(ctx, event);
1788
+ globalEventReporter?.report(sessionKey, "message_sent", {
1789
+ timestamp: new Date().toISOString(),
1790
+ to: event.to ?? "user",
1791
+ success: true,
1792
+ durationMs: event.durationMs,
1793
+ });
1794
+ });
1795
+ // Before message write: before_message_write (synchronous in current OpenClaw runtime)
1796
+ apiAny.on("before_message_write", (event, ctx) => {
1797
+ const sessionKey = resolveSessionKey(ctx, event);
1798
+ const sessionCandidates = collectSessionCandidates(ctx, event);
1799
+ const message = event?.message;
1800
+ const role = typeof message?.role === "string" ? message.role : "unknown";
1801
+ const content = extractTextContent(message?.content ?? event?.content ?? message);
1802
+ debugLog(`before_message_write: sessionKey=${sessionKey} role=${role} candidates=${sessionCandidates.join("|")} ` +
1803
+ `contentLength=${content.length}`);
1804
+ if (role === "assistant") {
1805
+ const activeDecision = resolveActivePromptDecision(sessionCandidates, globalBehaviorDetector, config.blockOnRisk);
1806
+ if (activeDecision) {
1807
+ const notice = buildPromptRiskNotice(activeDecision.decision, {
1808
+ brandName: BRAND_NAME,
1809
+ blockWarning: PROMPT_BLOCK_WARNING,
1810
+ alertWarning: PROMPT_ALERT_WARNING,
1811
+ });
1812
+ const rewritten = rewriteAssistantMessageWithNotice(message, notice);
1813
+ if (rewritten) {
1814
+ debugLog(`before_message_write:prompt-notice-applied action=${activeDecision.decision.action} ` +
1815
+ `sessionKey=${sessionKey} decisionKey=${activeDecision.decisionKey} ` +
1816
+ `risk=${activeDecision.decision.riskLevel} ` +
1817
+ `confidence=${Math.round(activeDecision.decision.confidence * 100)}%`);
1818
+ log.warn(`Prompt notice enforced at before_message_write: action=${activeDecision.decision.action}, ` +
1819
+ `risk=${activeDecision.decision.riskLevel}, ` +
1820
+ `confidence=${Math.round(activeDecision.decision.confidence * 100)}%`);
1821
+ void globalEventReporter?.report(sessionKey, "before_message_write", {
1822
+ timestamp: new Date().toISOString(),
1823
+ filePath: event?.filePath ?? event?.path ?? "unknown",
1824
+ content: notice.slice(0, 100000),
1825
+ contentLength: notice.length,
1826
+ }, false);
1827
+ return { message: rewritten };
1828
+ }
1829
+ }
1830
+ }
1831
+ void globalEventReporter?.report(sessionKey, "before_message_write", {
1832
+ timestamp: new Date().toISOString(),
1833
+ filePath: event?.filePath ?? event?.path ?? "unknown",
1834
+ content: content.slice(0, 100000),
1835
+ contentLength: content.length,
1836
+ }, false);
1837
+ });
1838
+ // Compaction: before_compaction
1839
+ api.on("before_compaction", async (event, ctx) => {
1840
+ const sessionKey = ctx.sessionKey ?? "";
1841
+ globalEventReporter?.report(sessionKey, "before_compaction", {
1842
+ timestamp: new Date().toISOString(),
1843
+ messageCount: event.messageCount ?? 0,
1844
+ tokenEstimate: event.tokenEstimate,
1845
+ reason: event.reason ?? "auto",
1846
+ });
1847
+ });
1848
+ // Compaction: after_compaction
1849
+ api.on("after_compaction", async (event, ctx) => {
1850
+ const sessionKey = ctx.sessionKey ?? "";
1851
+ globalEventReporter?.report(sessionKey, "after_compaction", {
1852
+ timestamp: new Date().toISOString(),
1853
+ messageCount: event.messageCount ?? 0,
1854
+ removedCount: event.removedCount ?? 0,
1855
+ tokenEstimate: event.tokenEstimate,
1856
+ });
1857
+ });
1858
+ // Reset: before_reset
1859
+ apiAny.on("before_reset", async (event, ctx) => {
1860
+ const sessionKey = ctx?.sessionKey ?? "";
1861
+ globalEventReporter?.report(sessionKey, "before_reset", {
1862
+ timestamp: new Date().toISOString(),
1863
+ reason: event?.reason ?? "unknown",
1864
+ messageCount: event?.messageCount ?? 0,
1865
+ });
1866
+ });
1867
+ // Subagent: subagent_spawning (blocking - critical for security)
1868
+ apiAny.on("subagent_spawning", async (event, ctx) => {
1869
+ const sessionKey = ctx?.sessionKey ?? "";
1870
+ const task = typeof event?.task === "string"
1871
+ ? event.task
1872
+ : typeof event?.prompt === "string"
1873
+ ? event.prompt
1874
+ : JSON.stringify(event?.task ?? event?.prompt ?? "");
1875
+ const decision = await globalEventReporter?.report(sessionKey, "subagent_spawning", {
1876
+ timestamp: new Date().toISOString(),
1877
+ subagentId: event?.subagentId ?? event?.id ?? "unknown",
1878
+ subagentType: event?.subagentType ?? event?.type ?? "unknown",
1879
+ task: task.slice(0, 100000),
1880
+ taskLength: task.length,
1881
+ parentContext: event?.parentContext,
1882
+ }, true);
1883
+ if (decision?.block) {
1884
+ log.warn(`BLOCKED subagent spawn: ${decision.reason}`);
1885
+ return { block: true, blockReason: decision.reason };
1886
+ }
1887
+ });
1888
+ // Subagent: subagent_delivery_target
1889
+ apiAny.on("subagent_delivery_target", async (event, ctx) => {
1890
+ const sessionKey = ctx?.sessionKey ?? "";
1891
+ globalEventReporter?.report(sessionKey, "subagent_delivery_target", {
1892
+ timestamp: new Date().toISOString(),
1893
+ subagentId: event?.subagentId ?? event?.id ?? "unknown",
1894
+ targetType: event?.targetType ?? event?.type ?? "unknown",
1895
+ targetDetails: event?.targetDetails ?? event?.details,
1896
+ });
1897
+ });
1898
+ // Subagent: subagent_spawned
1899
+ apiAny.on("subagent_spawned", async (event, ctx) => {
1900
+ const sessionKey = ctx?.sessionKey ?? "";
1901
+ globalEventReporter?.report(sessionKey, "subagent_spawned", {
1902
+ timestamp: new Date().toISOString(),
1903
+ subagentId: event?.subagentId ?? event?.id ?? "unknown",
1904
+ subagentType: event?.subagentType ?? event?.type ?? "unknown",
1905
+ success: event?.success ?? true,
1906
+ error: event?.error,
1907
+ });
1908
+ });
1909
+ // Subagent: subagent_ended
1910
+ apiAny.on("subagent_ended", async (event, ctx) => {
1911
+ const sessionKey = ctx?.sessionKey ?? "";
1912
+ globalEventReporter?.report(sessionKey, "subagent_ended", {
1913
+ timestamp: new Date().toISOString(),
1914
+ subagentId: event?.subagentId ?? event?.id ?? "unknown",
1915
+ reason: event?.reason ?? "unknown",
1916
+ resultSummary: event?.resultSummary ?? event?.result,
1917
+ error: event?.error,
1918
+ durationMs: event?.durationMs,
1919
+ });
1920
+ });
1921
+ // Gateway: gateway_start
1922
+ apiAny.on("gateway_start", async (event, ctx) => {
1923
+ const sessionKey = ctx?.sessionKey ?? "";
1924
+ globalEventReporter?.report(sessionKey, "gateway_start", {
1925
+ timestamp: new Date().toISOString(),
1926
+ port: event?.port ?? 0,
1927
+ url: event?.url ?? "",
1928
+ });
1929
+ });
1930
+ // Gateway: gateway_stop
1931
+ apiAny.on("gateway_stop", async (event, ctx) => {
1932
+ const sessionKey = ctx?.sessionKey ?? "";
1933
+ globalEventReporter?.report(sessionKey, "gateway_stop", {
1934
+ timestamp: new Date().toISOString(),
1935
+ reason: event?.reason ?? "unknown",
1936
+ error: event?.error,
1937
+ });
1938
+ });
1939
+ // ── Commands 注册命令─────────────────────────────────────────────────
1940
+ // 状态查询
1941
+ api.registerCommand({
1942
+ name: "og_status", // 状态查询
1943
+ description: "Show MoltGuard status, API key, and quota",
1944
+ requireAuth: true,
1945
+ handler: async () => {
1946
+ const creds = ensureCoreCredentials(config.coreUrl);
1947
+ deviceActivated = getActivationStatus().activated;
1948
+ applyBehaviorHooksCredentials();
1949
+ // Get live quota status from Core (skip in enterprise mode)
1950
+ const status = isEnterprise
1951
+ ? { email: "", plan: "enterprise", quotaUsed: 0, quotaTotal: 999_999_999, isAutonomous: false, resetAt: "" }
1952
+ : await getAccountStatus(creds.apiKey, config.coreUrl);
1953
+ const mode = status.isAutonomous ? "autonomous" : "human managed";
1954
+ const quotaDisplay = `${status.quotaUsed}/${status.quotaTotal}/day`;
1955
+ const lines = [
1956
+ "**MoltGuard Status**",
1957
+ "",
1958
+ `- API Key: ${maskApiKey(creds.apiKey)}`,
1959
+ `- Agent ID: ${creds.agentId}`,
1960
+ `- Email: ${status.email || "(not set)"}`,
1961
+ `- Plan: ${isEnterprise ? "enterprise" : status.plan}`,
1962
+ `- Quota: ${isEnterprise ? "unlimited" : quotaDisplay}${!isEnterprise && status.resetAt ? " (resets at UTC 0:00)" : ""}`,
1963
+ `- Mode: ${isEnterprise ? "enterprise" : mode}`,
1964
+ `- Authorization: Bearer <local-mac>`,
1965
+ ...(isEnterprise ? [`- Core: ${config.coreUrl}`] : []),
1966
+ `- blockOnRisk: ${config.blockOnRisk}`,
1967
+ ...getBehaviorHooksStatusText(),
1968
+ "",
1969
+ "Commands:",
1970
+ ...(isEnterprise ? [] : [
1971
+ "- /og_core — Open Core portal to upgrade plan",
1972
+ "- /og_claim — Show agent info for claiming",
1973
+ ]),
1974
+ "- /ct-scan — 执行本地混合安全巡检脚本",
1975
+ "- /ct_scan — 执行巡检并追加 --push",
1976
+ "- /ct_guard on|off|status — changewayguard 实时防护开关",
1977
+ "- /plugin_regist <激活码> — 输入激活码完成授权",
1978
+ "- /og_config — Configure API key",
1979
+ ];
1980
+ return { text: lines.join("\n") };
1981
+ },
1982
+ });
1983
+ // // 配置管理
1984
+ api.registerCommand({
1985
+ name: "og_config",
1986
+ description: "Show how to configure API key for cross-machine sharing",
1987
+ requireAuth: true,
1988
+ handler: async () => {
1989
+ // Show configuration instructions
1990
+ // Note: OpenClaw commands don't support arguments directly.
1991
+ // Users configure API key via openclaw.json or environment variable.
1992
+ return {
1993
+ text: [
1994
+ "**Configure MoltGuard API Key**",
1995
+ "",
1996
+ "To use an existing API key (e.g., from a paid plan) across multiple machines:",
1997
+ "",
1998
+ "**Option 1: Edit openclaw.json**",
1999
+ "```json",
2000
+ "{",
2001
+ ' "plugins": {',
2002
+ ' "entries": {',
2003
+ ' "changewayguard": {',
2004
+ ' "config": { "apiKey": "sk-og-<your-key>" }',
2005
+ " }",
2006
+ " }",
2007
+ " }",
2008
+ "}",
2009
+ "```",
2010
+ "",
2011
+ "**Option 2: Environment variable**",
2012
+ "```bash",
2013
+ "export OG_API_KEY=sk-og-<your-key>",
2014
+ "```",
2015
+ "",
2016
+ "Then restart the gateway: `openclaw gateway restart`",
2017
+ "",
2018
+ `Get your API key from: ${config.coreUrl}/login`,
2019
+ "",
2020
+ `Current API key: ${globalCoreCredentials?.apiKey ? maskApiKey(globalCoreCredentials.apiKey) : "(none)"}`,
2021
+ ].join("\n"),
2022
+ };
2023
+ },
2024
+ });
2025
+ api.registerCommand({
2026
+ name: "ct_guard",
2027
+ description: "Enable/disable behavior hooks and remote detection requests",
2028
+ requireAuth: true,
2029
+ acceptsArgs: true,
2030
+ handler: async (ctx) => {
2031
+ const command = (ctx.args?.trim().toLowerCase() || "status");
2032
+ deviceActivated = getActivationStatus().activated;
2033
+ if (command === "on") {
2034
+ if (!deviceActivated) {
2035
+ applyBehaviorHooksCredentials();
2036
+ return {
2037
+ text: [
2038
+ "**changewayguard 实时防护未开启**",
2039
+ "",
2040
+ "设备未激活,默认关闭且不可开启。",
2041
+ "请先执行:`/plugin_regist ACT-XXXX-XXXX-XXXX`",
2042
+ "",
2043
+ ...getBehaviorHooksStatusText(),
2044
+ ].join("\n"),
2045
+ };
2046
+ }
2047
+ behaviorHooksPreference = true;
2048
+ saveBehaviorHooksPreference(true);
2049
+ applyBehaviorHooksCredentials();
2050
+ await initBusinessFeatures(config.coreUrl);
2051
+ return {
2052
+ text: [
2053
+ "**changewayguard 实时防护已开启**",
2054
+ "",
2055
+ ...getBehaviorHooksStatusText(),
2056
+ ].join("\n"),
2057
+ };
2058
+ }
2059
+ if (command === "off") {
2060
+ behaviorHooksPreference = false;
2061
+ saveBehaviorHooksPreference(false);
2062
+ applyBehaviorHooksCredentials();
2063
+ if (globalConfigSync) {
2064
+ globalConfigSync.stop();
2065
+ globalConfigSync = null;
2066
+ }
2067
+ if (globalBusinessReporter) {
2068
+ globalBusinessReporter.setCredentials(null);
2069
+ await globalBusinessReporter.stop();
2070
+ globalBusinessReporter = null;
2071
+ }
2072
+ if (autoScanEnabled && globalFileWatcher?.running) {
2073
+ globalFileWatcher.stop();
2074
+ autoScanEnabled = false;
2075
+ }
2076
+ return {
2077
+ text: [
2078
+ "**changewayguard 实时防护已关闭**",
2079
+ "",
2080
+ "自动扫描已停止(如之前已开启)。",
2081
+ ...getBehaviorHooksStatusText(),
2082
+ ].join("\n"),
2083
+ };
2084
+ }
2085
+ return {
2086
+ text: [
2087
+ "**changewayguard 实时防护状态**",
2088
+ "",
2089
+ ...getBehaviorHooksStatusText(),
2090
+ "",
2091
+ "Usage:",
2092
+ " /ct_guard on — 开启实时防护(需设备已激活)",
2093
+ " /ct_guard off — 关闭实时防护(立即停止自动检测请求)",
2094
+ " /ct_guard status — 查看当前状态",
2095
+ ].join("\n"),
2096
+ };
2097
+ },
2098
+ });
2099
+ api.registerCommand({
2100
+ name: "plugin_regist",
2101
+ description: "Activate plugin with an activation code",
2102
+ requireAuth: true,
2103
+ acceptsArgs: true,
2104
+ handler: async (ctx) => {
2105
+ const rawArgs = ctx.args?.trim() ?? "";
2106
+ deviceActivated = getActivationStatus().activated;
2107
+ applyBehaviorHooksCredentials();
2108
+ const status = getActivationStatus();
2109
+ if (!rawArgs || rawArgs.toLowerCase() === "status") {
2110
+ return {
2111
+ text: [
2112
+ "**插件激活状态**",
2113
+ "",
2114
+ `- 设备 ID: ${status.deviceId}`,
2115
+ `- 激活状态: ${status.activated ? "已激活" : "未激活"}`,
2116
+ `- 激活时间: ${status.activatedAt ?? "(未激活)"}`,
2117
+ "",
2118
+ "**使用方式**",
2119
+ " /plugin_regist ACT-XXXX-XXXX-XXXX",
2120
+ ].join("\n"),
2121
+ };
2122
+ }
2123
+ const result = await activateDeviceByCode(config.coreUrl, rawArgs);
2124
+ if (!result.ok) {
2125
+ return {
2126
+ text: [
2127
+ "**激活失败**",
2128
+ "",
2129
+ result.message,
2130
+ ].join("\n"),
2131
+ };
2132
+ }
2133
+ deviceActivated = true;
2134
+ // Activation success should enable hooks by default.
2135
+ behaviorHooksPreference = true;
2136
+ saveBehaviorHooksPreference(true);
2137
+ applyBehaviorHooksCredentials();
2138
+ const keys = loadActivationKeys();
2139
+ return {
2140
+ text: [
2141
+ "**激活成功**",
2142
+ "",
2143
+ `- 设备 ID: ${result.status.deviceId}`,
2144
+ `- 激活时间: ${result.status.activatedAt ?? new Date().toISOString()}`,
2145
+ `- API Key: ${keys?.apiKey ? maskApiKey(keys.apiKey) : "(已保存)"}`,
2146
+ ...getBehaviorHooksStatusText(),
2147
+ "",
2148
+ result.message,
2149
+ ].join("\n"),
2150
+ };
2151
+ },
2152
+ });
2153
+ api.registerCommand({
2154
+ name: "og_core",
2155
+ description: "Open Core portal for account and billing",
2156
+ requireAuth: true,
2157
+ handler: async () => {
2158
+ return {
2159
+ text: [
2160
+ `**${BRAND_NAME} Core Portal**`,
2161
+ "",
2162
+ "Manage your account, view usage, and upgrade your plan:",
2163
+ "",
2164
+ ` ${config.coreUrl}/login`,
2165
+ "",
2166
+ "Enter your email to receive a magic login link.",
2167
+ ].join("\n"),
2168
+ };
2169
+ },
2170
+ });
2171
+ api.registerCommand({
2172
+ name: "ct_dashboard",
2173
+ description: "Start local Dashboard and get access URLs",
2174
+ requireAuth: true,
2175
+ handler: async () => {
2176
+ const creds = ensureCoreCredentials(config.coreUrl);
2177
+ // Import dashboard launcher (dynamic to avoid circular deps)
2178
+ const { startLocalDashboard, DevModeError } = await import("./dashboard-launcher.js");
2179
+ try {
2180
+ const result = await startLocalDashboard({
2181
+ apiKey: creds.apiKey,
2182
+ agentId: creds.agentId,
2183
+ coreUrl: config.coreUrl,
2184
+ });
2185
+ return {
2186
+ text: [
2187
+ "**Dashboard URL**",
2188
+ "",
2189
+ result.localUrl,
2190
+ ].join("\n"),
2191
+ };
2192
+ }
2193
+ catch (err) {
2194
+ // Development mode: show instructions for manual startup
2195
+ if (err instanceof DevModeError) {
2196
+ return { text: err.getInstructions() };
2197
+ }
2198
+ return {
2199
+ text: [
2200
+ "**Dashboard Startup Failed**",
2201
+ "",
2202
+ `Error: ${err}`,
2203
+ "",
2204
+ "Try running the Dashboard manually:",
2205
+ " cd dashboard && pnpm dev",
2206
+ ].join("\n"),
2207
+ };
2208
+ }
2209
+ },
2210
+ });
2211
+ api.registerCommand({
2212
+ name: "ct-scan",
2213
+ description: "Run local changeway hybrid audit script",
2214
+ requireAuth: true,
2215
+ handler: async () => {
2216
+ const result = await runCtScanCommand(log);
2217
+ return { text: result.text };
2218
+ },
2219
+ });
2220
+ api.registerCommand({
2221
+ name: "ct_scan",
2222
+ description: "Run /ct-scan with --push",
2223
+ requireAuth: true,
2224
+ handler: async () => {
2225
+ const result = await runCtScanCommand(log, ["--push"]);
2226
+ return { text: result.text };
2227
+ },
2228
+ });
2229
+ api.registerCommand({
2230
+ name: "og_claim",
2231
+ description: "Display agent ID and API key for claiming on Core",
2232
+ requireAuth: true,
2233
+ handler: async () => {
2234
+ const creds = ensureCoreCredentials(config.coreUrl);
2235
+ // Get current status to check if already claimed
2236
+ const status = await getAccountStatus(creds.apiKey, config.coreUrl);
2237
+ if (status.email) {
2238
+ return {
2239
+ text: [
2240
+ "**Agent Already Claimed**",
2241
+ "",
2242
+ `This agent is already linked to: ${status.email}`,
2243
+ "",
2244
+ `Agent ID: ${creds.agentId}`,
2245
+ `Plan: ${status.plan}`,
2246
+ `Quota: ${status.quotaUsed}/${status.quotaTotal}`,
2247
+ "",
2248
+ `Manage at: ${config.coreUrl}/login`,
2249
+ ].join("\n"),
2250
+ };
2251
+ }
2252
+ return {
2253
+ text: [
2254
+ "**Claim Your Agent**",
2255
+ "",
2256
+ "Copy and paste these credentials to claim this agent on the Core platform:",
2257
+ "",
2258
+ "```",
2259
+ `Agent ID: ${creds.agentId}`,
2260
+ `API Key: ${creds.apiKey}`,
2261
+ "```",
2262
+ "",
2263
+ "Steps:",
2264
+ `1. Go to ${config.coreUrl}/login and enter your email`,
2265
+ "2. Click the magic link in your email to log in",
2266
+ `3. Go to ${config.coreUrl}/claim-agent`,
2267
+ "4. Paste the Agent ID and API Key above",
2268
+ "",
2269
+ "After claiming, all your agents share the same quota.",
2270
+ ].join("\n"),
2271
+ };
2272
+ },
2273
+ });
2274
+ // 开启内容脱敏
2275
+ api.registerCommand({
2276
+ name: "og_sanitize",
2277
+ description: "Enable/disable AI Security Gateway for data sanitization",
2278
+ requireAuth: true,
2279
+ acceptsArgs: true,
2280
+ handler: async (ctx) => {
2281
+ const command = ctx.args?.trim().toLowerCase();
2282
+ if (command === "on") {
2283
+ // Enable gateway (only modifies agent configs, gateway is always running)
2284
+ try {
2285
+ const result = await enableGateway();
2286
+ return {
2287
+ text: [
2288
+ "**AI Security Gateway Enabled**",
2289
+ "",
2290
+ "All LLM requests will now be sanitized before being sent to providers.",
2291
+ "Sensitive data (API keys, PII, credentials) will be automatically detected and replaced with placeholders.",
2292
+ "",
2293
+ `- Gateway URL: http://127.0.0.1:53669`,
2294
+ `- Providers protected: ${result.providers.join(", ")}`,
2295
+ "",
2296
+ result.warnings.length > 0 ? "**Warnings:**" : "",
2297
+ ...result.warnings.map(w => ` ${w}`),
2298
+ result.warnings.length > 0 ? "" : "",
2299
+ "**IMPORTANT:** Do not add/modify providers in openclaw.json while Gateway is enabled.",
2300
+ "To add/modify providers:",
2301
+ " 1. Run `/og_sanitize off`",
2302
+ " 2. Modify openclaw.json",
2303
+ " 3. Run `/og_sanitize on`",
2304
+ "",
2305
+ "Configuration modified: ~/.openclaw/openclaw.json",
2306
+ "To disable, run: `/og_sanitize off`",
2307
+ ].filter(Boolean).join("\n"),
2308
+ };
2309
+ }
2310
+ catch (err) {
2311
+ return {
2312
+ text: [
2313
+ "**Failed to Enable Gateway**",
2314
+ "",
2315
+ `Error: ${err instanceof Error ? err.message : String(err)}`,
2316
+ "",
2317
+ "The AI Security Gateway is bundled with MoltGuard.",
2318
+ "If you see this error, please report it as a bug.",
2319
+ ].join("\n"),
2320
+ };
2321
+ }
2322
+ }
2323
+ else if (command === "off") {
2324
+ // Disable gateway (only restores agent configs, gateway keeps running)
2325
+ try {
2326
+ const status = getGatewayStatus();
2327
+ if (!status.enabled) {
2328
+ return {
2329
+ text: "AI Security Gateway is not currently enabled.",
2330
+ };
2331
+ }
2332
+ const result = disableGateway();
2333
+ return {
2334
+ text: [
2335
+ "**AI Security Gateway Disabled**",
2336
+ "",
2337
+ "LLM requests will now go directly to providers (no sanitization).",
2338
+ "",
2339
+ `- Providers restored: ${result.providers.join(", ")}`,
2340
+ "",
2341
+ result.warnings.length > 0 ? "**Warnings:**" : "",
2342
+ ...result.warnings.map(w => ` ${w}`),
2343
+ result.warnings.length > 0 ? "" : "",
2344
+ "Configuration restored: ~/.openclaw/openclaw.json",
2345
+ "Note: Gateway server continues running in the plugin process.",
2346
+ ].filter(Boolean).join("\n"),
2347
+ };
2348
+ }
2349
+ catch (err) {
2350
+ return {
2351
+ text: [
2352
+ "**Failed to Disable Gateway**",
2353
+ "",
2354
+ `Error: ${err instanceof Error ? err.message : String(err)}`,
2355
+ ].join("\n"),
2356
+ };
2357
+ }
2358
+ }
2359
+ else {
2360
+ // Show status
2361
+ const status = getGatewayStatus();
2362
+ return {
2363
+ text: [
2364
+ "**AI Security Gateway Status**",
2365
+ "",
2366
+ `- Enabled: ${status.enabled ? "Yes" : "No"}`,
2367
+ `- Running: ${status.running ? "Yes" : "No"}`,
2368
+ `- URL: ${status.url}`,
2369
+ "",
2370
+ status.enabled && status.providers.length > 0
2371
+ ? `Protected providers: ${status.providers.join(", ")}`
2372
+ : "",
2373
+ "",
2374
+ "Usage:",
2375
+ " /og_sanitize on — Enable data sanitization",
2376
+ " /og_sanitize off — Disable data sanitization",
2377
+ "",
2378
+ "The AI Security Gateway protects sensitive data before sending to LLMs:",
2379
+ "- API keys → <SECRET_TOKEN>",
2380
+ "- Email addresses → <EMAIL>",
2381
+ "- SSH keys → <SSH_PRIVATE_KEY>",
2382
+ "- Credit cards → <CREDIT_CARD>",
2383
+ "- And more...",
2384
+ ].filter(Boolean).join("\n"),
2385
+ };
2386
+ }
2387
+ },
2388
+ });
2389
+ api.registerCommand({
2390
+ name: "og_scan",
2391
+ description: "Scan workspace files for security risks (skills, plugins, memories, workspace md files)",
2392
+ requireAuth: true,
2393
+ acceptsArgs: true,
2394
+ handler: async (ctx) => {
2395
+ if (!behaviorHooksEnabled) {
2396
+ return {
2397
+ text: [
2398
+ "**Static Scan Disabled**",
2399
+ "",
2400
+ "changewayguard 实时防护当前关闭,已跳过网络扫描请求。",
2401
+ "未激活设备默认关闭;激活后可用 `/ct_guard on` 开启。",
2402
+ "",
2403
+ ...getBehaviorHooksStatusText(),
2404
+ ].join("\n"),
2405
+ };
2406
+ }
2407
+ ensureCoreCredentials(config.coreUrl);
2408
+ const scanType = ctx.args?.trim().toLowerCase() || "all";
2409
+ // Import workspace scanner
2410
+ const { scanWorkspaceMdFiles, scanFilesByType, getWorkspaceSummary } = await import("./agent/workspace-scanner.js");
2411
+ try {
2412
+ let filesToScan = [];
2413
+ if (scanType === "summary" || scanType === "info") {
2414
+ // Show summary only
2415
+ const summary = await getWorkspaceSummary();
2416
+ return {
2417
+ text: [
2418
+ "**Workspace File Summary**",
2419
+ "",
2420
+ `Total files: ${summary.totalFiles}`,
2421
+ `Total size: ${(summary.totalSizeBytes / 1024).toFixed(1)} KB`,
2422
+ "",
2423
+ "Files by type:",
2424
+ `- Soul: ${summary.byType.soul}`,
2425
+ `- Agent: ${summary.byType.agent}`,
2426
+ `- Memory: ${summary.byType.memory}`,
2427
+ `- Task: ${summary.byType.task}`,
2428
+ `- Skill: ${summary.byType.skill}`,
2429
+ `- Plugin: ${summary.byType.plugin}`,
2430
+ `- Other: ${summary.byType.other}`,
2431
+ "",
2432
+ "Run `/og_scan all` to scan all files for security risks.",
2433
+ ].join("\n"),
2434
+ };
2435
+ }
2436
+ // Determine what to scan
2437
+ if (scanType === "all") {
2438
+ filesToScan = await scanWorkspaceMdFiles();
2439
+ }
2440
+ else if (scanType === "memories" || scanType === "memory") {
2441
+ filesToScan = await scanFilesByType(["memory"]);
2442
+ }
2443
+ else if (scanType === "skills" || scanType === "skill") {
2444
+ filesToScan = await scanFilesByType(["skill"]);
2445
+ }
2446
+ else if (scanType === "plugins" || scanType === "plugin") {
2447
+ filesToScan = await scanFilesByType(["plugin"]);
2448
+ }
2449
+ else if (scanType === "workspace") {
2450
+ filesToScan = await scanFilesByType(["soul", "agent", "task", "other"]);
2451
+ }
2452
+ else {
2453
+ return {
2454
+ text: [
2455
+ "**Usage: /og_scan [type]**",
2456
+ "",
2457
+ "Types:",
2458
+ "- `all` — Scan all workspace files (default)",
2459
+ "- `memories` — Scan memory files only",
2460
+ "- `skills` — Scan skill files only",
2461
+ "- `plugins` — Scan plugin files only",
2462
+ "- `workspace` — Scan workspace md files (soul.md, agent.md, heartbeat.md, etc.)",
2463
+ "- `summary` — Show file count summary without scanning",
2464
+ "",
2465
+ "Examples:",
2466
+ " /og_scan all",
2467
+ " /og_scan memories",
2468
+ " /og_scan workspace",
2469
+ ].join("\n"),
2470
+ };
2471
+ }
2472
+ if (filesToScan.length === 0) {
2473
+ return {
2474
+ text: [
2475
+ "**No Files Found**",
2476
+ "",
2477
+ `No ${scanType === "all" ? "workspace" : scanType} files found to scan.`,
2478
+ ].join("\n"),
2479
+ };
2480
+ }
2481
+ // Ensure dashboard client is initialized for reporting
2482
+ if (!globalDashboardClient) {
2483
+ try {
2484
+ const fs = await import("node:fs");
2485
+ const path = await import("node:path");
2486
+ const os = await import("node:os");
2487
+ const tokenFile = path.join(os.homedir(), ".openclaw", "credentials", "changewayguard", "dashboard-session-token");
2488
+ if (fs.existsSync(tokenFile)) {
2489
+ const tokenData = loadJsonSync(tokenFile);
2490
+ if (tokenData.token) {
2491
+ const port = tokenData.port || 53667;
2492
+ initDashboardClient(tokenData.token, `http://localhost:${port}`);
2493
+ log.info("Dashboard client initialized from session token");
2494
+ }
2495
+ }
2496
+ }
2497
+ catch (err) {
2498
+ log.warn(`Could not initialize dashboard client: ${err}`);
2499
+ }
2500
+ }
2501
+ // Split files into batches of 50 (Core API limit)
2502
+ const BATCH_SIZE = 50;
2503
+ const batches = [];
2504
+ const creds = ensureCoreCredentials(config.coreUrl);
2505
+ for (let i = 0; i < filesToScan.length; i += BATCH_SIZE) {
2506
+ batches.push(filesToScan.slice(i, i + BATCH_SIZE));
2507
+ }
2508
+ // Scan each batch
2509
+ const allResults = [];
2510
+ let totalFilesScanned = 0;
2511
+ let totalRiskFiles = 0;
2512
+ for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
2513
+ const batch = batches[batchIdx];
2514
+ // Call Core API for static scanning
2515
+ const staticScanUrl = withChangewayOpenPrefix(`${config.coreUrl}/api/v1/static/scan`);
2516
+ const staticScanRequest = {
2517
+ agentId: creds.agentId,
2518
+ files: batch,
2519
+ meta: {
2520
+ pluginVersion: PLUGIN_VERSION,
2521
+ clientTimestamp: new Date().toISOString(),
2522
+ batch: `${batchIdx + 1}/${batches.length}`,
2523
+ },
2524
+ };
2525
+ const traceId = createTraceId();
2526
+ logEngineRequest(staticScanUrl, staticScanRequest, traceId);
2527
+ const res = await fetch(staticScanUrl, {
2528
+ method: "POST",
2529
+ headers: {
2530
+ "Content-Type": "application/json",
2531
+ traceId,
2532
+ ...buildSignedAuthHeadersForUrl({
2533
+ method: "POST",
2534
+ url: staticScanUrl,
2535
+ body: staticScanRequest,
2536
+ traceId,
2537
+ }),
2538
+ },
2539
+ body: JSON.stringify(staticScanRequest),
2540
+ });
2541
+ if (!res.ok) {
2542
+ const error = await res.text();
2543
+ logEngineResponse(staticScanUrl, res.status, error || "<empty>", traceId);
2544
+ return {
2545
+ text: [
2546
+ "**Static Scan Failed**",
2547
+ "",
2548
+ `Error in batch ${batchIdx + 1}/${batches.length}: ${error}`,
2549
+ ].join("\n"),
2550
+ };
2551
+ }
2552
+ const data = await res.json();
2553
+ logEngineResponse(staticScanUrl, res.status, data, traceId);
2554
+ if (!data.success) {
2555
+ if (data.data?.quotaExceeded) {
2556
+ return {
2557
+ text: [
2558
+ "**Quota Exceeded**",
2559
+ "",
2560
+ data.data.message || "Your detection quota has been exceeded.",
2561
+ "",
2562
+ `Quota: ${data.data.quotaUsed}/${data.data.quotaTotal}`,
2563
+ "",
2564
+ `Scanned ${totalFilesScanned} files before quota limit.`,
2565
+ "",
2566
+ `To continue scanning, upgrade your plan at: ${config.coreUrl}/login`,
2567
+ ].join("\n"),
2568
+ };
2569
+ }
2570
+ return {
2571
+ text: [
2572
+ "**Static Scan Failed**",
2573
+ "",
2574
+ `Error in batch ${batchIdx + 1}/${batches.length}: ${data.error || "Unknown error"}`,
2575
+ ].join("\n"),
2576
+ };
2577
+ }
2578
+ const batchResult = data.data;
2579
+ allResults.push(...batchResult.results);
2580
+ totalFilesScanned += batchResult.filesScanned;
2581
+ totalRiskFiles += batchResult.riskFiles;
2582
+ // Report batch results to dashboard immediately (non-blocking)
2583
+ if (globalDashboardClient && batchResult.results) {
2584
+ for (const fileResult of batchResult.results) {
2585
+ if (fileResult.riskLevel !== "safe") {
2586
+ globalDashboardClient
2587
+ .reportDetection({
2588
+ agentId: creds.agentId,
2589
+ safe: fileResult.riskLevel === "safe",
2590
+ categories: fileResult.findings.map((f) => f.scanner),
2591
+ findings: fileResult.findings,
2592
+ sensitivityScore: fileResult.riskLevel === "critical" ? 1.0 :
2593
+ fileResult.riskLevel === "high" ? 0.8 :
2594
+ fileResult.riskLevel === "medium" ? 0.6 :
2595
+ fileResult.riskLevel === "low" ? 0.4 : 0.0,
2596
+ latencyMs: 0,
2597
+ scanType: "static",
2598
+ filePath: fileResult.path,
2599
+ fileType: batch.find((f) => f.path === fileResult.path)?.type,
2600
+ })
2601
+ .catch((err) => {
2602
+ log.warn(`Failed to report detection to dashboard: ${err}`);
2603
+ });
2604
+ }
2605
+ }
2606
+ }
2607
+ else if (!globalDashboardClient) {
2608
+ log.warn("Dashboard client not initialized - scan results not reported to dashboard");
2609
+ }
2610
+ // Report static scan results to business reporter
2611
+ if (globalBusinessReporter && batchResult.results) {
2612
+ for (const fileResult of batchResult.results) {
2613
+ const categories = fileResult.findings?.map((f) => f.scanner) ?? [];
2614
+ globalBusinessReporter.recordScanResult("static", categories, fileResult.riskLevel !== "safe");
2615
+ }
2616
+ }
2617
+ }
2618
+ // Combine results from all batches
2619
+ const result = {
2620
+ filesScanned: totalFilesScanned,
2621
+ riskFiles: totalRiskFiles,
2622
+ results: allResults,
2623
+ };
2624
+ // Format results
2625
+ const criticalFiles = result.results.filter((r) => r.riskLevel === "critical");
2626
+ const highFiles = result.results.filter((r) => r.riskLevel === "high");
2627
+ const mediumFiles = result.results.filter((r) => r.riskLevel === "medium");
2628
+ const lowFiles = result.results.filter((r) => r.riskLevel === "low");
2629
+ const safeFiles = result.results.filter((r) => r.riskLevel === "safe");
2630
+ const lines = [
2631
+ "**Static Security Scan Results**",
2632
+ "",
2633
+ `Files scanned: ${result.filesScanned}`,
2634
+ `Files with risks: ${result.riskFiles}`,
2635
+ "",
2636
+ "Risk breakdown:",
2637
+ `- Critical: ${criticalFiles.length}`,
2638
+ `- High: ${highFiles.length}`,
2639
+ `- Medium: ${mediumFiles.length}`,
2640
+ `- Low: ${lowFiles.length}`,
2641
+ `- Safe: ${safeFiles.length}`,
2642
+ ];
2643
+ // Show critical and high risk files with details
2644
+ if (criticalFiles.length > 0) {
2645
+ lines.push("", "**Critical Risks:**");
2646
+ for (const file of criticalFiles.slice(0, 5)) {
2647
+ lines.push(`\n- **${file.path}**`);
2648
+ for (const finding of file.findings.slice(0, 3)) {
2649
+ lines.push(` - [${finding.scanner}] ${finding.message}`);
2650
+ }
2651
+ }
2652
+ if (criticalFiles.length > 5) {
2653
+ lines.push(`\n...and ${criticalFiles.length - 5} more critical files`);
2654
+ }
2655
+ }
2656
+ if (highFiles.length > 0) {
2657
+ lines.push("", "**High Risks:**");
2658
+ for (const file of highFiles.slice(0, 3)) {
2659
+ lines.push(`\n- **${file.path}**`);
2660
+ for (const finding of file.findings.slice(0, 2)) {
2661
+ lines.push(` - [${finding.scanner}] ${finding.message}`);
2662
+ }
2663
+ }
2664
+ if (highFiles.length > 3) {
2665
+ lines.push(`\n...and ${highFiles.length - 3} more high-risk files`);
2666
+ }
2667
+ }
2668
+ // Show summary for medium/low
2669
+ if (mediumFiles.length > 0) {
2670
+ lines.push("", `**Medium Risks:** ${mediumFiles.map((f) => f.path).slice(0, 5).join(", ")}`);
2671
+ if (mediumFiles.length > 5) {
2672
+ lines.push(`...and ${mediumFiles.length - 5} more`);
2673
+ }
2674
+ }
2675
+ if (lowFiles.length > 0) {
2676
+ lines.push("", `**Low Risks:** ${lowFiles.length} files (view in dashboard for details)`);
2677
+ }
2678
+ lines.push("", `Full details available in dashboard: /ct_dashboard`);
2679
+ return { text: lines.join("\n") };
2680
+ }
2681
+ catch (err) {
2682
+ return {
2683
+ text: [
2684
+ "**Static Scan Error**",
2685
+ "",
2686
+ `Error: ${err instanceof Error ? err.message : String(err)}`,
2687
+ ].join("\n"),
2688
+ };
2689
+ }
2690
+ },
2691
+ });
2692
+ // 开启自动扫描 (on)
2693
+ api.registerCommand({
2694
+ name: "og_autoscan",
2695
+ description: "Enable/disable automatic file scanning on workspace changes",
2696
+ requireAuth: true,
2697
+ acceptsArgs: true,
2698
+ handler: async (ctx) => {
2699
+ const command = ctx.args?.trim().toLowerCase();
2700
+ if (command === "on") {
2701
+ if (!behaviorHooksEnabled) {
2702
+ return {
2703
+ text: [
2704
+ "**Auto-Scan Disabled**",
2705
+ "",
2706
+ "changewayguard 实时防护当前关闭,自动扫描不会发起网络扫描请求。",
2707
+ "未激活设备默认关闭;激活后可用 `/ct_guard on` 开启。",
2708
+ "",
2709
+ ...getBehaviorHooksStatusText(),
2710
+ ].join("\n"),
2711
+ };
2712
+ }
2713
+ if (autoScanEnabled && globalFileWatcher?.running) {
2714
+ return {
2715
+ text: "Auto-scan is already enabled.",
2716
+ };
2717
+ }
2718
+ ensureCoreCredentials(config.coreUrl);
2719
+ // Create file watcher
2720
+ globalFileWatcher = new FileWatcher({
2721
+ onFilesChanged: async (changedFiles) => {
2722
+ // 1. 检查是否启用
2723
+ if (!behaviorHooksEnabled)
2724
+ return;
2725
+ if (!globalCoreCredentials)
2726
+ return;
2727
+ const creds = ensureCoreCredentials(config.coreUrl);
2728
+ // Import workspace scanner
2729
+ const { scanWorkspaceMdFiles } = await import("./agent/workspace-scanner.js");
2730
+ // Get file details for changed files
2731
+ // 2. 扫描工作区的 .md 文件
2732
+ const allFiles = await scanWorkspaceMdFiles();
2733
+ const filesToScan = allFiles.filter(f => changedFiles.some(cf => cf.endsWith(f.path)));
2734
+ if (filesToScan.length === 0)
2735
+ return;
2736
+ log.debug?.(`Auto-scanning ${filesToScan.length} changed file(s)...`);
2737
+ // Call Core API for scanning
2738
+ try {
2739
+ const staticScanUrl = withChangewayOpenPrefix(`${config.coreUrl}/api/v1/static/scan`);
2740
+ const staticScanRequest = {
2741
+ agentId: creds.agentId,
2742
+ files: filesToScan,
2743
+ meta: {
2744
+ pluginVersion: PLUGIN_VERSION,
2745
+ clientTimestamp: new Date().toISOString(),
2746
+ },
2747
+ };
2748
+ const traceId = createTraceId();
2749
+ logEngineRequest(staticScanUrl, staticScanRequest, traceId);
2750
+ // 3. 调用云端 API 进行安全扫描
2751
+ const res = await fetch(staticScanUrl, {
2752
+ method: "POST",
2753
+ headers: {
2754
+ "Content-Type": "application/json",
2755
+ traceId,
2756
+ ...buildSignedAuthHeadersForUrl({
2757
+ method: "POST",
2758
+ url: staticScanUrl,
2759
+ body: staticScanRequest,
2760
+ traceId,
2761
+ }),
2762
+ },
2763
+ body: JSON.stringify(staticScanRequest),
2764
+ });
2765
+ if (!res.ok) {
2766
+ const errorText = await res.text().catch(() => "");
2767
+ logEngineResponse(staticScanUrl, res.status, errorText || "<empty>", traceId);
2768
+ return;
2769
+ }
2770
+ const data = await res.json();
2771
+ logEngineResponse(staticScanUrl, res.status, data, traceId);
2772
+ if (!data.success || !data.data)
2773
+ return;
2774
+ const result = data.data;
2775
+ // Report to dashboard
2776
+ if (globalDashboardClient && result.results) {
2777
+ for (const fileResult of result.results) {
2778
+ if (fileResult.riskLevel !== "safe") {
2779
+ // 4. 上报结果到 Dashboard
2780
+ globalDashboardClient
2781
+ .reportDetection({
2782
+ agentId: creds.agentId,
2783
+ safe: fileResult.riskLevel === "safe",
2784
+ categories: fileResult.findings.map((f) => f.scanner),
2785
+ findings: fileResult.findings,
2786
+ sensitivityScore: fileResult.riskLevel === "critical" ? 1.0 :
2787
+ fileResult.riskLevel === "high" ? 0.8 :
2788
+ fileResult.riskLevel === "medium" ? 0.6 :
2789
+ fileResult.riskLevel === "low" ? 0.4 : 0.0,
2790
+ latencyMs: 0,
2791
+ scanType: "static",
2792
+ filePath: fileResult.path,
2793
+ fileType: filesToScan.find((f) => f.path === fileResult.path)?.type,
2794
+ })
2795
+ .catch(() => { });
2796
+ }
2797
+ }
2798
+ // Log summary
2799
+ const riskCount = result.results.filter((r) => r.riskLevel !== "safe").length;
2800
+ if (riskCount > 0) {
2801
+ log.info(`Auto-scan found ${riskCount} file(s) with security risks`);
2802
+ }
2803
+ }
2804
+ // Report auto-scan results to business reporter
2805
+ if (globalBusinessReporter && result.results) {
2806
+ for (const fileResult of result.results) {
2807
+ const categories = fileResult.findings?.map((f) => f.scanner) ?? [];
2808
+ // 5. 上报结果到商业统计
2809
+ globalBusinessReporter.recordScanResult("static", categories, fileResult.riskLevel !== "safe");
2810
+ }
2811
+ }
2812
+ }
2813
+ catch (err) {
2814
+ log.debug?.(`Auto-scan failed: ${err}`);
2815
+ }
2816
+ },
2817
+ logger: log,
2818
+ });
2819
+ globalFileWatcher.start();
2820
+ autoScanEnabled = true;
2821
+ return {
2822
+ text: [
2823
+ "**Auto-Scan Enabled**",
2824
+ "",
2825
+ "Workspace files are now being monitored for changes.",
2826
+ "When a .md file is modified, it will be automatically scanned for security risks.",
2827
+ "",
2828
+ `Watching ${globalFileWatcher.watchCount} directories`,
2829
+ "",
2830
+ "View scan results in Dashboard: `/ct_dashboard`",
2831
+ "",
2832
+ "To disable: `/og_autoscan off`",
2833
+ ].join("\n"),
2834
+ };
2835
+ }
2836
+ else if (command === "off") {
2837
+ if (!autoScanEnabled || !globalFileWatcher?.running) {
2838
+ return {
2839
+ text: "Auto-scan is not currently enabled.",
2840
+ };
2841
+ }
2842
+ globalFileWatcher.stop();
2843
+ autoScanEnabled = false;
2844
+ return {
2845
+ text: [
2846
+ "**Auto-Scan Disabled**",
2847
+ "",
2848
+ "File monitoring stopped. Changes will not trigger automatic scans.",
2849
+ "",
2850
+ "To re-enable: `/og_autoscan on`",
2851
+ ].join("\n"),
2852
+ };
2853
+ }
2854
+ else {
2855
+ // Show status
2856
+ return {
2857
+ text: [
2858
+ "**Auto-Scan Status**",
2859
+ "",
2860
+ `Enabled: ${autoScanEnabled ? "Yes" : "No"}`,
2861
+ globalFileWatcher?.running ? `Watching: ${globalFileWatcher.watchCount} directories` : "",
2862
+ "",
2863
+ "Usage:",
2864
+ " /og_autoscan on — Enable automatic scanning",
2865
+ " /og_autoscan off — Disable automatic scanning",
2866
+ "",
2867
+ "Auto-scan monitors workspace .md files and automatically scans them",
2868
+ "when changes are detected. Results are reported to the dashboard.",
2869
+ ].filter(Boolean).join("\n"),
2870
+ };
2871
+ }
2872
+ },
2873
+ });
2874
+ // 重置凭证
2875
+ api.registerCommand({
2876
+ name: "og_reset",
2877
+ description: "Reset MoltGuard local identity (MAC authorization mode)",
2878
+ requireAuth: true,
2879
+ handler: async () => {
2880
+ const hadCredentials = globalCoreCredentials !== null;
2881
+ const oldAgentId = globalCoreCredentials?.agentId;
2882
+ // Delete credentials file
2883
+ const deleted = deleteCoreCredentials();
2884
+ // Clear in-memory credentials
2885
+ globalCoreCredentials = null;
2886
+ globalBehaviorDetector = null;
2887
+ if (!deleted && !hadCredentials) {
2888
+ return {
2889
+ text: [
2890
+ "**MoltGuard Reset**",
2891
+ "",
2892
+ "No credentials to reset. Local MAC authorization is already active.",
2893
+ ].join("\n"),
2894
+ };
2895
+ }
2896
+ const localCreds = buildLocalCredentials(config.coreUrl);
2897
+ globalCoreCredentials = localCreds;
2898
+ if (!globalBehaviorDetector) {
2899
+ globalBehaviorDetector = new BehaviorDetector({
2900
+ coreUrl: config.coreUrl,
2901
+ assessTimeoutMs: Math.min(config.timeoutMs, 3000),
2902
+ blockOnRisk: config.blockOnRisk,
2903
+ pluginVersion: PLUGIN_VERSION,
2904
+ }, log);
2905
+ }
2906
+ deviceActivated = getActivationStatus().activated;
2907
+ applyBehaviorHooksCredentials();
2908
+ return {
2909
+ text: [
2910
+ "**MoltGuard Reset Complete**",
2911
+ "",
2912
+ oldAgentId ? `- Old Agent ID: ${oldAgentId}` : "",
2913
+ `- New Agent ID: ${localCreds.agentId}`,
2914
+ `- Authorization: Bearer ${localCreds.apiKey}`,
2915
+ ...getBehaviorHooksStatusText(),
2916
+ "",
2917
+ "Registration is disabled. MoltGuard now runs in local MAC authorization mode.",
2918
+ ].filter(Boolean).join("\n"),
2919
+ };
2920
+ },
2921
+ });
2922
+ },
2923
+ //插件卸载
2924
+ async unregister() {
2925
+ if (dashboardHeartbeatTimer) {
2926
+ clearInterval(dashboardHeartbeatTimer);
2927
+ dashboardHeartbeatTimer = null;
2928
+ }
2929
+ if (profileDebounceTimer) {
2930
+ clearTimeout(profileDebounceTimer);
2931
+ profileDebounceTimer = null;
2932
+ }
2933
+ for (const w of profileWatchers) {
2934
+ try {
2935
+ w.close();
2936
+ }
2937
+ catch { /* ignore */ }
2938
+ }
2939
+ profileWatchers = [];
2940
+ // Stop file watcher
2941
+ if (globalFileWatcher) {
2942
+ globalFileWatcher.stop();
2943
+ globalFileWatcher = null;
2944
+ }
2945
+ if (globalWorkspaceAgentsWatcher) {
2946
+ globalWorkspaceAgentsWatcher.stop();
2947
+ globalWorkspaceAgentsWatcher = null;
2948
+ }
2949
+ // Stop event reporter (flush remaining events)
2950
+ if (globalEventReporter) {
2951
+ await globalEventReporter.stop();
2952
+ globalEventReporter = null;
2953
+ }
2954
+ // Stop business reporter (flush remaining telemetry)
2955
+ if (globalBusinessReporter) {
2956
+ await globalBusinessReporter.stop();
2957
+ globalBusinessReporter = null;
2958
+ }
2959
+ // Stop config sync
2960
+ if (globalConfigSync) {
2961
+ globalConfigSync.stop();
2962
+ globalConfigSync = null;
2963
+ }
2964
+ // Stop dashboard client (flush agentic hours)
2965
+ if (globalDashboardClient) {
2966
+ await globalDashboardClient.stop();
2967
+ }
2968
+ // Stop gateway server
2969
+ try {
2970
+ await stopGateway();
2971
+ }
2972
+ catch { /* ignore */ }
2973
+ // Stop personal dashboard process
2974
+ if (personalDashboardStarted) {
2975
+ try {
2976
+ const { stopLocalDashboard } = await import("./dashboard-launcher.js");
2977
+ await stopLocalDashboard();
2978
+ }
2979
+ catch { /* ignore */ }
2980
+ personalDashboardStarted = false;
2981
+ }
2982
+ globalCoreCredentials = null;
2983
+ globalBehaviorDetector = null;
2984
+ globalDashboardClient = null;
2985
+ quotaExceededNotified = false;
2986
+ currentAccountPlan = "free";
2987
+ },
2988
+ };
2989
+ export default openClawGuardPlugin;
2990
+ //# sourceMappingURL=index.js.map