@kenhkl/agent-moss 0.10.5 → 0.11.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.
@@ -45,11 +45,13 @@ __export(config_exports, {
45
45
  isHeuristicEnabled: () => isHeuristicEnabled,
46
46
  isLLMAvailable: () => isLLMAvailable,
47
47
  isLogicRulesEnabled: () => isLogicRulesEnabled,
48
+ probeLLMConnection: () => probeLLMConnection,
48
49
  updateLLMConfig: () => updateLLMConfig
49
50
  });
50
51
  import fs from "fs";
51
52
  import path2 from "path";
52
53
  import os from "os";
54
+ import { fileURLToPath as fileURLToPath2 } from "url";
53
55
  function loadConfigFile(filePath) {
54
56
  try {
55
57
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -112,16 +114,38 @@ function getTimeoutConfig() {
112
114
  function getRetryConfig() {
113
115
  return retryConfig;
114
116
  }
117
+ function wildcardToRegExp(pattern) {
118
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
119
+ }
115
120
  function loadCustomRules() {
121
+ const rules = [];
116
122
  try {
117
123
  const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
118
- if (!raw) return [];
119
- const parsed = JSON.parse(raw);
120
- if (!Array.isArray(parsed)) return [];
121
- return parsed.filter((r) => r.pattern && r.action);
124
+ if (raw) {
125
+ const parsed = JSON.parse(raw);
126
+ if (Array.isArray(parsed)) {
127
+ for (const r of parsed) {
128
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
129
+ }
130
+ }
131
+ }
122
132
  } catch {
123
- return [];
124
133
  }
134
+ for (const rulesPath of USER_RULES_PATHS) {
135
+ try {
136
+ if (!fs.existsSync(rulesPath)) continue;
137
+ const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
138
+ const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
139
+ for (const r of actions) {
140
+ const p = r?.pattern;
141
+ if (!p) continue;
142
+ rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
143
+ }
144
+ break;
145
+ } catch {
146
+ }
147
+ }
148
+ return rules;
125
149
  }
126
150
  function getLLMConfig() {
127
151
  return llmConfig;
@@ -136,6 +160,30 @@ function isLLMAvailable() {
136
160
  if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
137
161
  return llmConfig.apiKey.length > 0;
138
162
  }
163
+ async function probeLLMConnection(timeoutMs = 8e3) {
164
+ if (!isLLMAvailable()) return false;
165
+ const OpenAI2 = (await import("openai")).default;
166
+ const config = getLLMConfig();
167
+ const provider = detectProvider(config.baseUrl, config.provider);
168
+ const extraHeaders = getProviderHeaders(provider);
169
+ const client = new OpenAI2({
170
+ baseURL: config.baseUrl,
171
+ apiKey: config.apiKey,
172
+ timeout: timeoutMs,
173
+ maxRetries: 0,
174
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
175
+ });
176
+ try {
177
+ const resp = await client.chat.completions.create({
178
+ model: config.model,
179
+ messages: [{ role: "user", content: "ping" }],
180
+ max_tokens: 1
181
+ });
182
+ return !!(resp && resp.choices && resp.choices[0]);
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
139
187
  function getLLMFailMode() {
140
188
  const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
141
189
  return val === "fail_closed" ? "fail_closed" : "fail_open";
@@ -179,7 +227,7 @@ function isHeuristicEnabled() {
179
227
  function isLogicRulesEnabled() {
180
228
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
181
229
  }
182
- var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
230
+ var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
183
231
  var init_config = __esm({
184
232
  "src/config.ts"() {
185
233
  "use strict";
@@ -201,6 +249,21 @@ var init_config = __esm({
201
249
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
202
250
  };
203
251
  })();
252
+ __configModuleDir = (() => {
253
+ try {
254
+ return path2.dirname(fileURLToPath2(import.meta.url));
255
+ } catch {
256
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
257
+ }
258
+ })();
259
+ USER_RULES_PATHS = [
260
+ path2.join(__configModuleDir, "rules", "user_rules.json"),
261
+ // 源在 ts/src/rules(一般不存在)
262
+ path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
263
+ // dist: dist/engine/../rules = dist/rules ✅
264
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
265
+ // dev src: ts/src/../../agent_moss
266
+ ];
204
267
  customRules = loadCustomRules();
205
268
  PROVIDER_HEADERS = {
206
269
  "openrouter": {
@@ -217,7 +280,17 @@ var init_config = __esm({
217
280
  // src/engine/inline-analyzer.ts
218
281
  function isLiteralTextCommand(command) {
219
282
  ECHO_CMD_RE.lastIndex = 0;
220
- return ECHO_CMD_RE.test(command);
283
+ if (!ECHO_CMD_RE.test(command)) return false;
284
+ if (command.includes(">")) {
285
+ let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
286
+ d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
287
+ d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
288
+ d = d.replace(/'[^']*'|"[^"]*"/g, "");
289
+ if (/>{1,2}\s*(?!&)\S/.test(d)) {
290
+ return false;
291
+ }
292
+ }
293
+ return true;
221
294
  }
222
295
  function extractInlineCode(command) {
223
296
  const parts = [];
@@ -294,7 +367,7 @@ function analyzeInlineScript(command) {
294
367
  riskType: "file_access"
295
368
  };
296
369
  }
297
- var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
370
+ var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SHELL_RC_FRAGMENTS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
298
371
  var init_inline_analyzer = __esm({
299
372
  "src/engine/inline-analyzer.ts"() {
300
373
  "use strict";
@@ -364,6 +437,13 @@ var init_inline_analyzer = __esm({
364
437
  // xargs bash / xargs sh
365
438
  /\|\s*xargs\s+(?:bash|sh|zsh)/gi
366
439
  ];
440
+ SHELL_RC_FRAGMENTS = [
441
+ ".bashrc",
442
+ ".zshrc",
443
+ ".profile",
444
+ ".bash_profile",
445
+ ".zprofile"
446
+ ];
367
447
  SENSITIVE_PATH_FRAGMENTS = [
368
448
  "/etc/shadow",
369
449
  "/etc/gshadow",
@@ -380,9 +460,10 @@ var init_inline_analyzer = __esm({
380
460
  "/dev/kmsg",
381
461
  ".ssh/id_rsa",
382
462
  ".ssh/id_ed25519",
383
- ".ssh/authorized_keys"
463
+ ".ssh/authorized_keys",
464
+ ...SHELL_RC_FRAGMENTS
384
465
  ];
385
- REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
466
+ REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
386
467
  DIR_TRAVERSAL_KEYWORDS = [
387
468
  // Python 目录遍历
388
469
  "os.listdir",
@@ -405,11 +486,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
405
486
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
406
487
  if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
407
488
  if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
489
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
490
+ const trimmed = seg.trim();
491
+ if (!trimmed) continue;
492
+ const parts = trimmed.split(/\s+/);
493
+ const fw = parts[0]?.toLowerCase() || "";
494
+ if (fw === "wget" || fw === "curl") {
495
+ const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
496
+ if (isStdout) continue;
497
+ const isDevnull = /\s(?:-O|--output-document|--output)\s*\/dev\/null(?:\s|$)/i.test(trimmed) || /\s(?:-O|--output-document|--output)=[/]?dev\/null(?:\s|$)/i.test(trimmed);
498
+ if (isDevnull) continue;
499
+ if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
500
+ return true;
501
+ }
502
+ }
503
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
504
+ return true;
505
+ }
506
+ }
408
507
  if (actionDetail.includes(">")) {
409
- const detailWithoutDevnull = actionDetail.replace(DEVNULL_REDIRECT_RE, "");
410
- if (detailWithoutDevnull.includes(">")) {
411
- const firstWord = actionDetail.trim().split(/\s+/)[0]?.toLowerCase() || "";
412
- if (!READ_ONLY_COMMANDS.has(firstWord)) return true;
508
+ const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
509
+ const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
510
+ const detailUnquotedRedirect = detailWithoutDevnull.replace(
511
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
512
+ (_m, _q, p) => ">" + p
513
+ );
514
+ const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
515
+ if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
516
+ return true;
413
517
  }
414
518
  }
415
519
  return false;
@@ -433,27 +537,31 @@ function runLogicCheck(input) {
433
537
  if (rbwResult) return rbwResult;
434
538
  if (input.promptSession && input.promptSession.trim()) {
435
539
  if (!isExcludedType) {
436
- const promptLower = input.promptSession.toLowerCase();
437
- for (const pattern of INTENT_DEVIATION_PATTERNS) {
438
- const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
439
- const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
440
- if (intentMatched && dangerousMatched) {
441
- return {
442
- allowed: false,
443
- reason: pattern.reason,
444
- riskLevel: "high",
445
- riskType: "intent_deviation",
446
- confidence: 85,
447
- source: "logic_rule",
448
- violatedLayers: ["2"]
449
- };
540
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
541
+ } else {
542
+ const promptLower = input.promptSession.toLowerCase();
543
+ for (const pattern of INTENT_DEVIATION_PATTERNS) {
544
+ const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
545
+ const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
546
+ if (intentMatched && dangerousMatched) {
547
+ return {
548
+ allowed: false,
549
+ reason: pattern.reason,
550
+ riskLevel: "high",
551
+ riskType: "intent_deviation",
552
+ confidence: 85,
553
+ source: "logic_rule",
554
+ violatedLayers: ["2"]
555
+ };
556
+ }
450
557
  }
451
558
  }
452
559
  }
453
560
  }
454
561
  const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
455
562
  if (indirectResult) return indirectResult;
456
- if (actionDetail.includes("sudo") || actionDetail.includes("su -") || actionDetail.includes("chmod u+s") || actionDetail.includes("setuid")) {
563
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
564
+ } else if (containsPrivilegeEscalation(actionDetail)) {
457
565
  const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
458
566
  const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
459
567
  if (!isAdminCmd) {
@@ -483,6 +591,9 @@ function extractFilePaths(text) {
483
591
  const paths = [];
484
592
  const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
485
593
  if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
594
+ const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
595
+ const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
596
+ if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
486
597
  const quotedPaths = text.match(/["']\/([^"']+)["']/g);
487
598
  if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
488
599
  return [...new Set(paths)];
@@ -493,8 +604,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
493
604
  const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
494
605
  const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
495
606
  if (!isWrite) return null;
496
- const targetPaths = extractFilePaths(actionDetail);
497
- if (targetPaths.length === 0) return null;
607
+ const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
608
+ if (isBashLike) {
609
+ const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
610
+ const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
611
+ const detailUnquotedRedirect = detailNoDevnull.replace(
612
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
613
+ (_m, _q, p) => ">" + p
614
+ );
615
+ const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
616
+ const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
617
+ let hasWriteSeg = hasRedirectWrite;
618
+ if (!hasWriteSeg) {
619
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
620
+ const trimmed = seg.trim();
621
+ if (!trimmed) continue;
622
+ const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
623
+ if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
624
+ hasWriteSeg = true;
625
+ break;
626
+ }
627
+ if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
628
+ hasWriteSeg = true;
629
+ break;
630
+ }
631
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
632
+ hasWriteSeg = true;
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ if (!hasWriteSeg) return null;
638
+ }
639
+ const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
640
+ const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
641
+ const targetPaths = [];
642
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
643
+ const trimmed = seg.trim();
644
+ if (!trimmed) continue;
645
+ const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
646
+ if (firstWord === "cd") {
647
+ continue;
648
+ }
649
+ if (READ_ONLY_COMMANDS.has(firstWord)) {
650
+ const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
651
+ const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
652
+ const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
653
+ const m = segNoQuotes.match(REDIRECT_TARGET_RE);
654
+ if (m) targetPaths.push(m[1] || m[2] || m[3]);
655
+ continue;
656
+ }
657
+ const segPaths = extractFilePaths(trimmed);
658
+ const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
659
+ if (SOURCE_ARG_CMDS.has(firstWord)) {
660
+ if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
661
+ } else if (firstWord === "dd") {
662
+ const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
663
+ const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
664
+ const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
665
+ const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
666
+ for (const sp of segPaths) {
667
+ if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
668
+ }
669
+ } else {
670
+ targetPaths.push(...segPathsFiltered);
671
+ }
672
+ }
673
+ const uniqueTargetPaths = [...new Set(targetPaths)];
674
+ if (uniqueTargetPaths.length === 0) return null;
498
675
  const historyReadPaths = /* @__PURE__ */ new Set();
499
676
  for (const action of actionHistory) {
500
677
  const histDetail = action.actionDetail.toLowerCase();
@@ -507,7 +684,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
507
684
  }
508
685
  }
509
686
  const unreadPaths = [];
510
- for (const path3 of targetPaths) {
687
+ for (const path3 of uniqueTargetPaths) {
511
688
  const hasRead = [...historyReadPaths].some(
512
689
  (rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
513
690
  );
@@ -570,6 +747,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
570
747
  violatedLayers: ["2"]
571
748
  };
572
749
  }
750
+ function containsPrivilegeEscalation(actionDetail) {
751
+ const ad = actionDetail.toLowerCase();
752
+ for (const seg of ad.split(/&&|\|\||;|\n/)) {
753
+ const trimmed = seg.trim();
754
+ if (!trimmed) continue;
755
+ const parts = trimmed.split(/\s+/);
756
+ const fw = parts[0]?.toLowerCase() || "";
757
+ if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
758
+ if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
759
+ }
760
+ return false;
761
+ }
573
762
  function checkIndirectFileAccess(actionType, actionDetail, cwd) {
574
763
  if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
575
764
  return null;
@@ -674,12 +863,13 @@ var init_logic_rules = __esm({
674
863
  { path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
675
864
  // 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
676
865
  { path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
677
- { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
866
+ { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
678
867
  { path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
679
868
  // SSH 主机私钥
680
- { path: "/etc/ssh/ssh_host_rsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
681
- { path: "/etc/ssh/ssh_host_ed25519_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
682
- { path: "/etc/ssh/ssh_host_ecdsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
869
+ // SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
870
+ { path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
871
+ { path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
872
+ { path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
683
873
  ];
684
874
  HOME_SENSITIVE_PATHS = [
685
875
  { path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
@@ -720,7 +910,7 @@ var init_logic_rules = __esm({
720
910
  "cat"
721
911
  ]);
722
912
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
723
- WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362"];
913
+ WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362", "edit"];
724
914
  WRITE_COMMAND_RES = [
725
915
  /\brm\b/,
726
916
  /\bunlink\b/,
@@ -766,6 +956,12 @@ var init_logic_rules = __esm({
766
956
  "nl",
767
957
  "tac",
768
958
  "rev",
959
+ // NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
960
+ // read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
961
+ // 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
962
+ // echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
963
+ "echo",
964
+ "printf",
769
965
  "lsblk",
770
966
  "blockdev",
771
967
  "smartctl",
@@ -818,7 +1014,7 @@ var init_logic_rules = __esm({
818
1014
  "modinfo",
819
1015
  "sysctl"
820
1016
  ]);
821
- DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/;
1017
+ DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
822
1018
  READ_KEYWORDS = ["read", "\u8BFB\u53D6", "cat", "head", "tail", "less", "more", "view", "\u67E5\u770B", "open", "\u6253\u5F00", "load", "\u52A0\u8F7D", "grep", "search", "\u641C\u7D22", "find", "\u67E5\u627E"];
823
1019
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
824
1020
  PASSWORD_MODIFY_PATTERNS = [
@@ -868,7 +1064,7 @@ var init_logic_rules = __esm({
868
1064
  // src/engine/skill-loader.ts
869
1065
  import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
870
1066
  import { resolve, dirname } from "path";
871
- import { fileURLToPath as fileURLToPath2 } from "url";
1067
+ import { fileURLToPath as fileURLToPath3 } from "url";
872
1068
  function loadAllSkills() {
873
1069
  if (cachedSkills) return cachedSkills;
874
1070
  cachedSkills = {};
@@ -939,7 +1135,7 @@ var init_skill_loader = __esm({
939
1135
  "src/engine/skill-loader.ts"() {
940
1136
  "use strict";
941
1137
  init_esm_shims();
942
- __filename2 = fileURLToPath2(import.meta.url);
1138
+ __filename2 = fileURLToPath3(import.meta.url);
943
1139
  __dirname2 = dirname(__filename2);
944
1140
  SKILLS_DIR = resolve(__dirname2, "../skills");
945
1141
  SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
@@ -1014,7 +1210,7 @@ __export(runtime_config_exports, {
1014
1210
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1015
1211
  import { join } from "path";
1016
1212
  import { homedir } from "os";
1017
- import { fileURLToPath as fileURLToPath3 } from "url";
1213
+ import { fileURLToPath as fileURLToPath4 } from "url";
1018
1214
  import { dirname as dirname2 } from "path";
1019
1215
  function getRuntimeConfigPath() {
1020
1216
  const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
@@ -1870,7 +2066,7 @@ var init_runtime_config = __esm({
1870
2066
  init_heuristic();
1871
2067
  init_logic_rules();
1872
2068
  init_skill_loader();
1873
- __dirname3 = dirname2(fileURLToPath3(import.meta.url));
2069
+ __dirname3 = dirname2(fileURLToPath4(import.meta.url));
1874
2070
  RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
1875
2071
  RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
1876
2072
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
@@ -1893,7 +2089,7 @@ var init_runtime_config = __esm({
1893
2089
  // src/agents/index.ts
1894
2090
  import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
1895
2091
  import { resolve as resolve2, dirname as dirname3 } from "path";
1896
- import { fileURLToPath as fileURLToPath4 } from "url";
2092
+ import { fileURLToPath as fileURLToPath5 } from "url";
1897
2093
  function resolveAgentsDir() {
1898
2094
  if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
1899
2095
  if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
@@ -1942,7 +2138,7 @@ var init_agents = __esm({
1942
2138
  "src/agents/index.ts"() {
1943
2139
  "use strict";
1944
2140
  init_esm_shims();
1945
- __filename3 = fileURLToPath4(import.meta.url);
2141
+ __filename3 = fileURLToPath5(import.meta.url);
1946
2142
  __dirname4 = dirname3(__filename3);
1947
2143
  AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
1948
2144
  AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
@@ -1962,19 +2158,25 @@ function runHeuristicCheck(input) {
1962
2158
  const re = new RegExp(rule.pattern);
1963
2159
  if (re.test(command)) {
1964
2160
  if (rule.action === "Deny") {
1965
- matched.push(`\u81EA\u5B9A\u4E49\u89C4\u5219: ${rule.pattern}`);
1966
- if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1967
- highestSeverity = rule.severity;
1968
- highestRiskType = "custom_rule";
1969
- }
2161
+ return {
2162
+ allowed: false,
2163
+ reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
2164
+ riskLevel: rule.severity,
2165
+ riskType: "sensitive_action",
2166
+ confidence: 90,
2167
+ source: "heuristic",
2168
+ violatedLayers: ["1.1"]
2169
+ };
1970
2170
  }
1971
2171
  }
1972
2172
  } catch {
1973
2173
  }
1974
2174
  }
2175
+ const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
2176
+ const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
1975
2177
  let topRiskType = "";
1976
2178
  for (const rule of DANGEROUS_PATTERNS) {
1977
- if (rule.pattern.test(command)) {
2179
+ if (rule.pattern.test(scanCommand)) {
1978
2180
  matched.push(rule.description);
1979
2181
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1980
2182
  highestSeverity = rule.severity;
@@ -1988,7 +2190,7 @@ function runHeuristicCheck(input) {
1988
2190
  for (const rule of agentExt.extra_command_patterns) {
1989
2191
  try {
1990
2192
  const re = new RegExp(rule.pattern, "i");
1991
- if (re.test(command)) {
2193
+ if (re.test(scanCommand)) {
1992
2194
  matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
1993
2195
  if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
1994
2196
  highestSeverity = rule.risk_level;
@@ -2012,14 +2214,12 @@ function runHeuristicCheck(input) {
2012
2214
  const actionTypeL = input.aNext.actionType.toLowerCase();
2013
2215
  const detailL = input.aNext.actionDetail.toLowerCase();
2014
2216
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2217
+ const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2218
+ if (dangerousResult) return dangerousResult;
2015
2219
  if (!isExcludedType) {
2016
2220
  const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2017
2221
  if (pathResult) return pathResult;
2018
2222
  }
2019
- const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2020
- if (dangerousResult) return dangerousResult;
2021
- const lateralResult = checkLateralMovement(detailL);
2022
- if (lateralResult) return lateralResult;
2023
2223
  if (matched.length > 0) {
2024
2224
  return {
2025
2225
  allowed: false,
@@ -2032,6 +2232,8 @@ function runHeuristicCheck(input) {
2032
2232
  // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2033
2233
  };
2034
2234
  }
2235
+ const lateralResult = checkLateralMovement(detailL);
2236
+ if (lateralResult) return lateralResult;
2035
2237
  return null;
2036
2238
  }
2037
2239
  function checkInjectionKeywords(text, extraKeywords) {
@@ -2054,6 +2256,68 @@ function isInlineScriptCommand(actionDetail) {
2054
2256
  const re = /(?:^|\s)(?:python|python3|python3\.\d+|perl|perl5|ruby|jruby|mruby|node|php|lua)\s+(?:-[a-zA-Z]*[cer][a-zA-Z]*\s+)/i;
2055
2257
  return re.test(actionDetail);
2056
2258
  }
2259
+ function stripLiteralTextSegments(actionDetail) {
2260
+ const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
2261
+ const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
2262
+ if (writeScriptExec || pipeToInterpreter) {
2263
+ return actionDetail;
2264
+ }
2265
+ const resultParts = [];
2266
+ for (const seg of splitCommandSegments(actionDetail)) {
2267
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
2268
+ if (fw === "echo" || fw === "printf") {
2269
+ const segUnquotedRedirect = seg.replace(
2270
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
2271
+ (_m, _q, p) => ">" + p
2272
+ );
2273
+ resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
2274
+ } else {
2275
+ resultParts.push(seg);
2276
+ }
2277
+ }
2278
+ return resultParts.join("");
2279
+ }
2280
+ function splitCommandSegments(actionDetail) {
2281
+ const segments = [];
2282
+ let cur = "";
2283
+ let quote = null;
2284
+ for (let i = 0; i < actionDetail.length; i++) {
2285
+ const ch = actionDetail[i];
2286
+ if (quote) {
2287
+ cur += ch;
2288
+ if (ch === quote) quote = null;
2289
+ continue;
2290
+ }
2291
+ if (ch === "'" || ch === '"') {
2292
+ quote = ch;
2293
+ cur += ch;
2294
+ continue;
2295
+ }
2296
+ if (ch === ";" || ch === "\n") {
2297
+ segments.push(cur);
2298
+ segments.push(ch);
2299
+ cur = "";
2300
+ continue;
2301
+ }
2302
+ if (ch === "&" && actionDetail[i + 1] === "&") {
2303
+ segments.push(cur);
2304
+ segments.push("&&");
2305
+ cur = "";
2306
+ i++;
2307
+ continue;
2308
+ }
2309
+ if (ch === "|" && actionDetail[i + 1] === "|") {
2310
+ segments.push(cur);
2311
+ segments.push("||");
2312
+ cur = "";
2313
+ i++;
2314
+ continue;
2315
+ }
2316
+ cur += ch;
2317
+ }
2318
+ if (cur) segments.push(cur);
2319
+ return segments;
2320
+ }
2057
2321
  function severityRank(s) {
2058
2322
  const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
2059
2323
  return ranks[s] || 0;
@@ -2061,13 +2325,18 @@ function severityRank(s) {
2061
2325
  function checkSensitivePath(command, cwd, agentExt) {
2062
2326
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2063
2327
  const isWriteOpFlag = isWriteOperation("", command);
2328
+ const pathScanText = stripLiteralTextSegments(command);
2064
2329
  const pathMatches = (text, path3) => {
2065
- const escaped = path3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2330
+ const parts = path3.split(/(\(\?!.*?\))/);
2331
+ const escaped = parts.map((part) => {
2332
+ if (part.startsWith("(?!")) return part;
2333
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2334
+ }).join("");
2066
2335
  const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
2067
2336
  return re.test(text);
2068
2337
  };
2069
2338
  for (const sp of ROOT_SENSITIVE_PATHS) {
2070
- if (pathMatches(command, sp.path.toLowerCase())) {
2339
+ if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2071
2340
  if (sp.path === "/dev/random") {
2072
2341
  if (isWriteOpFlag) {
2073
2342
  continue;
@@ -2101,7 +2370,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2101
2370
  const expanded = `${expandedCwd}/${sp.path}`;
2102
2371
  const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2103
2372
  const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2104
- if (pathRe.test(command) || expandedRe.test(command)) {
2373
+ if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2105
2374
  if (!isWriteOpFlag && !sp.credential) {
2106
2375
  continue;
2107
2376
  }
@@ -2172,8 +2441,10 @@ function checkSensitivePath(command, cwd, agentExt) {
2172
2441
  }
2173
2442
  function checkDangerousPatterns(actionType, actionDetail) {
2174
2443
  if (actionType === "file_write" || actionType === "file_edit") return null;
2175
- const TEMP_DIRS = ["/tmp/", "/var/tmp/", "/run/", "/dev/shm/"];
2176
- const isTempPath = TEMP_DIRS.some((td) => actionDetail.includes(td));
2444
+ const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2445
+ const isTempPath = TEMP_DIRS.some(
2446
+ (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
2447
+ );
2177
2448
  if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
2178
2449
  return {
2179
2450
  allowed: false,
@@ -2185,7 +2456,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
2185
2456
  violatedLayers: ["1.1"]
2186
2457
  };
2187
2458
  }
2188
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail) || />\s*\/proc\//.test(actionDetail)) {
2459
+ const findWriteMatch = actionDetail.match(
2460
+ /\bfind\b.*?(?:(?:^|\s)-exec(?:dir)?\s+(chmod|chown|cp|mv|rm|dd|tee|truncate|mkfs|shred|rmdir|unlink)\b|(?:^|\s)\|\s*xargs\s+(?:sed\s+-[a-zA-Z]*i|chmod|chown|cp|mv|rm|dd|tee|truncate|mkfs|shred|rmdir|unlink)\b)/i
2461
+ );
2462
+ if (findWriteMatch) {
2463
+ const cmdHit = findWriteMatch[1] || "xargs";
2464
+ return {
2465
+ allowed: false,
2466
+ reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
2467
+ riskLevel: "high",
2468
+ riskType: "script_execution",
2469
+ confidence: 90,
2470
+ source: "heuristic",
2471
+ violatedLayers: ["1.1"]
2472
+ };
2473
+ }
2474
+ if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
2189
2475
  return {
2190
2476
  allowed: false,
2191
2477
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -2196,17 +2482,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
2196
2482
  violatedLayers: ["1.1"]
2197
2483
  };
2198
2484
  }
2485
+ if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
2486
+ return {
2487
+ allowed: false,
2488
+ reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
2489
+ riskLevel: "high",
2490
+ riskType: "file_access",
2491
+ confidence: 90,
2492
+ source: "heuristic",
2493
+ violatedLayers: ["1.1"]
2494
+ };
2495
+ }
2199
2496
  return null;
2200
2497
  }
2201
2498
  function extractRemoteHost(command) {
2202
- const m = command.match(/(?:ssh|scp|rsync)\s+(?:-[\w]+\s+)*(\S+@\S+|\b(?!-)[\w.-]+\b)/);
2203
- return m ? m[1] : null;
2499
+ const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
2500
+ for (const tok of tokens) {
2501
+ const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
2502
+ if (am && am[2] && !isLocalHost(am[2])) {
2503
+ return `${am[1]}@${am[2]}`;
2504
+ }
2505
+ }
2506
+ for (const tok of tokens) {
2507
+ const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
2508
+ if (hm && hm[1] && !isLocalHost(hm[1])) {
2509
+ return hm[1];
2510
+ }
2511
+ }
2512
+ const skipValueOpts = /* @__PURE__ */ new Set([
2513
+ "-p",
2514
+ "-l",
2515
+ "-i",
2516
+ "-o",
2517
+ "-b",
2518
+ "-c",
2519
+ "-m",
2520
+ "-F",
2521
+ "-e",
2522
+ "-S",
2523
+ "-W",
2524
+ "-J",
2525
+ "-I",
2526
+ "-T",
2527
+ "-B",
2528
+ "-L",
2529
+ "-R",
2530
+ "-D",
2531
+ "-E",
2532
+ "-M",
2533
+ "-O",
2534
+ "-P",
2535
+ "-q"
2536
+ ]);
2537
+ let foundCmd = false;
2538
+ for (let i = 0; i < tokens.length; i++) {
2539
+ const t = tokens[i];
2540
+ if (t === "ssh" || t === "scp" || t === "rsync") {
2541
+ foundCmd = true;
2542
+ continue;
2543
+ }
2544
+ if (!foundCmd) continue;
2545
+ if (t.startsWith("-")) {
2546
+ if (skipValueOpts.has(t) && i + 1 < tokens.length) {
2547
+ i++;
2548
+ }
2549
+ continue;
2550
+ }
2551
+ if (t.startsWith("/")) continue;
2552
+ return t;
2553
+ }
2554
+ return null;
2204
2555
  }
2205
2556
  function isLocalHost(host) {
2206
2557
  const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
2207
2558
  return localNames.includes(host.replace(/.*@/, ""));
2208
2559
  }
2209
2560
  function checkLateralMovement(actionDetail) {
2561
+ if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
2562
+ const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
2563
+ let hasRealCall = false;
2564
+ for (const seg of segments) {
2565
+ const parts = seg.split(/\s+/);
2566
+ const fw = (parts[0] || "").toLowerCase();
2567
+ if (fw === "echo" || fw === "printf") continue;
2568
+ if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
2569
+ hasRealCall = true;
2570
+ break;
2571
+ }
2572
+ }
2573
+ if (!hasRealCall) return null;
2210
2574
  if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
2211
2575
  const targetHost = extractRemoteHost(actionDetail);
2212
2576
  if (targetHost && !isLocalHost(targetHost)) {
@@ -2306,10 +2670,14 @@ var init_heuristic = __esm({
2306
2670
  { pattern: /mkfs\.\w+\s+\/dev\/(sd|hd|nvme|vd|loop|mapper|md|dm)\S+/i, severity: "critical", description: "\u683C\u5F0F\u5316\u5757\u8BBE\u5907 (mkfs /dev/\u5757\u8BBE\u5907)", riskType: "script_execution" },
2307
2671
  { pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
2308
2672
  // --- Critical: remote code execution ---
2309
- { pattern: /curl\s+.*\|\s*(bash|sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2673
+ // curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
2674
+ // 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
2675
+ // runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
2676
+ { pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
2677
+ { pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2310
2678
  { pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
2311
2679
  { pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
2312
- { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "critical", description: "base64 \u89E3\u7801\u6267\u884C", riskType: "script_execution" },
2680
+ { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
2313
2681
  // --- Critical: reverse shell ---
2314
2682
  { pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
2315
2683
  { pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
@@ -2320,13 +2688,25 @@ var init_heuristic = __esm({
2320
2688
  { pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
2321
2689
  // --- Critical: privilege escalation / container escape ---
2322
2690
  { pattern: /\/var\/run\/docker\.sock/i, severity: "critical", description: "\u8BBF\u95EE Docker socket\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5BB9\u5668\u9003\u9038", riskType: "privilege_escalation" },
2323
- // --- High: dangerous permissions / ownership ---
2324
- { pattern: /chmod\s+[0-7]*777[0-7]*/i, severity: "high", description: "\u5168\u6743\u9650\u8BBE\u7F6E (chmod 777)", riskType: "script_execution" },
2691
+ // --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
2692
+ // 修正「只拦 777、xx7 漏放」的缺口:
2693
+ // 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
2694
+ // 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
2695
+ // 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
2696
+ // 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
2697
+ // 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
2698
+ { pattern: /chmod\s+[0-7]{0,3}7\b/i, severity: "critical", description: "\u5371\u9669\u6743\u9650\u8BBE\u7F6E (chmod ...7, other \u4F4D\u542B\u5199+\u6267\u884C\u655E\u53E3)", riskType: "privilege_escalation" },
2699
+ { pattern: /chmod\s+[24][0-7]{3}\b/i, severity: "critical", description: "setuid/setgid \u4F4D\u8BBE\u7F6E (chmod 4xxx/2xxx, \u63D0\u6743)", riskType: "privilege_escalation" },
2700
+ { pattern: /chmod\s+[ugoa]\+s\b/i, severity: "critical", description: "\u7B26\u53F7 setuid/setgid \u8BBE\u7F6E (chmod u+s/g+s/a+s, \u63D0\u6743)", riskType: "privilege_escalation" },
2325
2701
  { pattern: /chown\s+root|chown\s+-R\s+\w+:\w+\s+\//i, severity: "high", description: "\u4FEE\u6539\u6240\u6709\u8005\u4E3A root / \u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6240\u6709\u8005", riskType: "script_execution" },
2326
2702
  // sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
2327
2703
  { pattern: /sudo\s+(rm|chmod|chown|dd|mkfs|format|iptables|crontab|passwd|userdel|useradd|groupdel)\b/i, severity: "high", description: "\u63D0\u6743\u6267\u884C\u9AD8\u5371\u547D\u4EE4 (sudo + \u5371\u9669\u64CD\u4F5C)", riskType: "privilege_escalation" },
2328
2704
  // --- High: system config overwrite ---
2705
+ // > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
2706
+ // 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
2329
2707
  { pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
2708
+ { pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
2709
+ // 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
2330
2710
  // --- High: download script + execute risk ---
2331
2711
  { pattern: /(?:curl|wget)\s+.*(?:-o|--output|>)\s*\S+\.(?:sh|bash)\b/i, severity: "high", description: "\u8FDC\u7A0B\u4E0B\u8F7D\u811A\u672C\u5230\u672C\u5730", riskType: "script_execution" },
2332
2712
  { pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
@@ -2342,7 +2722,11 @@ var init_heuristic = __esm({
2342
2722
  // --- Medium: credential export ---
2343
2723
  { pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
2344
2724
  // --- High: shell rc file append (persistence) ---
2725
+ // 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
2726
+ // 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
2727
+ // /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
2345
2728
  { pattern: />>\s*(~|\/home\/[^/]+)\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08~/.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
2729
+ { pattern: />>\s*\S*\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
2346
2730
  // --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
2347
2731
  { pattern: />\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i, severity: "high", description: "\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6", riskType: "file_access" },
2348
2732
  // --- High: non-interactive password modification ---
@@ -2555,8 +2939,8 @@ init_skill_loader();
2555
2939
  init_esm_shims();
2556
2940
  import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2557
2941
  import { resolve as resolve3, dirname as dirname4 } from "path";
2558
- import { fileURLToPath as fileURLToPath5 } from "url";
2559
- var __filename4 = fileURLToPath5(import.meta.url);
2942
+ import { fileURLToPath as fileURLToPath6 } from "url";
2943
+ var __filename4 = fileURLToPath6(import.meta.url);
2560
2944
  var __dirname5 = dirname4(__filename4);
2561
2945
  var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
2562
2946
  var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
@@ -3471,11 +3855,13 @@ async function analyzeWithLLM(prompt, config) {
3471
3855
  } catch {
3472
3856
  }
3473
3857
  clearTimeout(totalTimeoutTimer);
3858
+ const riskLevel = parsed.riskLevel ?? parsed.risk_level;
3859
+ const riskType = parsed.riskType ?? parsed.risk_type;
3474
3860
  return {
3475
3861
  allowed: parsed.allowed !== false,
3476
3862
  reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
3477
- riskLevel: ["low", "medium", "high", "critical"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",
3478
- riskType: parsed.riskType || "",
3863
+ riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
3864
+ riskType: riskType || "",
3479
3865
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
3480
3866
  source: "llm",
3481
3867
  violatedLayers: ["3"]
@@ -3593,7 +3979,10 @@ async function runAnalysis(input) {
3593
3979
  if (heuristicResult && !heuristicResult.allowed) {
3594
3980
  const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
3595
3981
  if (!skipInlineFileAccess) {
3596
- return heuristicResult;
3982
+ const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
3983
+ if (shouldDenyL1) {
3984
+ return heuristicResult;
3985
+ }
3597
3986
  }
3598
3987
  }
3599
3988
  let scriptChain = null;
@@ -3671,7 +4060,8 @@ async function runAnalysis(input) {
3671
4060
  ) && DANGEROUS_PATTERNS.some(
3672
4061
  (p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
3673
4062
  );
3674
- if (!fullCmdHit) {
4063
+ const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
4064
+ if (!fullCmdHit && !hasRedirectWrite) {
3675
4065
  return {
3676
4066
  allowed: true,
3677
4067
  reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,