@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.
package/dist/server.js CHANGED
@@ -55,11 +55,13 @@ __export(config_exports, {
55
55
  isHeuristicEnabled: () => isHeuristicEnabled,
56
56
  isLLMAvailable: () => isLLMAvailable,
57
57
  isLogicRulesEnabled: () => isLogicRulesEnabled,
58
+ probeLLMConnection: () => probeLLMConnection,
58
59
  updateLLMConfig: () => updateLLMConfig
59
60
  });
60
61
  import fs from "fs";
61
62
  import path2 from "path";
62
63
  import os from "os";
64
+ import { fileURLToPath as fileURLToPath2 } from "url";
63
65
  function loadConfigFile(filePath) {
64
66
  try {
65
67
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -122,16 +124,38 @@ function getTimeoutConfig() {
122
124
  function getRetryConfig() {
123
125
  return retryConfig;
124
126
  }
127
+ function wildcardToRegExp(pattern) {
128
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
129
+ }
125
130
  function loadCustomRules() {
131
+ const rules = [];
126
132
  try {
127
133
  const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
128
- if (!raw) return [];
129
- const parsed = JSON.parse(raw);
130
- if (!Array.isArray(parsed)) return [];
131
- return parsed.filter((r) => r.pattern && r.action);
134
+ if (raw) {
135
+ const parsed = JSON.parse(raw);
136
+ if (Array.isArray(parsed)) {
137
+ for (const r of parsed) {
138
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
139
+ }
140
+ }
141
+ }
132
142
  } catch {
133
- return [];
134
143
  }
144
+ for (const rulesPath of USER_RULES_PATHS) {
145
+ try {
146
+ if (!fs.existsSync(rulesPath)) continue;
147
+ const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
148
+ const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
149
+ for (const r of actions) {
150
+ const p = r?.pattern;
151
+ if (!p) continue;
152
+ rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
153
+ }
154
+ break;
155
+ } catch {
156
+ }
157
+ }
158
+ return rules;
135
159
  }
136
160
  function getLLMConfig() {
137
161
  return llmConfig;
@@ -146,6 +170,30 @@ function isLLMAvailable() {
146
170
  if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
147
171
  return llmConfig.apiKey.length > 0;
148
172
  }
173
+ async function probeLLMConnection(timeoutMs = 8e3) {
174
+ if (!isLLMAvailable()) return false;
175
+ const OpenAI2 = (await import("openai")).default;
176
+ const config = getLLMConfig();
177
+ const provider = detectProvider(config.baseUrl, config.provider);
178
+ const extraHeaders = getProviderHeaders(provider);
179
+ const client = new OpenAI2({
180
+ baseURL: config.baseUrl,
181
+ apiKey: config.apiKey,
182
+ timeout: timeoutMs,
183
+ maxRetries: 0,
184
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
185
+ });
186
+ try {
187
+ const resp = await client.chat.completions.create({
188
+ model: config.model,
189
+ messages: [{ role: "user", content: "ping" }],
190
+ max_tokens: 1
191
+ });
192
+ return !!(resp && resp.choices && resp.choices[0]);
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
149
197
  function getLLMFailMode() {
150
198
  const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
151
199
  return val === "fail_closed" ? "fail_closed" : "fail_open";
@@ -189,7 +237,7 @@ function isHeuristicEnabled() {
189
237
  function isLogicRulesEnabled() {
190
238
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
191
239
  }
192
- var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
240
+ var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
193
241
  var init_config = __esm({
194
242
  "src/config.ts"() {
195
243
  "use strict";
@@ -211,6 +259,21 @@ var init_config = __esm({
211
259
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
212
260
  };
213
261
  })();
262
+ __configModuleDir = (() => {
263
+ try {
264
+ return path2.dirname(fileURLToPath2(import.meta.url));
265
+ } catch {
266
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
267
+ }
268
+ })();
269
+ USER_RULES_PATHS = [
270
+ path2.join(__configModuleDir, "rules", "user_rules.json"),
271
+ // 源在 ts/src/rules(一般不存在)
272
+ path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
273
+ // dist: dist/engine/../rules = dist/rules ✅
274
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
275
+ // dev src: ts/src/../../agent_moss
276
+ ];
214
277
  customRules = loadCustomRules();
215
278
  PROVIDER_HEADERS = {
216
279
  "openrouter": {
@@ -227,7 +290,17 @@ var init_config = __esm({
227
290
  // src/engine/inline-analyzer.ts
228
291
  function isLiteralTextCommand(command) {
229
292
  ECHO_CMD_RE.lastIndex = 0;
230
- return ECHO_CMD_RE.test(command);
293
+ if (!ECHO_CMD_RE.test(command)) return false;
294
+ if (command.includes(">")) {
295
+ let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
296
+ d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
297
+ d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
298
+ d = d.replace(/'[^']*'|"[^"]*"/g, "");
299
+ if (/>{1,2}\s*(?!&)\S/.test(d)) {
300
+ return false;
301
+ }
302
+ }
303
+ return true;
231
304
  }
232
305
  function extractInlineCode(command) {
233
306
  const parts = [];
@@ -304,7 +377,7 @@ function analyzeInlineScript(command) {
304
377
  riskType: "file_access"
305
378
  };
306
379
  }
307
- 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;
380
+ 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;
308
381
  var init_inline_analyzer = __esm({
309
382
  "src/engine/inline-analyzer.ts"() {
310
383
  "use strict";
@@ -374,6 +447,13 @@ var init_inline_analyzer = __esm({
374
447
  // xargs bash / xargs sh
375
448
  /\|\s*xargs\s+(?:bash|sh|zsh)/gi
376
449
  ];
450
+ SHELL_RC_FRAGMENTS = [
451
+ ".bashrc",
452
+ ".zshrc",
453
+ ".profile",
454
+ ".bash_profile",
455
+ ".zprofile"
456
+ ];
377
457
  SENSITIVE_PATH_FRAGMENTS = [
378
458
  "/etc/shadow",
379
459
  "/etc/gshadow",
@@ -390,9 +470,10 @@ var init_inline_analyzer = __esm({
390
470
  "/dev/kmsg",
391
471
  ".ssh/id_rsa",
392
472
  ".ssh/id_ed25519",
393
- ".ssh/authorized_keys"
473
+ ".ssh/authorized_keys",
474
+ ...SHELL_RC_FRAGMENTS
394
475
  ];
395
- REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
476
+ REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
396
477
  DIR_TRAVERSAL_KEYWORDS = [
397
478
  // Python 目录遍历
398
479
  "os.listdir",
@@ -415,11 +496,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
415
496
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
416
497
  if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
417
498
  if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
499
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
500
+ const trimmed = seg.trim();
501
+ if (!trimmed) continue;
502
+ const parts = trimmed.split(/\s+/);
503
+ const fw = parts[0]?.toLowerCase() || "";
504
+ if (fw === "wget" || fw === "curl") {
505
+ const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
506
+ if (isStdout) continue;
507
+ 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);
508
+ if (isDevnull) continue;
509
+ if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
510
+ return true;
511
+ }
512
+ }
513
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
514
+ return true;
515
+ }
516
+ }
418
517
  if (actionDetail.includes(">")) {
419
- const detailWithoutDevnull = actionDetail.replace(DEVNULL_REDIRECT_RE, "");
420
- if (detailWithoutDevnull.includes(">")) {
421
- const firstWord = actionDetail.trim().split(/\s+/)[0]?.toLowerCase() || "";
422
- if (!READ_ONLY_COMMANDS.has(firstWord)) return true;
518
+ const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
519
+ const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
520
+ const detailUnquotedRedirect = detailWithoutDevnull.replace(
521
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
522
+ (_m, _q, p) => ">" + p
523
+ );
524
+ const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
525
+ if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
526
+ return true;
423
527
  }
424
528
  }
425
529
  return false;
@@ -443,27 +547,31 @@ function runLogicCheck(input) {
443
547
  if (rbwResult) return rbwResult;
444
548
  if (input.promptSession && input.promptSession.trim()) {
445
549
  if (!isExcludedType) {
446
- const promptLower = input.promptSession.toLowerCase();
447
- for (const pattern of INTENT_DEVIATION_PATTERNS) {
448
- const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
449
- const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
450
- if (intentMatched && dangerousMatched) {
451
- return {
452
- allowed: false,
453
- reason: pattern.reason,
454
- riskLevel: "high",
455
- riskType: "intent_deviation",
456
- confidence: 85,
457
- source: "logic_rule",
458
- violatedLayers: ["2"]
459
- };
550
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
551
+ } else {
552
+ const promptLower = input.promptSession.toLowerCase();
553
+ for (const pattern of INTENT_DEVIATION_PATTERNS) {
554
+ const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
555
+ const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
556
+ if (intentMatched && dangerousMatched) {
557
+ return {
558
+ allowed: false,
559
+ reason: pattern.reason,
560
+ riskLevel: "high",
561
+ riskType: "intent_deviation",
562
+ confidence: 85,
563
+ source: "logic_rule",
564
+ violatedLayers: ["2"]
565
+ };
566
+ }
460
567
  }
461
568
  }
462
569
  }
463
570
  }
464
571
  const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
465
572
  if (indirectResult) return indirectResult;
466
- if (actionDetail.includes("sudo") || actionDetail.includes("su -") || actionDetail.includes("chmod u+s") || actionDetail.includes("setuid")) {
573
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
574
+ } else if (containsPrivilegeEscalation(actionDetail)) {
467
575
  const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
468
576
  const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
469
577
  if (!isAdminCmd) {
@@ -493,6 +601,9 @@ function extractFilePaths(text) {
493
601
  const paths = [];
494
602
  const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
495
603
  if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
604
+ const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
605
+ const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
606
+ if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
496
607
  const quotedPaths = text.match(/["']\/([^"']+)["']/g);
497
608
  if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
498
609
  return [...new Set(paths)];
@@ -503,8 +614,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
503
614
  const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
504
615
  const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
505
616
  if (!isWrite) return null;
506
- const targetPaths = extractFilePaths(actionDetail);
507
- if (targetPaths.length === 0) return null;
617
+ const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
618
+ if (isBashLike) {
619
+ const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
620
+ const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
621
+ const detailUnquotedRedirect = detailNoDevnull.replace(
622
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
623
+ (_m, _q, p) => ">" + p
624
+ );
625
+ const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
626
+ const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
627
+ let hasWriteSeg = hasRedirectWrite;
628
+ if (!hasWriteSeg) {
629
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
630
+ const trimmed = seg.trim();
631
+ if (!trimmed) continue;
632
+ const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
633
+ if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
634
+ hasWriteSeg = true;
635
+ break;
636
+ }
637
+ if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
638
+ hasWriteSeg = true;
639
+ break;
640
+ }
641
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
642
+ hasWriteSeg = true;
643
+ break;
644
+ }
645
+ }
646
+ }
647
+ if (!hasWriteSeg) return null;
648
+ }
649
+ const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
650
+ const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
651
+ const targetPaths = [];
652
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
653
+ const trimmed = seg.trim();
654
+ if (!trimmed) continue;
655
+ const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
656
+ if (firstWord === "cd") {
657
+ continue;
658
+ }
659
+ if (READ_ONLY_COMMANDS.has(firstWord)) {
660
+ const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
661
+ const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
662
+ const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
663
+ const m = segNoQuotes.match(REDIRECT_TARGET_RE);
664
+ if (m) targetPaths.push(m[1] || m[2] || m[3]);
665
+ continue;
666
+ }
667
+ const segPaths = extractFilePaths(trimmed);
668
+ const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
669
+ if (SOURCE_ARG_CMDS.has(firstWord)) {
670
+ if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
671
+ } else if (firstWord === "dd") {
672
+ const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
673
+ const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
674
+ const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
675
+ const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
676
+ for (const sp of segPaths) {
677
+ if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
678
+ }
679
+ } else {
680
+ targetPaths.push(...segPathsFiltered);
681
+ }
682
+ }
683
+ const uniqueTargetPaths = [...new Set(targetPaths)];
684
+ if (uniqueTargetPaths.length === 0) return null;
508
685
  const historyReadPaths = /* @__PURE__ */ new Set();
509
686
  for (const action of actionHistory) {
510
687
  const histDetail = action.actionDetail.toLowerCase();
@@ -517,7 +694,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
517
694
  }
518
695
  }
519
696
  const unreadPaths = [];
520
- for (const path3 of targetPaths) {
697
+ for (const path3 of uniqueTargetPaths) {
521
698
  const hasRead = [...historyReadPaths].some(
522
699
  (rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
523
700
  );
@@ -580,6 +757,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
580
757
  violatedLayers: ["2"]
581
758
  };
582
759
  }
760
+ function containsPrivilegeEscalation(actionDetail) {
761
+ const ad = actionDetail.toLowerCase();
762
+ for (const seg of ad.split(/&&|\|\||;|\n/)) {
763
+ const trimmed = seg.trim();
764
+ if (!trimmed) continue;
765
+ const parts = trimmed.split(/\s+/);
766
+ const fw = parts[0]?.toLowerCase() || "";
767
+ if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
768
+ if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
769
+ }
770
+ return false;
771
+ }
583
772
  function checkIndirectFileAccess(actionType, actionDetail, cwd) {
584
773
  if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
585
774
  return null;
@@ -684,12 +873,13 @@ var init_logic_rules = __esm({
684
873
  { path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
685
874
  // 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
686
875
  { path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
687
- { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
876
+ { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
688
877
  { path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
689
878
  // SSH 主机私钥
690
- { path: "/etc/ssh/ssh_host_rsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
691
- { path: "/etc/ssh/ssh_host_ed25519_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
692
- { path: "/etc/ssh/ssh_host_ecdsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
879
+ // SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
880
+ { path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
881
+ { path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
882
+ { path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
693
883
  ];
694
884
  HOME_SENSITIVE_PATHS = [
695
885
  { path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
@@ -730,7 +920,7 @@ var init_logic_rules = __esm({
730
920
  "cat"
731
921
  ]);
732
922
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
733
- 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"];
923
+ 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"];
734
924
  WRITE_COMMAND_RES = [
735
925
  /\brm\b/,
736
926
  /\bunlink\b/,
@@ -776,6 +966,12 @@ var init_logic_rules = __esm({
776
966
  "nl",
777
967
  "tac",
778
968
  "rev",
969
+ // NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
970
+ // read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
971
+ // 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
972
+ // echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
973
+ "echo",
974
+ "printf",
779
975
  "lsblk",
780
976
  "blockdev",
781
977
  "smartctl",
@@ -828,7 +1024,7 @@ var init_logic_rules = __esm({
828
1024
  "modinfo",
829
1025
  "sysctl"
830
1026
  ]);
831
- DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/;
1027
+ DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
832
1028
  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"];
833
1029
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
834
1030
  PASSWORD_MODIFY_PATTERNS = [
@@ -878,7 +1074,7 @@ var init_logic_rules = __esm({
878
1074
  // src/engine/skill-loader.ts
879
1075
  import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
880
1076
  import { resolve, dirname } from "path";
881
- import { fileURLToPath as fileURLToPath2 } from "url";
1077
+ import { fileURLToPath as fileURLToPath3 } from "url";
882
1078
  function loadAllSkills() {
883
1079
  if (cachedSkills) return cachedSkills;
884
1080
  cachedSkills = {};
@@ -949,7 +1145,7 @@ var init_skill_loader = __esm({
949
1145
  "src/engine/skill-loader.ts"() {
950
1146
  "use strict";
951
1147
  init_esm_shims();
952
- __filename2 = fileURLToPath2(import.meta.url);
1148
+ __filename2 = fileURLToPath3(import.meta.url);
953
1149
  __dirname2 = dirname(__filename2);
954
1150
  SKILLS_DIR = resolve(__dirname2, "../skills");
955
1151
  SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
@@ -1024,7 +1220,7 @@ __export(runtime_config_exports, {
1024
1220
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1025
1221
  import { join } from "path";
1026
1222
  import { homedir } from "os";
1027
- import { fileURLToPath as fileURLToPath3 } from "url";
1223
+ import { fileURLToPath as fileURLToPath4 } from "url";
1028
1224
  import { dirname as dirname2 } from "path";
1029
1225
  function getRuntimeConfigPath() {
1030
1226
  const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
@@ -1880,7 +2076,7 @@ var init_runtime_config = __esm({
1880
2076
  init_heuristic();
1881
2077
  init_logic_rules();
1882
2078
  init_skill_loader();
1883
- __dirname3 = dirname2(fileURLToPath3(import.meta.url));
2079
+ __dirname3 = dirname2(fileURLToPath4(import.meta.url));
1884
2080
  RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
1885
2081
  RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
1886
2082
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
@@ -1903,7 +2099,7 @@ var init_runtime_config = __esm({
1903
2099
  // src/agents/index.ts
1904
2100
  import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
1905
2101
  import { resolve as resolve2, dirname as dirname3 } from "path";
1906
- import { fileURLToPath as fileURLToPath4 } from "url";
2102
+ import { fileURLToPath as fileURLToPath5 } from "url";
1907
2103
  function resolveAgentsDir() {
1908
2104
  if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
1909
2105
  if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
@@ -1952,7 +2148,7 @@ var init_agents = __esm({
1952
2148
  "src/agents/index.ts"() {
1953
2149
  "use strict";
1954
2150
  init_esm_shims();
1955
- __filename3 = fileURLToPath4(import.meta.url);
2151
+ __filename3 = fileURLToPath5(import.meta.url);
1956
2152
  __dirname4 = dirname3(__filename3);
1957
2153
  AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
1958
2154
  AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
@@ -1972,19 +2168,25 @@ function runHeuristicCheck(input) {
1972
2168
  const re = new RegExp(rule.pattern);
1973
2169
  if (re.test(command)) {
1974
2170
  if (rule.action === "Deny") {
1975
- matched.push(`\u81EA\u5B9A\u4E49\u89C4\u5219: ${rule.pattern}`);
1976
- if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1977
- highestSeverity = rule.severity;
1978
- highestRiskType = "custom_rule";
1979
- }
2171
+ return {
2172
+ allowed: false,
2173
+ reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
2174
+ riskLevel: rule.severity,
2175
+ riskType: "sensitive_action",
2176
+ confidence: 90,
2177
+ source: "heuristic",
2178
+ violatedLayers: ["1.1"]
2179
+ };
1980
2180
  }
1981
2181
  }
1982
2182
  } catch {
1983
2183
  }
1984
2184
  }
2185
+ const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
2186
+ const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
1985
2187
  let topRiskType = "";
1986
2188
  for (const rule of DANGEROUS_PATTERNS) {
1987
- if (rule.pattern.test(command)) {
2189
+ if (rule.pattern.test(scanCommand)) {
1988
2190
  matched.push(rule.description);
1989
2191
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1990
2192
  highestSeverity = rule.severity;
@@ -1998,7 +2200,7 @@ function runHeuristicCheck(input) {
1998
2200
  for (const rule of agentExt.extra_command_patterns) {
1999
2201
  try {
2000
2202
  const re = new RegExp(rule.pattern, "i");
2001
- if (re.test(command)) {
2203
+ if (re.test(scanCommand)) {
2002
2204
  matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
2003
2205
  if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
2004
2206
  highestSeverity = rule.risk_level;
@@ -2022,14 +2224,12 @@ function runHeuristicCheck(input) {
2022
2224
  const actionTypeL = input.aNext.actionType.toLowerCase();
2023
2225
  const detailL = input.aNext.actionDetail.toLowerCase();
2024
2226
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2227
+ const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2228
+ if (dangerousResult) return dangerousResult;
2025
2229
  if (!isExcludedType) {
2026
2230
  const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2027
2231
  if (pathResult) return pathResult;
2028
2232
  }
2029
- const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2030
- if (dangerousResult) return dangerousResult;
2031
- const lateralResult = checkLateralMovement(detailL);
2032
- if (lateralResult) return lateralResult;
2033
2233
  if (matched.length > 0) {
2034
2234
  return {
2035
2235
  allowed: false,
@@ -2042,6 +2242,8 @@ function runHeuristicCheck(input) {
2042
2242
  // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2043
2243
  };
2044
2244
  }
2245
+ const lateralResult = checkLateralMovement(detailL);
2246
+ if (lateralResult) return lateralResult;
2045
2247
  return null;
2046
2248
  }
2047
2249
  function checkInjectionKeywords(text, extraKeywords) {
@@ -2064,6 +2266,68 @@ function isInlineScriptCommand(actionDetail) {
2064
2266
  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;
2065
2267
  return re.test(actionDetail);
2066
2268
  }
2269
+ function stripLiteralTextSegments(actionDetail) {
2270
+ const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
2271
+ const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
2272
+ if (writeScriptExec || pipeToInterpreter) {
2273
+ return actionDetail;
2274
+ }
2275
+ const resultParts = [];
2276
+ for (const seg of splitCommandSegments(actionDetail)) {
2277
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
2278
+ if (fw === "echo" || fw === "printf") {
2279
+ const segUnquotedRedirect = seg.replace(
2280
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
2281
+ (_m, _q, p) => ">" + p
2282
+ );
2283
+ resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
2284
+ } else {
2285
+ resultParts.push(seg);
2286
+ }
2287
+ }
2288
+ return resultParts.join("");
2289
+ }
2290
+ function splitCommandSegments(actionDetail) {
2291
+ const segments = [];
2292
+ let cur = "";
2293
+ let quote = null;
2294
+ for (let i = 0; i < actionDetail.length; i++) {
2295
+ const ch = actionDetail[i];
2296
+ if (quote) {
2297
+ cur += ch;
2298
+ if (ch === quote) quote = null;
2299
+ continue;
2300
+ }
2301
+ if (ch === "'" || ch === '"') {
2302
+ quote = ch;
2303
+ cur += ch;
2304
+ continue;
2305
+ }
2306
+ if (ch === ";" || ch === "\n") {
2307
+ segments.push(cur);
2308
+ segments.push(ch);
2309
+ cur = "";
2310
+ continue;
2311
+ }
2312
+ if (ch === "&" && actionDetail[i + 1] === "&") {
2313
+ segments.push(cur);
2314
+ segments.push("&&");
2315
+ cur = "";
2316
+ i++;
2317
+ continue;
2318
+ }
2319
+ if (ch === "|" && actionDetail[i + 1] === "|") {
2320
+ segments.push(cur);
2321
+ segments.push("||");
2322
+ cur = "";
2323
+ i++;
2324
+ continue;
2325
+ }
2326
+ cur += ch;
2327
+ }
2328
+ if (cur) segments.push(cur);
2329
+ return segments;
2330
+ }
2067
2331
  function severityRank(s) {
2068
2332
  const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
2069
2333
  return ranks[s] || 0;
@@ -2071,13 +2335,18 @@ function severityRank(s) {
2071
2335
  function checkSensitivePath(command, cwd, agentExt) {
2072
2336
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2073
2337
  const isWriteOpFlag = isWriteOperation("", command);
2338
+ const pathScanText = stripLiteralTextSegments(command);
2074
2339
  const pathMatches = (text, path3) => {
2075
- const escaped = path3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2340
+ const parts = path3.split(/(\(\?!.*?\))/);
2341
+ const escaped = parts.map((part) => {
2342
+ if (part.startsWith("(?!")) return part;
2343
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2344
+ }).join("");
2076
2345
  const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
2077
2346
  return re.test(text);
2078
2347
  };
2079
2348
  for (const sp of ROOT_SENSITIVE_PATHS) {
2080
- if (pathMatches(command, sp.path.toLowerCase())) {
2349
+ if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2081
2350
  if (sp.path === "/dev/random") {
2082
2351
  if (isWriteOpFlag) {
2083
2352
  continue;
@@ -2111,7 +2380,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2111
2380
  const expanded = `${expandedCwd}/${sp.path}`;
2112
2381
  const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2113
2382
  const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2114
- if (pathRe.test(command) || expandedRe.test(command)) {
2383
+ if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2115
2384
  if (!isWriteOpFlag && !sp.credential) {
2116
2385
  continue;
2117
2386
  }
@@ -2182,8 +2451,10 @@ function checkSensitivePath(command, cwd, agentExt) {
2182
2451
  }
2183
2452
  function checkDangerousPatterns(actionType, actionDetail) {
2184
2453
  if (actionType === "file_write" || actionType === "file_edit") return null;
2185
- const TEMP_DIRS = ["/tmp/", "/var/tmp/", "/run/", "/dev/shm/"];
2186
- const isTempPath = TEMP_DIRS.some((td) => actionDetail.includes(td));
2454
+ const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2455
+ const isTempPath = TEMP_DIRS.some(
2456
+ (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
2457
+ );
2187
2458
  if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
2188
2459
  return {
2189
2460
  allowed: false,
@@ -2195,7 +2466,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
2195
2466
  violatedLayers: ["1.1"]
2196
2467
  };
2197
2468
  }
2198
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail) || />\s*\/proc\//.test(actionDetail)) {
2469
+ const findWriteMatch = actionDetail.match(
2470
+ /\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
2471
+ );
2472
+ if (findWriteMatch) {
2473
+ const cmdHit = findWriteMatch[1] || "xargs";
2474
+ return {
2475
+ allowed: false,
2476
+ reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
2477
+ riskLevel: "high",
2478
+ riskType: "script_execution",
2479
+ confidence: 90,
2480
+ source: "heuristic",
2481
+ violatedLayers: ["1.1"]
2482
+ };
2483
+ }
2484
+ if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
2199
2485
  return {
2200
2486
  allowed: false,
2201
2487
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -2206,17 +2492,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
2206
2492
  violatedLayers: ["1.1"]
2207
2493
  };
2208
2494
  }
2495
+ if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
2496
+ return {
2497
+ allowed: false,
2498
+ reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
2499
+ riskLevel: "high",
2500
+ riskType: "file_access",
2501
+ confidence: 90,
2502
+ source: "heuristic",
2503
+ violatedLayers: ["1.1"]
2504
+ };
2505
+ }
2209
2506
  return null;
2210
2507
  }
2211
2508
  function extractRemoteHost(command) {
2212
- const m = command.match(/(?:ssh|scp|rsync)\s+(?:-[\w]+\s+)*(\S+@\S+|\b(?!-)[\w.-]+\b)/);
2213
- return m ? m[1] : null;
2509
+ const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
2510
+ for (const tok of tokens) {
2511
+ const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
2512
+ if (am && am[2] && !isLocalHost(am[2])) {
2513
+ return `${am[1]}@${am[2]}`;
2514
+ }
2515
+ }
2516
+ for (const tok of tokens) {
2517
+ const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
2518
+ if (hm && hm[1] && !isLocalHost(hm[1])) {
2519
+ return hm[1];
2520
+ }
2521
+ }
2522
+ const skipValueOpts = /* @__PURE__ */ new Set([
2523
+ "-p",
2524
+ "-l",
2525
+ "-i",
2526
+ "-o",
2527
+ "-b",
2528
+ "-c",
2529
+ "-m",
2530
+ "-F",
2531
+ "-e",
2532
+ "-S",
2533
+ "-W",
2534
+ "-J",
2535
+ "-I",
2536
+ "-T",
2537
+ "-B",
2538
+ "-L",
2539
+ "-R",
2540
+ "-D",
2541
+ "-E",
2542
+ "-M",
2543
+ "-O",
2544
+ "-P",
2545
+ "-q"
2546
+ ]);
2547
+ let foundCmd = false;
2548
+ for (let i = 0; i < tokens.length; i++) {
2549
+ const t = tokens[i];
2550
+ if (t === "ssh" || t === "scp" || t === "rsync") {
2551
+ foundCmd = true;
2552
+ continue;
2553
+ }
2554
+ if (!foundCmd) continue;
2555
+ if (t.startsWith("-")) {
2556
+ if (skipValueOpts.has(t) && i + 1 < tokens.length) {
2557
+ i++;
2558
+ }
2559
+ continue;
2560
+ }
2561
+ if (t.startsWith("/")) continue;
2562
+ return t;
2563
+ }
2564
+ return null;
2214
2565
  }
2215
2566
  function isLocalHost(host) {
2216
2567
  const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
2217
2568
  return localNames.includes(host.replace(/.*@/, ""));
2218
2569
  }
2219
2570
  function checkLateralMovement(actionDetail) {
2571
+ if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
2572
+ const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
2573
+ let hasRealCall = false;
2574
+ for (const seg of segments) {
2575
+ const parts = seg.split(/\s+/);
2576
+ const fw = (parts[0] || "").toLowerCase();
2577
+ if (fw === "echo" || fw === "printf") continue;
2578
+ if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
2579
+ hasRealCall = true;
2580
+ break;
2581
+ }
2582
+ }
2583
+ if (!hasRealCall) return null;
2220
2584
  if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
2221
2585
  const targetHost = extractRemoteHost(actionDetail);
2222
2586
  if (targetHost && !isLocalHost(targetHost)) {
@@ -2316,10 +2680,14 @@ var init_heuristic = __esm({
2316
2680
  { 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" },
2317
2681
  { pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
2318
2682
  // --- Critical: remote code execution ---
2319
- { pattern: /curl\s+.*\|\s*(bash|sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2683
+ // curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
2684
+ // 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
2685
+ // runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
2686
+ { pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
2687
+ { pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2320
2688
  { pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
2321
2689
  { pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
2322
- { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "critical", description: "base64 \u89E3\u7801\u6267\u884C", riskType: "script_execution" },
2690
+ { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
2323
2691
  // --- Critical: reverse shell ---
2324
2692
  { pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
2325
2693
  { pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
@@ -2330,13 +2698,25 @@ var init_heuristic = __esm({
2330
2698
  { pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
2331
2699
  // --- Critical: privilege escalation / container escape ---
2332
2700
  { 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" },
2333
- // --- High: dangerous permissions / ownership ---
2334
- { pattern: /chmod\s+[0-7]*777[0-7]*/i, severity: "high", description: "\u5168\u6743\u9650\u8BBE\u7F6E (chmod 777)", riskType: "script_execution" },
2701
+ // --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
2702
+ // 修正「只拦 777、xx7 漏放」的缺口:
2703
+ // 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
2704
+ // 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
2705
+ // 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
2706
+ // 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
2707
+ // 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
2708
+ { 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" },
2709
+ { 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" },
2710
+ { 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" },
2335
2711
  { 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" },
2336
2712
  // sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
2337
2713
  { 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" },
2338
2714
  // --- High: system config overwrite ---
2715
+ // > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
2716
+ // 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
2339
2717
  { pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
2718
+ { pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
2719
+ // 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
2340
2720
  // --- High: download script + execute risk ---
2341
2721
  { 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" },
2342
2722
  { pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
@@ -2352,7 +2732,11 @@ var init_heuristic = __esm({
2352
2732
  // --- Medium: credential export ---
2353
2733
  { pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
2354
2734
  // --- High: shell rc file append (persistence) ---
2735
+ // 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
2736
+ // 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
2737
+ // /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
2355
2738
  { 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" },
2739
+ { 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" },
2356
2740
  // --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
2357
2741
  { 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" },
2358
2742
  // --- High: non-interactive password modification ---
@@ -2632,7 +3016,7 @@ var require_bindings = __commonJS({
2632
3016
  init_esm_shims();
2633
3017
  var fs4 = __require("fs");
2634
3018
  var path3 = __require("path");
2635
- var fileURLToPath7 = require_file_uri_to_path();
3019
+ var fileURLToPath8 = require_file_uri_to_path();
2636
3020
  var join6 = path3.join;
2637
3021
  var dirname7 = path3.dirname;
2638
3022
  var exists = fs4.accessSync && function(path4) {
@@ -2744,7 +3128,7 @@ var require_bindings = __commonJS({
2744
3128
  Error.stackTraceLimit = origSTL;
2745
3129
  var fileSchema = "file://";
2746
3130
  if (fileName.indexOf(fileSchema) === 0) {
2747
- fileName = fileURLToPath7(fileName);
3131
+ fileName = fileURLToPath8(fileName);
2748
3132
  }
2749
3133
  return fileName;
2750
3134
  };
@@ -3411,8 +3795,8 @@ init_skill_loader();
3411
3795
  init_esm_shims();
3412
3796
  import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
3413
3797
  import { resolve as resolve3, dirname as dirname4 } from "path";
3414
- import { fileURLToPath as fileURLToPath5 } from "url";
3415
- var __filename4 = fileURLToPath5(import.meta.url);
3798
+ import { fileURLToPath as fileURLToPath6 } from "url";
3799
+ var __filename4 = fileURLToPath6(import.meta.url);
3416
3800
  var __dirname5 = dirname4(__filename4);
3417
3801
  var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
3418
3802
  var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
@@ -4327,11 +4711,13 @@ async function analyzeWithLLM(prompt, config) {
4327
4711
  } catch {
4328
4712
  }
4329
4713
  clearTimeout(totalTimeoutTimer);
4714
+ const riskLevel = parsed.riskLevel ?? parsed.risk_level;
4715
+ const riskType = parsed.riskType ?? parsed.risk_type;
4330
4716
  return {
4331
4717
  allowed: parsed.allowed !== false,
4332
4718
  reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
4333
- riskLevel: ["low", "medium", "high", "critical"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",
4334
- riskType: parsed.riskType || "",
4719
+ riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
4720
+ riskType: riskType || "",
4335
4721
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
4336
4722
  source: "llm",
4337
4723
  violatedLayers: ["3"]
@@ -4449,7 +4835,10 @@ async function runAnalysis(input) {
4449
4835
  if (heuristicResult && !heuristicResult.allowed) {
4450
4836
  const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
4451
4837
  if (!skipInlineFileAccess) {
4452
- return heuristicResult;
4838
+ const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
4839
+ if (shouldDenyL1) {
4840
+ return heuristicResult;
4841
+ }
4453
4842
  }
4454
4843
  }
4455
4844
  let scriptChain = null;
@@ -4527,7 +4916,8 @@ async function runAnalysis(input) {
4527
4916
  ) && DANGEROUS_PATTERNS.some(
4528
4917
  (p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
4529
4918
  );
4530
- if (!fullCmdHit) {
4919
+ const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
4920
+ if (!fullCmdHit && !hasRedirectWrite) {
4531
4921
  return {
4532
4922
  allowed: true,
4533
4923
  reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
@@ -4854,8 +5244,8 @@ init_token_stats();
4854
5244
  import { Hono } from "hono";
4855
5245
  import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
4856
5246
  import { join as join5, dirname as dirname6 } from "path";
4857
- import { fileURLToPath as fileURLToPath6 } from "url";
4858
- var __dirname6 = dirname6(fileURLToPath6(import.meta.url));
5247
+ import { fileURLToPath as fileURLToPath7 } from "url";
5248
+ var __dirname6 = dirname6(fileURLToPath7(import.meta.url));
4859
5249
  var STATIC_DIR = join5(__dirname6, "static");
4860
5250
  var CONSOLE_TOKEN_ENV = "AGENT_MOSS_CONSOLE_TOKEN";
4861
5251
  function getConsoleToken() {