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