@dcrays/mobook-security-plugin 2.0.19 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { realpathSync } from "node:fs";
3
5
 
4
6
  // ============================================================
5
7
  // MoBook 安全防护插件 v2.0
@@ -280,6 +282,8 @@ function sanitizeParamsForLog(params) {
280
282
 
281
283
  const FETCH_WRAPPED_KEY = Symbol.for("mobook-security-plugin.fetch-wrapped");
282
284
  const ORIGINAL_FETCH_KEY = Symbol.for("mobook-security-plugin.original-fetch");
285
+ const UNDICI_WRAPPED_KEY = Symbol.for("mobook-security-plugin.undici-wrapped");
286
+ const UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
283
287
 
284
288
  // 保存原始 fetch
285
289
  function saveOriginalFetch() {
@@ -628,15 +632,145 @@ function installFetchInterceptor(api) {
628
632
  return true;
629
633
  }
630
634
 
635
+ // ============================================================
636
+ // 5b. undici.fetch 劫持 — 兼容 openclaw 2026.6.11+ 的 LLM 调用路径
637
+ // openclaw 2026.6.11+ 使用 undici.fetch 而非 globalThis.fetch 发送 LLM 请求
638
+ // 通过注入 globalThis["__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__"] 让
639
+ // openclaw 的 loadUndiciRuntimeDeps() 使用被包装的 undici.fetch
640
+ // ============================================================
641
+
642
+ function installUndiciInterceptor(api) {
643
+ if (globalThis[UNDICI_WRAPPED_KEY]) {
644
+ log("undici 拦截器已安装(避免重复包装)", "DEBUG");
645
+ return true;
646
+ }
647
+
648
+ try {
649
+ // 通过 createRequire 加载 openclaw 进程中的 undici 模块
650
+ const pluginRequire = createRequire(import.meta.url);
651
+ const argv1 = process.argv[1];
652
+ if (!argv1) {
653
+ log("⚠️ process.argv[1] 为空,无法定位 openclaw 安装路径,undici 拦截器未安装", "WARN");
654
+ return false;
655
+ }
656
+ const realPath = realpathSync(path.resolve(argv1));
657
+ const hostRequire = createRequire(realPath);
658
+ const undici = hostRequire("undici");
659
+
660
+ const originalUndiciFetch = undici.fetch;
661
+ if (!originalUndiciFetch) {
662
+ log("⚠️ undici.fetch 不可用,undici 拦截器未安装", "WARN");
663
+ return false;
664
+ }
665
+
666
+ let undiciCallId = 0;
667
+
668
+ const wrappedUndiciFetch = async function(input, init) {
669
+ const thisCallId = ++undiciCallId;
670
+ const url = getUrlFromFetchArgs(input);
671
+
672
+ // 只拦截 LLM 相关请求
673
+ const isLlmCall = url.includes("/chat/completions") || url.includes("/v1/chat") || url.includes("/completions");
674
+ if (!isLlmCall) {
675
+ return originalUndiciFetch(input, init);
676
+ }
677
+
678
+ const method = init?.method || (input instanceof Request ? input.method : "GET") || "GET";
679
+ const reqHeaders = getMergedRequestHeaders(input, init);
680
+ const reqBodyText = await getRequestBodyText(input, init);
681
+
682
+ log(`[UNDICI-LLM-REQ #${thisCallId}] ${method} ${url.slice(0, 120)}`, "DEBUG");
683
+
684
+ // ---- 请求侧检查:禁止调用指定大模型端点 ----
685
+ const BLOCKED_LLM_ENDPOINTS = ["ai-clawbot.shuwenda.com"];
686
+ if (process.env.MOBOOK_ALLOW_SHUWENDA !== "1") {
687
+ try {
688
+ const urlObj = new URL(url);
689
+ if (BLOCKED_LLM_ENDPOINTS.some(host => urlObj.hostname === host)) {
690
+ log(`[UNDICI-LLM-REQ-BLOCK #${thisCallId}] 禁止调用被封禁的大模型端点: ${urlObj.hostname}`, "WARN");
691
+ alert(`UNDICI_BLOCKED_LLM_ENDPOINT: ${url.slice(0, 200)}`);
692
+ const wantsSse = guessRequestWantsSse(url, reqHeaders, reqBodyText);
693
+ return makeBlockedResponse(wantsSse, "⚠️ 安全策略:该大模型端点已被禁止调用");
694
+ }
695
+ } catch {}
696
+ }
697
+
698
+ // ---- 放行请求,获取响应 ----
699
+ const fetchStart = Date.now();
700
+ let resp;
701
+ try {
702
+ resp = await originalUndiciFetch(input, init);
703
+ } catch (e) {
704
+ log(`[UNDICI-LLM-FETCH-ERR #${thisCallId}] ${e.message}`, "ERROR");
705
+ throw e;
706
+ }
707
+ const durationMs = Date.now() - fetchStart;
708
+
709
+ // ---- 响应侧检查:敏感信息脱敏 ----
710
+ const sse = isSseResponse(resp);
711
+ let respBody = "";
712
+ try {
713
+ respBody = await resp.clone().text();
714
+ } catch {
715
+ respBody = "[unreadable]";
716
+ }
717
+
718
+ const respText = extractTextFromResponseBody(respBody, sse);
719
+ if (!respText) {
720
+ log(`[UNDICI-LLM-RESP #${thisCallId}] 空响应体,放行 | ${durationMs}ms`, "DEBUG");
721
+ return resp;
722
+ }
723
+
724
+ const { sanitized, matches } = sanitizeContent(respText);
725
+ if (matches.length > 0) {
726
+ alert(`UNDICI_LLM_RESPONSE_SANITIZED [RESP #${thisCallId}]: 脱敏类型=${matches.join(", ")} | 原文长度=${respText.length} | 脱敏后长度=${sanitized.length} | 耗时=${durationMs}ms`);
727
+ log(`[UNDICI-LLM-RESP-SANITIZE #${thisCallId}] sanitized ${matches.join(", ")} (${respText.length}→${sanitized.length} chars)`, "WARN");
728
+
729
+ const patchedBody = patchResponseBody(respBody, sse, sanitized);
730
+ const headers = new Headers(resp.headers);
731
+ headers.delete("content-length");
732
+ headers.delete("content-encoding");
733
+ return new Response(patchedBody, {
734
+ status: resp.status,
735
+ statusText: resp.statusText,
736
+ headers
737
+ });
738
+ }
739
+
740
+ log(`[UNDICI-LLM-RESP #${thisCallId}] 无敏感信息,放行 | ${durationMs}ms`, "DEBUG");
741
+ return resp;
742
+ };
743
+
744
+ // 注入到 globalThis,让 openclaw 的 loadUndiciRuntimeDeps() 使用包装版
745
+ globalThis[UNDICI_RUNTIME_DEPS_KEY] = {
746
+ Agent: undici.Agent,
747
+ EnvHttpProxyAgent: undici.EnvHttpProxyAgent,
748
+ ProxyAgent: undici.ProxyAgent,
749
+ FormData: undici.FormData,
750
+ fetch: wrappedUndiciFetch
751
+ };
752
+ globalThis[UNDICI_WRAPPED_KEY] = true;
753
+
754
+ log("✅ undici.fetch 拦截器已安装 — 兼容 openclaw 2026.6.11+ LLM 调用路径", "INFO");
755
+ return true;
756
+ } catch (e) {
757
+ log(`⚠️ undici 拦截器安装失败: ${e.message}`, "ERROR");
758
+ return false;
759
+ }
760
+ }
761
+
631
762
  // ============================================================
632
763
  // 插件注册
633
764
  // ============================================================
634
765
  export default function register(api) {
635
766
  log("MoBook 安全防护插件 v2.0 启动");
636
767
 
637
- // ---- 安装 Fetch 拦截器(核心:全渠道 LLM 拦截) ----
768
+ // ---- 安装 Fetch 拦截器(globalThis.fetch 劫持) ----
638
769
  installFetchInterceptor(api);
639
770
 
771
+ // ---- 安装 undici.fetch 拦截器(兼容 openclaw 2026.6.11+) ----
772
+ installUndiciInterceptor(api);
773
+
640
774
  // ---- Hook 1: before_tool_call(命令拦截 + 审计) ----
641
775
  api.on("before_tool_call", async (event, ctx) => {
642
776
  const toolName = event?.toolName || "unknown";
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "id": "mobook-security-plugin",
3
3
  "name": "MoBook 安全防护插件",
4
- "version": "2.0.19",
4
+ "version": "2.1.0",
5
5
  "description": "MoBook 企业级安全插件 - 危险命令分级拦截、敏感信息全渠道脱敏(身份证GB11643/银行卡Luhn校验)、LLM响应拦截、操作审计",
6
6
  "entry": "./index.js",
7
7
  "type": "plugin",
8
- "configSchema": {}
9
- }
8
+ "configSchema": {},
9
+ "activation": {
10
+ "onStartup": true
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/mobook-security-plugin",
3
- "version": "2.0.19",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "MoBook 企业级安全插件 - 危险命令分级拦截、敏感信息全渠道脱敏(身份证GB11643/银行卡Luhn校验)、LLM响应拦截、操作审计",
6
6
  "main": "index.js",
@@ -20,4 +20,4 @@
20
20
  "./index.js"
21
21
  ]
22
22
  }
23
- }
23
+ }