@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.
- package/LICENSE +21 -0
- package/OpenClaw-linux_Mac-Guide-zh.md +89 -0
- package/dashboard-dist/api/122.index.js +95 -0
- package/dashboard-dist/api/122.index.js.map +1 -0
- package/dashboard-dist/api/143.index.js +2734 -0
- package/dashboard-dist/api/143.index.js.map +1 -0
- package/dashboard-dist/api/154.index.js +4151 -0
- package/dashboard-dist/api/154.index.js.map +1 -0
- package/dashboard-dist/api/173.index.js +24112 -0
- package/dashboard-dist/api/173.index.js.map +1 -0
- package/dashboard-dist/api/217.index.js +44 -0
- package/dashboard-dist/api/217.index.js.map +1 -0
- package/dashboard-dist/api/222.index.js +90 -0
- package/dashboard-dist/api/222.index.js.map +1 -0
- package/dashboard-dist/api/280.index.js +213 -0
- package/dashboard-dist/api/280.index.js.map +1 -0
- package/dashboard-dist/api/369.index.js +115 -0
- package/dashboard-dist/api/369.index.js.map +1 -0
- package/dashboard-dist/api/374.index.js +1896 -0
- package/dashboard-dist/api/374.index.js.map +1 -0
- package/dashboard-dist/api/424.index.js +135 -0
- package/dashboard-dist/api/424.index.js.map +1 -0
- package/dashboard-dist/api/445.index.js +3562 -0
- package/dashboard-dist/api/445.index.js.map +1 -0
- package/dashboard-dist/api/555.index.js +496 -0
- package/dashboard-dist/api/555.index.js.map +1 -0
- package/dashboard-dist/api/573.index.js +806 -0
- package/dashboard-dist/api/573.index.js.map +1 -0
- package/dashboard-dist/api/580.index.js +1420 -0
- package/dashboard-dist/api/580.index.js.map +1 -0
- package/dashboard-dist/api/581.index.js +67 -0
- package/dashboard-dist/api/581.index.js.map +1 -0
- package/dashboard-dist/api/598.index.js +328 -0
- package/dashboard-dist/api/598.index.js.map +1 -0
- package/dashboard-dist/api/720.index.js +105 -0
- package/dashboard-dist/api/720.index.js.map +1 -0
- package/dashboard-dist/api/744.index.js +333 -0
- package/dashboard-dist/api/744.index.js.map +1 -0
- package/dashboard-dist/api/818.index.js +374 -0
- package/dashboard-dist/api/818.index.js.map +1 -0
- package/dashboard-dist/api/831.index.js +99 -0
- package/dashboard-dist/api/831.index.js.map +1 -0
- package/dashboard-dist/api/84.index.js +64 -0
- package/dashboard-dist/api/84.index.js.map +1 -0
- package/dashboard-dist/api/900.index.js +81 -0
- package/dashboard-dist/api/900.index.js.map +1 -0
- package/dashboard-dist/api/917.index.js +88 -0
- package/dashboard-dist/api/917.index.js.map +1 -0
- package/dashboard-dist/api/927.index.js +4250 -0
- package/dashboard-dist/api/927.index.js.map +1 -0
- package/dashboard-dist/api/948.index.js +64 -0
- package/dashboard-dist/api/948.index.js.map +1 -0
- package/dashboard-dist/api/982.index.js +67 -0
- package/dashboard-dist/api/982.index.js.map +1 -0
- package/dashboard-dist/api/99.index.js +1176 -0
- package/dashboard-dist/api/99.index.js.map +1 -0
- package/dashboard-dist/api/drizzle/sqlite/0000_short_captain_stacy.sql +70 -0
- package/dashboard-dist/api/drizzle/sqlite/0001_closed_magus.sql +10 -0
- package/dashboard-dist/api/drizzle/sqlite/0002_agent_capability_observation.sql +38 -0
- package/dashboard-dist/api/drizzle/sqlite/0003_auth_magic_link.sql +28 -0
- package/dashboard-dist/api/drizzle/sqlite/0004_static_scan_fields.sql +8 -0
- package/dashboard-dist/api/drizzle/sqlite/0005_gateway_activity.sql +24 -0
- package/dashboard-dist/api/drizzle/sqlite/0006_sour_marauders.sql +41 -0
- package/dashboard-dist/api/drizzle/sqlite/meta/0000_snapshot.json +460 -0
- package/dashboard-dist/api/drizzle/sqlite/meta/0001_snapshot.json +536 -0
- package/dashboard-dist/api/drizzle/sqlite/meta/0006_snapshot.json +1249 -0
- package/dashboard-dist/api/drizzle/sqlite/meta/_journal.json +55 -0
- package/dashboard-dist/api/index.js +28482 -0
- package/dashboard-dist/api/index.js.map +1 -0
- package/dashboard-dist/api/package.json +16 -0
- package/dashboard-dist/api/sourcemap-register.cjs +1 -0
- package/dashboard-dist/web/assets/index-BKUfzbIg.js +148 -0
- package/dashboard-dist/web/assets/index-rHRH99IQ.css +1 -0
- package/dashboard-dist/web/changeway-logo.png +0 -0
- package/dashboard-dist/web/favicon.svg +29 -0
- package/dashboard-dist/web/index.html +15 -0
- package/dashboard-dist/web/logo.svg +16 -0
- package/dist/agent/activation.d.ts +21 -0
- package/dist/agent/activation.d.ts.map +1 -0
- package/dist/agent/activation.js +94 -0
- package/dist/agent/activation.js.map +1 -0
- package/dist/agent/auth.d.ts +73 -0
- package/dist/agent/auth.d.ts.map +1 -0
- package/dist/agent/auth.js +363 -0
- package/dist/agent/auth.js.map +1 -0
- package/dist/agent/behavior-detector.d.ts +150 -0
- package/dist/agent/behavior-detector.d.ts.map +1 -0
- package/dist/agent/behavior-detector.js +559 -0
- package/dist/agent/behavior-detector.js.map +1 -0
- package/dist/agent/business-reporter.d.ts +114 -0
- package/dist/agent/business-reporter.d.ts.map +1 -0
- package/dist/agent/business-reporter.js +359 -0
- package/dist/agent/business-reporter.js.map +1 -0
- package/dist/agent/config-sync.d.ts +70 -0
- package/dist/agent/config-sync.d.ts.map +1 -0
- package/dist/agent/config-sync.js +133 -0
- package/dist/agent/config-sync.js.map +1 -0
- package/dist/agent/config.d.ts +98 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +348 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/content-injection-scanner.d.ts +35 -0
- package/dist/agent/content-injection-scanner.d.ts.map +1 -0
- package/dist/agent/content-injection-scanner.js +270 -0
- package/dist/agent/content-injection-scanner.js.map +1 -0
- package/dist/agent/engine-log-writer.d.ts +6 -0
- package/dist/agent/engine-log-writer.d.ts.map +1 -0
- package/dist/agent/engine-log-writer.js +18 -0
- package/dist/agent/engine-log-writer.js.map +1 -0
- package/dist/agent/env.d.ts +19 -0
- package/dist/agent/env.d.ts.map +1 -0
- package/dist/agent/env.js +44 -0
- package/dist/agent/env.js.map +1 -0
- package/dist/agent/event-reporter.d.ts +87 -0
- package/dist/agent/event-reporter.d.ts.map +1 -0
- package/dist/agent/event-reporter.js +306 -0
- package/dist/agent/event-reporter.js.map +1 -0
- package/dist/agent/file-watcher.d.ts +50 -0
- package/dist/agent/file-watcher.d.ts.map +1 -0
- package/dist/agent/file-watcher.js +135 -0
- package/dist/agent/file-watcher.js.map +1 -0
- package/dist/agent/fs-utils.d.ts +22 -0
- package/dist/agent/fs-utils.d.ts.map +1 -0
- package/dist/agent/fs-utils.js +41 -0
- package/dist/agent/fs-utils.js.map +1 -0
- package/dist/agent/gateway-manager.d.ts +59 -0
- package/dist/agent/gateway-manager.d.ts.map +1 -0
- package/dist/agent/gateway-manager.js +583 -0
- package/dist/agent/gateway-manager.js.map +1 -0
- package/dist/agent/hook-types.d.ts +276 -0
- package/dist/agent/hook-types.d.ts.map +1 -0
- package/dist/agent/hook-types.js +51 -0
- package/dist/agent/hook-types.js.map +1 -0
- package/dist/agent/http-client.d.ts +19 -0
- package/dist/agent/http-client.d.ts.map +1 -0
- package/dist/agent/http-client.js +37 -0
- package/dist/agent/http-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +8 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/openclaw-hybrid-audit-changeway.js +1447 -0
- package/dist/agent/prompt-gate.d.ts +16 -0
- package/dist/agent/prompt-gate.d.ts.map +1 -0
- package/dist/agent/prompt-gate.js +58 -0
- package/dist/agent/prompt-gate.js.map +1 -0
- package/dist/agent/prompt-input.d.ts +9 -0
- package/dist/agent/prompt-input.d.ts.map +1 -0
- package/dist/agent/prompt-input.js +173 -0
- package/dist/agent/prompt-input.js.map +1 -0
- package/dist/agent/prompt-output.d.ts +4 -0
- package/dist/agent/prompt-output.d.ts.map +1 -0
- package/dist/agent/prompt-output.js +19 -0
- package/dist/agent/prompt-output.js.map +1 -0
- package/dist/agent/runner.d.ts +23 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +165 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/runtime-mode.d.ts +10 -0
- package/dist/agent/runtime-mode.d.ts.map +1 -0
- package/dist/agent/runtime-mode.js +19 -0
- package/dist/agent/runtime-mode.js.map +1 -0
- package/dist/agent/sanitizer.d.ts +10 -0
- package/dist/agent/sanitizer.d.ts.map +1 -0
- package/dist/agent/sanitizer.js +175 -0
- package/dist/agent/sanitizer.js.map +1 -0
- package/dist/agent/scan-activity.d.ts +19 -0
- package/dist/agent/scan-activity.d.ts.map +1 -0
- package/dist/agent/scan-activity.js +34 -0
- package/dist/agent/scan-activity.js.map +1 -0
- package/dist/agent/types.d.ts +177 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +5 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/agent/workspace-scanner.d.ts +35 -0
- package/dist/agent/workspace-scanner.d.ts.map +1 -0
- package/dist/agent/workspace-scanner.js +137 -0
- package/dist/agent/workspace-scanner.js.map +1 -0
- package/dist/dashboard-launcher.d.ts +52 -0
- package/dist/dashboard-launcher.d.ts.map +1 -0
- package/dist/dashboard-launcher.js +363 -0
- package/dist/dashboard-launcher.js.map +1 -0
- package/dist/gateway/activity.d.ts +52 -0
- package/dist/gateway/activity.d.ts.map +1 -0
- package/dist/gateway/activity.js +111 -0
- package/dist/gateway/activity.js.map +1 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.d.ts.map +1 -0
- package/dist/gateway/config.js +200 -0
- package/dist/gateway/config.js.map +1 -0
- package/dist/gateway/handlers/anthropic.d.ts +12 -0
- package/dist/gateway/handlers/anthropic.d.ts.map +1 -0
- package/dist/gateway/handlers/anthropic.js +254 -0
- package/dist/gateway/handlers/anthropic.js.map +1 -0
- package/dist/gateway/handlers/gemini.d.ts +12 -0
- package/dist/gateway/handlers/gemini.d.ts.map +1 -0
- package/dist/gateway/handlers/gemini.js +101 -0
- package/dist/gateway/handlers/gemini.js.map +1 -0
- package/dist/gateway/handlers/models.d.ts +4 -0
- package/dist/gateway/handlers/models.d.ts.map +1 -0
- package/dist/gateway/handlers/models.js +36 -0
- package/dist/gateway/handlers/models.js.map +1 -0
- package/dist/gateway/handlers/openai.d.ts +16 -0
- package/dist/gateway/handlers/openai.d.ts.map +1 -0
- package/dist/gateway/handlers/openai.js +254 -0
- package/dist/gateway/handlers/openai.js.map +1 -0
- package/dist/gateway/index.d.ts +27 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +290 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/mapping-store.d.ts +38 -0
- package/dist/gateway/mapping-store.d.ts.map +1 -0
- package/dist/gateway/mapping-store.js +74 -0
- package/dist/gateway/mapping-store.js.map +1 -0
- package/dist/gateway/restorer.d.ts +63 -0
- package/dist/gateway/restorer.d.ts.map +1 -0
- package/dist/gateway/restorer.js +284 -0
- package/dist/gateway/restorer.js.map +1 -0
- package/dist/gateway/sanitizer.d.ts +17 -0
- package/dist/gateway/sanitizer.d.ts.map +1 -0
- package/dist/gateway/sanitizer.js +228 -0
- package/dist/gateway/sanitizer.js.map +1 -0
- package/dist/gateway/types.d.ts +53 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +5 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2990 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/store.d.ts +82 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +194 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/platform-client/index.d.ts +63 -0
- package/dist/platform-client/index.d.ts.map +1 -0
- package/dist/platform-client/index.js +294 -0
- package/dist/platform-client/index.js.map +1 -0
- package/dist/platform-client/types.d.ts +109 -0
- package/dist/platform-client/types.d.ts.map +1 -0
- package/dist/platform-client/types.js +3 -0
- package/dist/platform-client/types.js.map +1 -0
- package/dist/workspace-agents-guide.d.ts +22 -0
- package/dist/workspace-agents-guide.d.ts.map +1 -0
- package/dist/workspace-agents-guide.js +92 -0
- package/dist/workspace-agents-guide.js.map +1 -0
- package/dist/workspace-agents-sync.d.ts +24 -0
- package/dist/workspace-agents-sync.d.ts.map +1 -0
- package/dist/workspace-agents-sync.js +41 -0
- package/dist/workspace-agents-sync.js.map +1 -0
- package/dist/workspace-agents-watcher.d.ts +23 -0
- package/dist/workspace-agents-watcher.d.ts.map +1 -0
- package/dist/workspace-agents-watcher.js +152 -0
- package/dist/workspace-agents-watcher.js.map +1 -0
- package/dist/workspace-discovery.d.ts +11 -0
- package/dist/workspace-discovery.d.ts.map +1 -0
- package/dist/workspace-discovery.js +116 -0
- package/dist/workspace-discovery.js.map +1 -0
- package/gateway/package-lock.json +597 -0
- package/gateway/package.json +57 -0
- package/gateway/pnpm-lock.yaml +342 -0
- package/gateway/src/activity.ts +142 -0
- package/gateway/src/config.ts +246 -0
- package/gateway/src/handlers/anthropic.ts +328 -0
- package/gateway/src/handlers/gemini.ts +122 -0
- package/gateway/src/handlers/models.ts +45 -0
- package/gateway/src/handlers/openai.ts +333 -0
- package/gateway/src/index.ts +344 -0
- package/gateway/src/mapping-store.ts +88 -0
- package/gateway/src/restorer.ts +322 -0
- package/gateway/src/sanitizer.ts +298 -0
- package/gateway/src/types.ts +73 -0
- package/gateway/tsconfig.json +20 -0
- package/openclaw.plugin.json +86 -0
- package/package.json +74 -0
- package/samples/Untitled +1 -0
- package/samples/clean-email.txt +20 -0
- package/samples/test-document.md +53 -0
- package/samples/test-email-popup.txt +44 -0
- package/samples/test-email.txt +32 -0
- 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
|