@kenhkl/agent-moss 0.10.5 → 0.11.2

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.
@@ -32,6 +32,90 @@ var init_esm_shims = __esm({
32
32
  }
33
33
  });
34
34
 
35
+ // src/infra/paths.ts
36
+ import { join } from "path";
37
+ import { homedir } from "os";
38
+ import { readFileSync } from "fs";
39
+ function isRoot() {
40
+ const getuid = process.getuid;
41
+ return typeof getuid === "function" && getuid() === 0;
42
+ }
43
+ function resolveOwnerHome() {
44
+ for (const name of [process.env.SUDO_USER, process.env.LOGNAME, process.env.USER]) {
45
+ if (name && name !== "root" && name !== "") {
46
+ const home = homeOf(name);
47
+ if (home) return home;
48
+ }
49
+ }
50
+ try {
51
+ const passwd = readFileSync("/etc/passwd", "utf-8");
52
+ for (const line of passwd.split("\n")) {
53
+ if (!line || line.startsWith("#")) continue;
54
+ const parts = line.split(":");
55
+ if (parts.length < 6) continue;
56
+ const uid = parseInt(parts[2], 10);
57
+ const name = parts[0];
58
+ if (uid >= 1e3 && name && !name.startsWith("_")) {
59
+ return parts[5];
60
+ }
61
+ }
62
+ } catch {
63
+ }
64
+ return null;
65
+ }
66
+ function homeOf(name) {
67
+ try {
68
+ const passwd = readFileSync("/etc/passwd", "utf-8");
69
+ for (const line of passwd.split("\n")) {
70
+ if (!line || line.startsWith("#")) continue;
71
+ const parts = line.split(":");
72
+ if (parts.length >= 6 && parts[0] === name) {
73
+ return parts[5];
74
+ }
75
+ }
76
+ } catch {
77
+ }
78
+ return null;
79
+ }
80
+ function getDataRoot() {
81
+ const envPath = process.env[ENV_DATA_ROOT];
82
+ if (envPath) return envPath;
83
+ if (isRoot()) {
84
+ const ownerHome = resolveOwnerHome();
85
+ if (ownerHome) {
86
+ return join(ownerHome, ".config", "agentmoss");
87
+ }
88
+ }
89
+ return DEFAULT_DATA_ROOT;
90
+ }
91
+ function getInstance() {
92
+ const v = process.env[ENV_INSTANCE] || "";
93
+ return v.trim().replace(/^[/\\]+|[/\\]+$/g, "");
94
+ }
95
+ function getConfigDir() {
96
+ return getDataRoot();
97
+ }
98
+ function getRuntimeConfigDir() {
99
+ return getDataRoot();
100
+ }
101
+ function getTokenStatsPath() {
102
+ const instance = getInstance();
103
+ if (instance) {
104
+ return join(getDataRoot(), instance, "agent_moss_token_stats.json");
105
+ }
106
+ return join(getDataRoot(), "agent_moss_token_stats.json");
107
+ }
108
+ var ENV_DATA_ROOT, ENV_INSTANCE, DEFAULT_DATA_ROOT;
109
+ var init_paths = __esm({
110
+ "src/infra/paths.ts"() {
111
+ "use strict";
112
+ init_esm_shims();
113
+ ENV_DATA_ROOT = "AGENT_MOSS_DATA_DIR";
114
+ ENV_INSTANCE = "AGENT_MOSS_INSTANCE";
115
+ DEFAULT_DATA_ROOT = join(homedir(), ".config", "agentmoss");
116
+ }
117
+ });
118
+
35
119
  // src/config.ts
36
120
  var config_exports = {};
37
121
  __export(config_exports, {
@@ -45,11 +129,12 @@ __export(config_exports, {
45
129
  isHeuristicEnabled: () => isHeuristicEnabled,
46
130
  isLLMAvailable: () => isLLMAvailable,
47
131
  isLogicRulesEnabled: () => isLogicRulesEnabled,
132
+ probeLLMConnection: () => probeLLMConnection,
48
133
  updateLLMConfig: () => updateLLMConfig
49
134
  });
50
135
  import fs from "fs";
51
136
  import path2 from "path";
52
- import os from "os";
137
+ import { fileURLToPath as fileURLToPath2 } from "url";
53
138
  function loadConfigFile(filePath) {
54
139
  try {
55
140
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -61,7 +146,7 @@ function loadConfigFile(filePath) {
61
146
  function resolveConfigPath() {
62
147
  const envPath = process.env.AGENT_MOSS_CONFIG_PATH;
63
148
  if (envPath) return envPath;
64
- const userConfig = path2.join(os.homedir(), ".config", "agentmoss", "config.json");
149
+ const userConfig = path2.join(getConfigDir(), "config.json");
65
150
  if (fs.existsSync(userConfig)) return userConfig;
66
151
  const defaultConfig = path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example");
67
152
  if (fs.existsSync(defaultConfig)) {
@@ -112,16 +197,38 @@ function getTimeoutConfig() {
112
197
  function getRetryConfig() {
113
198
  return retryConfig;
114
199
  }
200
+ function wildcardToRegExp(pattern) {
201
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
202
+ }
115
203
  function loadCustomRules() {
204
+ const rules = [];
116
205
  try {
117
206
  const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
118
- if (!raw) return [];
119
- const parsed = JSON.parse(raw);
120
- if (!Array.isArray(parsed)) return [];
121
- return parsed.filter((r) => r.pattern && r.action);
207
+ if (raw) {
208
+ const parsed = JSON.parse(raw);
209
+ if (Array.isArray(parsed)) {
210
+ for (const r of parsed) {
211
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
212
+ }
213
+ }
214
+ }
122
215
  } catch {
123
- return [];
124
216
  }
217
+ for (const rulesPath of USER_RULES_PATHS) {
218
+ try {
219
+ if (!fs.existsSync(rulesPath)) continue;
220
+ const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
221
+ const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
222
+ for (const r of actions) {
223
+ const p = r?.pattern;
224
+ if (!p) continue;
225
+ rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
226
+ }
227
+ break;
228
+ } catch {
229
+ }
230
+ }
231
+ return rules;
125
232
  }
126
233
  function getLLMConfig() {
127
234
  return llmConfig;
@@ -136,6 +243,30 @@ function isLLMAvailable() {
136
243
  if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
137
244
  return llmConfig.apiKey.length > 0;
138
245
  }
246
+ async function probeLLMConnection(timeoutMs = 8e3) {
247
+ if (!isLLMAvailable()) return false;
248
+ const OpenAI2 = (await import("openai")).default;
249
+ const config = getLLMConfig();
250
+ const provider = detectProvider(config.baseUrl, config.provider);
251
+ const extraHeaders = getProviderHeaders(provider);
252
+ const client = new OpenAI2({
253
+ baseURL: config.baseUrl,
254
+ apiKey: config.apiKey,
255
+ timeout: timeoutMs,
256
+ maxRetries: 0,
257
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
258
+ });
259
+ try {
260
+ const resp = await client.chat.completions.create({
261
+ model: config.model,
262
+ messages: [{ role: "user", content: "ping" }],
263
+ max_tokens: 1
264
+ });
265
+ return !!(resp && resp.choices && resp.choices[0]);
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
139
270
  function getLLMFailMode() {
140
271
  const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
141
272
  return val === "fail_closed" ? "fail_closed" : "fail_open";
@@ -179,11 +310,12 @@ function isHeuristicEnabled() {
179
310
  function isLogicRulesEnabled() {
180
311
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
181
312
  }
182
- var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
313
+ var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
183
314
  var init_config = __esm({
184
315
  "src/config.ts"() {
185
316
  "use strict";
186
317
  init_esm_shims();
318
+ init_paths();
187
319
  llmConfig = (() => {
188
320
  const configPath = resolveConfigPath();
189
321
  const fileConfig = fs.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
@@ -201,6 +333,21 @@ var init_config = __esm({
201
333
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
202
334
  };
203
335
  })();
336
+ __configModuleDir = (() => {
337
+ try {
338
+ return path2.dirname(fileURLToPath2(import.meta.url));
339
+ } catch {
340
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
341
+ }
342
+ })();
343
+ USER_RULES_PATHS = [
344
+ path2.join(__configModuleDir, "rules", "user_rules.json"),
345
+ // 源在 ts/src/rules(一般不存在)
346
+ path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
347
+ // dist: dist/engine/../rules = dist/rules ✅
348
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
349
+ // dev src: ts/src/../../agent_moss
350
+ ];
204
351
  customRules = loadCustomRules();
205
352
  PROVIDER_HEADERS = {
206
353
  "openrouter": {
@@ -217,7 +364,17 @@ var init_config = __esm({
217
364
  // src/engine/inline-analyzer.ts
218
365
  function isLiteralTextCommand(command) {
219
366
  ECHO_CMD_RE.lastIndex = 0;
220
- return ECHO_CMD_RE.test(command);
367
+ if (!ECHO_CMD_RE.test(command)) return false;
368
+ if (command.includes(">")) {
369
+ let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
370
+ d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
371
+ d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
372
+ d = d.replace(/'[^']*'|"[^"]*"/g, "");
373
+ if (/>{1,2}\s*(?!&)\S/.test(d)) {
374
+ return false;
375
+ }
376
+ }
377
+ return true;
221
378
  }
222
379
  function extractInlineCode(command) {
223
380
  const parts = [];
@@ -294,7 +451,7 @@ function analyzeInlineScript(command) {
294
451
  riskType: "file_access"
295
452
  };
296
453
  }
297
- var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
454
+ var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SHELL_RC_FRAGMENTS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
298
455
  var init_inline_analyzer = __esm({
299
456
  "src/engine/inline-analyzer.ts"() {
300
457
  "use strict";
@@ -364,6 +521,13 @@ var init_inline_analyzer = __esm({
364
521
  // xargs bash / xargs sh
365
522
  /\|\s*xargs\s+(?:bash|sh|zsh)/gi
366
523
  ];
524
+ SHELL_RC_FRAGMENTS = [
525
+ ".bashrc",
526
+ ".zshrc",
527
+ ".profile",
528
+ ".bash_profile",
529
+ ".zprofile"
530
+ ];
367
531
  SENSITIVE_PATH_FRAGMENTS = [
368
532
  "/etc/shadow",
369
533
  "/etc/gshadow",
@@ -380,9 +544,10 @@ var init_inline_analyzer = __esm({
380
544
  "/dev/kmsg",
381
545
  ".ssh/id_rsa",
382
546
  ".ssh/id_ed25519",
383
- ".ssh/authorized_keys"
547
+ ".ssh/authorized_keys",
548
+ ...SHELL_RC_FRAGMENTS
384
549
  ];
385
- REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
550
+ REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
386
551
  DIR_TRAVERSAL_KEYWORDS = [
387
552
  // Python 目录遍历
388
553
  "os.listdir",
@@ -405,11 +570,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
405
570
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
406
571
  if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
407
572
  if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
573
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
574
+ const trimmed = seg.trim();
575
+ if (!trimmed) continue;
576
+ const parts = trimmed.split(/\s+/);
577
+ const fw = parts[0]?.toLowerCase() || "";
578
+ if (fw === "wget" || fw === "curl") {
579
+ const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
580
+ if (isStdout) continue;
581
+ 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);
582
+ if (isDevnull) continue;
583
+ if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
584
+ return true;
585
+ }
586
+ }
587
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
588
+ return true;
589
+ }
590
+ }
408
591
  if (actionDetail.includes(">")) {
409
- const detailWithoutDevnull = actionDetail.replace(DEVNULL_REDIRECT_RE, "");
410
- if (detailWithoutDevnull.includes(">")) {
411
- const firstWord = actionDetail.trim().split(/\s+/)[0]?.toLowerCase() || "";
412
- if (!READ_ONLY_COMMANDS.has(firstWord)) return true;
592
+ const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
593
+ const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
594
+ const detailUnquotedRedirect = detailWithoutDevnull.replace(
595
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
596
+ (_m, _q, p) => ">" + p
597
+ );
598
+ const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
599
+ if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
600
+ return true;
413
601
  }
414
602
  }
415
603
  return false;
@@ -433,27 +621,31 @@ function runLogicCheck(input) {
433
621
  if (rbwResult) return rbwResult;
434
622
  if (input.promptSession && input.promptSession.trim()) {
435
623
  if (!isExcludedType) {
436
- const promptLower = input.promptSession.toLowerCase();
437
- for (const pattern of INTENT_DEVIATION_PATTERNS) {
438
- const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
439
- const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
440
- if (intentMatched && dangerousMatched) {
441
- return {
442
- allowed: false,
443
- reason: pattern.reason,
444
- riskLevel: "high",
445
- riskType: "intent_deviation",
446
- confidence: 85,
447
- source: "logic_rule",
448
- violatedLayers: ["2"]
449
- };
624
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
625
+ } else {
626
+ const promptLower = input.promptSession.toLowerCase();
627
+ for (const pattern of INTENT_DEVIATION_PATTERNS) {
628
+ const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
629
+ const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
630
+ if (intentMatched && dangerousMatched) {
631
+ return {
632
+ allowed: false,
633
+ reason: pattern.reason,
634
+ riskLevel: "high",
635
+ riskType: "intent_deviation",
636
+ confidence: 85,
637
+ source: "logic_rule",
638
+ violatedLayers: ["2"]
639
+ };
640
+ }
450
641
  }
451
642
  }
452
643
  }
453
644
  }
454
645
  const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
455
646
  if (indirectResult) return indirectResult;
456
- if (actionDetail.includes("sudo") || actionDetail.includes("su -") || actionDetail.includes("chmod u+s") || actionDetail.includes("setuid")) {
647
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
648
+ } else if (containsPrivilegeEscalation(actionDetail)) {
457
649
  const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
458
650
  const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
459
651
  if (!isAdminCmd) {
@@ -483,6 +675,9 @@ function extractFilePaths(text) {
483
675
  const paths = [];
484
676
  const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
485
677
  if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
678
+ const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
679
+ const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
680
+ if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
486
681
  const quotedPaths = text.match(/["']\/([^"']+)["']/g);
487
682
  if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
488
683
  return [...new Set(paths)];
@@ -493,8 +688,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
493
688
  const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
494
689
  const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
495
690
  if (!isWrite) return null;
496
- const targetPaths = extractFilePaths(actionDetail);
497
- if (targetPaths.length === 0) return null;
691
+ const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
692
+ if (isBashLike) {
693
+ const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
694
+ const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
695
+ const detailUnquotedRedirect = detailNoDevnull.replace(
696
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
697
+ (_m, _q, p) => ">" + p
698
+ );
699
+ const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
700
+ const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
701
+ let hasWriteSeg = hasRedirectWrite;
702
+ if (!hasWriteSeg) {
703
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
704
+ const trimmed = seg.trim();
705
+ if (!trimmed) continue;
706
+ const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
707
+ if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
708
+ hasWriteSeg = true;
709
+ break;
710
+ }
711
+ if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
712
+ hasWriteSeg = true;
713
+ break;
714
+ }
715
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
716
+ hasWriteSeg = true;
717
+ break;
718
+ }
719
+ }
720
+ }
721
+ if (!hasWriteSeg) return null;
722
+ }
723
+ const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
724
+ const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
725
+ const targetPaths = [];
726
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
727
+ const trimmed = seg.trim();
728
+ if (!trimmed) continue;
729
+ const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
730
+ if (firstWord === "cd") {
731
+ continue;
732
+ }
733
+ if (READ_ONLY_COMMANDS.has(firstWord)) {
734
+ const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
735
+ const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
736
+ const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
737
+ const m = segNoQuotes.match(REDIRECT_TARGET_RE);
738
+ if (m) targetPaths.push(m[1] || m[2] || m[3]);
739
+ continue;
740
+ }
741
+ const segPaths = extractFilePaths(trimmed);
742
+ const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
743
+ if (SOURCE_ARG_CMDS.has(firstWord)) {
744
+ if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
745
+ } else if (firstWord === "dd") {
746
+ const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
747
+ const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
748
+ const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
749
+ const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
750
+ for (const sp of segPaths) {
751
+ if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
752
+ }
753
+ } else {
754
+ targetPaths.push(...segPathsFiltered);
755
+ }
756
+ }
757
+ const uniqueTargetPaths = [...new Set(targetPaths)];
758
+ if (uniqueTargetPaths.length === 0) return null;
498
759
  const historyReadPaths = /* @__PURE__ */ new Set();
499
760
  for (const action of actionHistory) {
500
761
  const histDetail = action.actionDetail.toLowerCase();
@@ -507,7 +768,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
507
768
  }
508
769
  }
509
770
  const unreadPaths = [];
510
- for (const path3 of targetPaths) {
771
+ for (const path3 of uniqueTargetPaths) {
511
772
  const hasRead = [...historyReadPaths].some(
512
773
  (rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
513
774
  );
@@ -570,6 +831,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
570
831
  violatedLayers: ["2"]
571
832
  };
572
833
  }
834
+ function containsPrivilegeEscalation(actionDetail) {
835
+ const ad = actionDetail.toLowerCase();
836
+ for (const seg of ad.split(/&&|\|\||;|\n/)) {
837
+ const trimmed = seg.trim();
838
+ if (!trimmed) continue;
839
+ const parts = trimmed.split(/\s+/);
840
+ const fw = parts[0]?.toLowerCase() || "";
841
+ if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
842
+ if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
843
+ }
844
+ return false;
845
+ }
573
846
  function checkIndirectFileAccess(actionType, actionDetail, cwd) {
574
847
  if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
575
848
  return null;
@@ -674,12 +947,13 @@ var init_logic_rules = __esm({
674
947
  { path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
675
948
  // 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
676
949
  { path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
677
- { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
950
+ { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
678
951
  { path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
679
952
  // SSH 主机私钥
680
- { path: "/etc/ssh/ssh_host_rsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
681
- { path: "/etc/ssh/ssh_host_ed25519_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
682
- { path: "/etc/ssh/ssh_host_ecdsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
953
+ // SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
954
+ { path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
955
+ { path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
956
+ { path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
683
957
  ];
684
958
  HOME_SENSITIVE_PATHS = [
685
959
  { path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
@@ -720,7 +994,7 @@ var init_logic_rules = __esm({
720
994
  "cat"
721
995
  ]);
722
996
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
723
- WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362"];
997
+ WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362", "edit"];
724
998
  WRITE_COMMAND_RES = [
725
999
  /\brm\b/,
726
1000
  /\bunlink\b/,
@@ -766,6 +1040,12 @@ var init_logic_rules = __esm({
766
1040
  "nl",
767
1041
  "tac",
768
1042
  "rev",
1043
+ // NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
1044
+ // read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
1045
+ // 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
1046
+ // echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
1047
+ "echo",
1048
+ "printf",
769
1049
  "lsblk",
770
1050
  "blockdev",
771
1051
  "smartctl",
@@ -818,7 +1098,7 @@ var init_logic_rules = __esm({
818
1098
  "modinfo",
819
1099
  "sysctl"
820
1100
  ]);
821
- DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/;
1101
+ DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
822
1102
  READ_KEYWORDS = ["read", "\u8BFB\u53D6", "cat", "head", "tail", "less", "more", "view", "\u67E5\u770B", "open", "\u6253\u5F00", "load", "\u52A0\u8F7D", "grep", "search", "\u641C\u7D22", "find", "\u67E5\u627E"];
823
1103
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
824
1104
  PASSWORD_MODIFY_PATTERNS = [
@@ -866,9 +1146,9 @@ var init_logic_rules = __esm({
866
1146
  });
867
1147
 
868
1148
  // src/engine/skill-loader.ts
869
- import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
1149
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
870
1150
  import { resolve, dirname } from "path";
871
- import { fileURLToPath as fileURLToPath2 } from "url";
1151
+ import { fileURLToPath as fileURLToPath3 } from "url";
872
1152
  function loadAllSkills() {
873
1153
  if (cachedSkills) return cachedSkills;
874
1154
  cachedSkills = {};
@@ -880,7 +1160,7 @@ function loadAllSkills() {
880
1160
  const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
881
1161
  for (const f of files) {
882
1162
  const name = f.replace(".md", "");
883
- cachedSkills[name] = readFileSync(resolve(dir, f), "utf-8");
1163
+ cachedSkills[name] = readFileSync2(resolve(dir, f), "utf-8");
884
1164
  }
885
1165
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedSkills).length} skills from ${dir}`);
886
1166
  return cachedSkills;
@@ -939,7 +1219,7 @@ var init_skill_loader = __esm({
939
1219
  "src/engine/skill-loader.ts"() {
940
1220
  "use strict";
941
1221
  init_esm_shims();
942
- __filename2 = fileURLToPath2(import.meta.url);
1222
+ __filename2 = fileURLToPath3(import.meta.url);
943
1223
  __dirname2 = dirname(__filename2);
944
1224
  SKILLS_DIR = resolve(__dirname2, "../skills");
945
1225
  SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
@@ -1011,11 +1291,13 @@ __export(runtime_config_exports, {
1011
1291
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1012
1292
  updateSkillEnabled: () => updateSkillEnabled
1013
1293
  });
1014
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1015
- import { join } from "path";
1016
- import { homedir } from "os";
1017
- import { fileURLToPath as fileURLToPath3 } from "url";
1294
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1295
+ import { join as join2 } from "path";
1296
+ import { fileURLToPath as fileURLToPath4 } from "url";
1018
1297
  import { dirname as dirname2 } from "path";
1298
+ function runtimeDir() {
1299
+ return getRuntimeConfigDir();
1300
+ }
1019
1301
  function getRuntimeConfigPath() {
1020
1302
  const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
1021
1303
  if (envPath) return envPath;
@@ -1048,7 +1330,7 @@ function loadRuntimeConfig() {
1048
1330
  return seeded;
1049
1331
  }
1050
1332
  try {
1051
- const data = JSON.parse(readFileSync2(p, "utf-8"));
1333
+ const data = JSON.parse(readFileSync3(p, "utf-8"));
1052
1334
  const merged = mergeConfig(data, generateSourceDefaults());
1053
1335
  return {
1054
1336
  version: data.version || 1,
@@ -1118,7 +1400,7 @@ function hasRuntimeConfig() {
1118
1400
  const p = getRuntimeConfigPath();
1119
1401
  if (!existsSync3(p)) return false;
1120
1402
  try {
1121
- JSON.parse(readFileSync2(p, "utf-8"));
1403
+ JSON.parse(readFileSync3(p, "utf-8"));
1122
1404
  return true;
1123
1405
  } catch {
1124
1406
  return false;
@@ -1306,11 +1588,11 @@ function _agentRulesToDefaults() {
1306
1588
  return out;
1307
1589
  }
1308
1590
  for (const agentId of entries) {
1309
- const rulesFile = join(agentsDir, agentId, "rules.json");
1591
+ const rulesFile = join2(agentsDir, agentId, "rules.json");
1310
1592
  if (!existsSync3(rulesFile)) continue;
1311
1593
  let data;
1312
1594
  try {
1313
- data = JSON.parse(readFileSync2(rulesFile, "utf-8"));
1595
+ data = JSON.parse(readFileSync3(rulesFile, "utf-8"));
1314
1596
  } catch {
1315
1597
  continue;
1316
1598
  }
@@ -1726,12 +2008,12 @@ function deleteCustomRule(layer, category, ruleId) {
1726
2008
  function addCustomSkill(skillId, category, keywords, content) {
1727
2009
  _validateSkillId(skillId);
1728
2010
  const runtime = loadRuntimeConfig();
1729
- const skillsDir = join(RUNTIME_CONFIG_DIR, "agent_moss_skills");
2011
+ const skillsDir = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills");
1730
2012
  try {
1731
2013
  mkdirSync(skillsDir, { recursive: true });
1732
2014
  } catch {
1733
2015
  }
1734
- writeFileSync(join(skillsDir, `${skillId}.md`), content, "utf-8");
2016
+ writeFileSync(join2(skillsDir, `${skillId}.md`), content, "utf-8");
1735
2017
  const l3 = runtime.L3_skills || (runtime.L3_skills = {});
1736
2018
  const cat = l3[category] || (l3[category] = { category_enabled: true, category_desc: "", skills: [] });
1737
2019
  (cat.skills || (cat.skills = [])).push({ id: skillId, keywords, enabled: true, builtin: false });
@@ -1746,7 +2028,7 @@ function deleteCustomSkill(skillId, category) {
1746
2028
  if (idx >= 0) {
1747
2029
  if (skills[idx].builtin) return null;
1748
2030
  skills.splice(idx, 1);
1749
- const skillPath = join(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
2031
+ const skillPath = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
1750
2032
  try {
1751
2033
  if (existsSync3(skillPath)) unlinkSync(skillPath);
1752
2034
  } catch {
@@ -1862,18 +2144,19 @@ function updateAgentCategoryEnabled(agentId, category, enabled) {
1862
2144
  saveRuntimeConfig(runtime);
1863
2145
  return runtime;
1864
2146
  }
1865
- var __dirname3, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH, ENV_RUNTIME_CONFIG_PATH, LAYER_DISABLE_ENV, DENY_MODE_LABELS, _AGENTS_DIR_PROD, _AGENTS_DIR_DEV, SKILL_ID_RE;
2147
+ var __dirname3, ENV_RUNTIME_CONFIG_PATH, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH, LAYER_DISABLE_ENV, DENY_MODE_LABELS, _AGENTS_DIR_PROD, _AGENTS_DIR_DEV, SKILL_ID_RE;
1866
2148
  var init_runtime_config = __esm({
1867
2149
  "src/infra/runtime-config.ts"() {
1868
2150
  "use strict";
1869
2151
  init_esm_shims();
2152
+ init_paths();
1870
2153
  init_heuristic();
1871
2154
  init_logic_rules();
1872
2155
  init_skill_loader();
1873
- __dirname3 = dirname2(fileURLToPath3(import.meta.url));
1874
- RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
1875
- RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
2156
+ __dirname3 = dirname2(fileURLToPath4(import.meta.url));
1876
2157
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
2158
+ RUNTIME_CONFIG_DIR = runtimeDir();
2159
+ RUNTIME_CONFIG_PATH = join2(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
1877
2160
  LAYER_DISABLE_ENV = {
1878
2161
  L1_heuristic: "AGENT_MOSS_DISABLE_HEURISTIC",
1879
2162
  L2_logic_rules: "AGENT_MOSS_DISABLE_LOGIC_RULES",
@@ -1884,16 +2167,16 @@ var init_runtime_config = __esm({
1884
2167
  deny_read: "\u4EC5\u62E6\u622A\u8BFB\u53D6",
1885
2168
  deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
1886
2169
  };
1887
- _AGENTS_DIR_PROD = join(__dirname3, "agents");
1888
- _AGENTS_DIR_DEV = join(__dirname3, "..", "..", "..", "agent_moss", "agents");
2170
+ _AGENTS_DIR_PROD = join2(__dirname3, "agents");
2171
+ _AGENTS_DIR_DEV = join2(__dirname3, "..", "..", "..", "agent_moss", "agents");
1889
2172
  SKILL_ID_RE = /^[A-Za-z0-9_-]+$/;
1890
2173
  }
1891
2174
  });
1892
2175
 
1893
2176
  // src/agents/index.ts
1894
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2177
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1895
2178
  import { resolve as resolve2, dirname as dirname3 } from "path";
1896
- import { fileURLToPath as fileURLToPath4 } from "url";
2179
+ import { fileURLToPath as fileURLToPath5 } from "url";
1897
2180
  function resolveAgentsDir() {
1898
2181
  if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
1899
2182
  if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
@@ -1918,7 +2201,7 @@ function getAgentExtensions(agentId) {
1918
2201
  return null;
1919
2202
  }
1920
2203
  try {
1921
- const raw = readFileSync3(rulesFile, "utf-8");
2204
+ const raw = readFileSync4(rulesFile, "utf-8");
1922
2205
  ext = JSON.parse(raw);
1923
2206
  if (!ext.agent_id) {
1924
2207
  console.warn(`[AgentMoss agents] rules.json \u7F3A\u5C11 agent_id \u5B57\u6BB5: ${rulesFile}`);
@@ -1942,7 +2225,7 @@ var init_agents = __esm({
1942
2225
  "src/agents/index.ts"() {
1943
2226
  "use strict";
1944
2227
  init_esm_shims();
1945
- __filename3 = fileURLToPath4(import.meta.url);
2228
+ __filename3 = fileURLToPath5(import.meta.url);
1946
2229
  __dirname4 = dirname3(__filename3);
1947
2230
  AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
1948
2231
  AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
@@ -1962,19 +2245,25 @@ function runHeuristicCheck(input) {
1962
2245
  const re = new RegExp(rule.pattern);
1963
2246
  if (re.test(command)) {
1964
2247
  if (rule.action === "Deny") {
1965
- matched.push(`\u81EA\u5B9A\u4E49\u89C4\u5219: ${rule.pattern}`);
1966
- if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1967
- highestSeverity = rule.severity;
1968
- highestRiskType = "custom_rule";
1969
- }
2248
+ return {
2249
+ allowed: false,
2250
+ reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
2251
+ riskLevel: rule.severity,
2252
+ riskType: "sensitive_action",
2253
+ confidence: 90,
2254
+ source: "heuristic",
2255
+ violatedLayers: ["1.1"]
2256
+ };
1970
2257
  }
1971
2258
  }
1972
2259
  } catch {
1973
2260
  }
1974
2261
  }
2262
+ const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
2263
+ const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
1975
2264
  let topRiskType = "";
1976
2265
  for (const rule of DANGEROUS_PATTERNS) {
1977
- if (rule.pattern.test(command)) {
2266
+ if (rule.pattern.test(scanCommand)) {
1978
2267
  matched.push(rule.description);
1979
2268
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1980
2269
  highestSeverity = rule.severity;
@@ -1988,7 +2277,7 @@ function runHeuristicCheck(input) {
1988
2277
  for (const rule of agentExt.extra_command_patterns) {
1989
2278
  try {
1990
2279
  const re = new RegExp(rule.pattern, "i");
1991
- if (re.test(command)) {
2280
+ if (re.test(scanCommand)) {
1992
2281
  matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
1993
2282
  if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
1994
2283
  highestSeverity = rule.risk_level;
@@ -2012,14 +2301,12 @@ function runHeuristicCheck(input) {
2012
2301
  const actionTypeL = input.aNext.actionType.toLowerCase();
2013
2302
  const detailL = input.aNext.actionDetail.toLowerCase();
2014
2303
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2304
+ const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2305
+ if (dangerousResult) return dangerousResult;
2015
2306
  if (!isExcludedType) {
2016
2307
  const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2017
2308
  if (pathResult) return pathResult;
2018
2309
  }
2019
- const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2020
- if (dangerousResult) return dangerousResult;
2021
- const lateralResult = checkLateralMovement(detailL);
2022
- if (lateralResult) return lateralResult;
2023
2310
  if (matched.length > 0) {
2024
2311
  return {
2025
2312
  allowed: false,
@@ -2032,6 +2319,8 @@ function runHeuristicCheck(input) {
2032
2319
  // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2033
2320
  };
2034
2321
  }
2322
+ const lateralResult = checkLateralMovement(detailL);
2323
+ if (lateralResult) return lateralResult;
2035
2324
  return null;
2036
2325
  }
2037
2326
  function checkInjectionKeywords(text, extraKeywords) {
@@ -2054,6 +2343,68 @@ function isInlineScriptCommand(actionDetail) {
2054
2343
  const re = /(?:^|\s)(?:python|python3|python3\.\d+|perl|perl5|ruby|jruby|mruby|node|php|lua)\s+(?:-[a-zA-Z]*[cer][a-zA-Z]*\s+)/i;
2055
2344
  return re.test(actionDetail);
2056
2345
  }
2346
+ function stripLiteralTextSegments(actionDetail) {
2347
+ const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
2348
+ const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
2349
+ if (writeScriptExec || pipeToInterpreter) {
2350
+ return actionDetail;
2351
+ }
2352
+ const resultParts = [];
2353
+ for (const seg of splitCommandSegments(actionDetail)) {
2354
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
2355
+ if (fw === "echo" || fw === "printf") {
2356
+ const segUnquotedRedirect = seg.replace(
2357
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
2358
+ (_m, _q, p) => ">" + p
2359
+ );
2360
+ resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
2361
+ } else {
2362
+ resultParts.push(seg);
2363
+ }
2364
+ }
2365
+ return resultParts.join("");
2366
+ }
2367
+ function splitCommandSegments(actionDetail) {
2368
+ const segments = [];
2369
+ let cur = "";
2370
+ let quote = null;
2371
+ for (let i = 0; i < actionDetail.length; i++) {
2372
+ const ch = actionDetail[i];
2373
+ if (quote) {
2374
+ cur += ch;
2375
+ if (ch === quote) quote = null;
2376
+ continue;
2377
+ }
2378
+ if (ch === "'" || ch === '"') {
2379
+ quote = ch;
2380
+ cur += ch;
2381
+ continue;
2382
+ }
2383
+ if (ch === ";" || ch === "\n") {
2384
+ segments.push(cur);
2385
+ segments.push(ch);
2386
+ cur = "";
2387
+ continue;
2388
+ }
2389
+ if (ch === "&" && actionDetail[i + 1] === "&") {
2390
+ segments.push(cur);
2391
+ segments.push("&&");
2392
+ cur = "";
2393
+ i++;
2394
+ continue;
2395
+ }
2396
+ if (ch === "|" && actionDetail[i + 1] === "|") {
2397
+ segments.push(cur);
2398
+ segments.push("||");
2399
+ cur = "";
2400
+ i++;
2401
+ continue;
2402
+ }
2403
+ cur += ch;
2404
+ }
2405
+ if (cur) segments.push(cur);
2406
+ return segments;
2407
+ }
2057
2408
  function severityRank(s) {
2058
2409
  const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
2059
2410
  return ranks[s] || 0;
@@ -2061,13 +2412,18 @@ function severityRank(s) {
2061
2412
  function checkSensitivePath(command, cwd, agentExt) {
2062
2413
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2063
2414
  const isWriteOpFlag = isWriteOperation("", command);
2415
+ const pathScanText = stripLiteralTextSegments(command);
2064
2416
  const pathMatches = (text, path3) => {
2065
- const escaped = path3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2417
+ const parts = path3.split(/(\(\?!.*?\))/);
2418
+ const escaped = parts.map((part) => {
2419
+ if (part.startsWith("(?!")) return part;
2420
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2421
+ }).join("");
2066
2422
  const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
2067
2423
  return re.test(text);
2068
2424
  };
2069
2425
  for (const sp of ROOT_SENSITIVE_PATHS) {
2070
- if (pathMatches(command, sp.path.toLowerCase())) {
2426
+ if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2071
2427
  if (sp.path === "/dev/random") {
2072
2428
  if (isWriteOpFlag) {
2073
2429
  continue;
@@ -2101,7 +2457,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2101
2457
  const expanded = `${expandedCwd}/${sp.path}`;
2102
2458
  const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2103
2459
  const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2104
- if (pathRe.test(command) || expandedRe.test(command)) {
2460
+ if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2105
2461
  if (!isWriteOpFlag && !sp.credential) {
2106
2462
  continue;
2107
2463
  }
@@ -2172,8 +2528,10 @@ function checkSensitivePath(command, cwd, agentExt) {
2172
2528
  }
2173
2529
  function checkDangerousPatterns(actionType, actionDetail) {
2174
2530
  if (actionType === "file_write" || actionType === "file_edit") return null;
2175
- const TEMP_DIRS = ["/tmp/", "/var/tmp/", "/run/", "/dev/shm/"];
2176
- const isTempPath = TEMP_DIRS.some((td) => actionDetail.includes(td));
2531
+ const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2532
+ const isTempPath = TEMP_DIRS.some(
2533
+ (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
2534
+ );
2177
2535
  if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
2178
2536
  return {
2179
2537
  allowed: false,
@@ -2185,7 +2543,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
2185
2543
  violatedLayers: ["1.1"]
2186
2544
  };
2187
2545
  }
2188
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail) || />\s*\/proc\//.test(actionDetail)) {
2546
+ const findWriteMatch = actionDetail.match(
2547
+ /\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
2548
+ );
2549
+ if (findWriteMatch) {
2550
+ const cmdHit = findWriteMatch[1] || "xargs";
2551
+ return {
2552
+ allowed: false,
2553
+ reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
2554
+ riskLevel: "high",
2555
+ riskType: "script_execution",
2556
+ confidence: 90,
2557
+ source: "heuristic",
2558
+ violatedLayers: ["1.1"]
2559
+ };
2560
+ }
2561
+ if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
2189
2562
  return {
2190
2563
  allowed: false,
2191
2564
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -2196,17 +2569,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
2196
2569
  violatedLayers: ["1.1"]
2197
2570
  };
2198
2571
  }
2572
+ if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
2573
+ return {
2574
+ allowed: false,
2575
+ reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
2576
+ riskLevel: "high",
2577
+ riskType: "file_access",
2578
+ confidence: 90,
2579
+ source: "heuristic",
2580
+ violatedLayers: ["1.1"]
2581
+ };
2582
+ }
2199
2583
  return null;
2200
2584
  }
2201
2585
  function extractRemoteHost(command) {
2202
- const m = command.match(/(?:ssh|scp|rsync)\s+(?:-[\w]+\s+)*(\S+@\S+|\b(?!-)[\w.-]+\b)/);
2203
- return m ? m[1] : null;
2586
+ const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
2587
+ for (const tok of tokens) {
2588
+ const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
2589
+ if (am && am[2] && !isLocalHost(am[2])) {
2590
+ return `${am[1]}@${am[2]}`;
2591
+ }
2592
+ }
2593
+ for (const tok of tokens) {
2594
+ const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
2595
+ if (hm && hm[1] && !isLocalHost(hm[1])) {
2596
+ return hm[1];
2597
+ }
2598
+ }
2599
+ const skipValueOpts = /* @__PURE__ */ new Set([
2600
+ "-p",
2601
+ "-l",
2602
+ "-i",
2603
+ "-o",
2604
+ "-b",
2605
+ "-c",
2606
+ "-m",
2607
+ "-F",
2608
+ "-e",
2609
+ "-S",
2610
+ "-W",
2611
+ "-J",
2612
+ "-I",
2613
+ "-T",
2614
+ "-B",
2615
+ "-L",
2616
+ "-R",
2617
+ "-D",
2618
+ "-E",
2619
+ "-M",
2620
+ "-O",
2621
+ "-P",
2622
+ "-q"
2623
+ ]);
2624
+ let foundCmd = false;
2625
+ for (let i = 0; i < tokens.length; i++) {
2626
+ const t = tokens[i];
2627
+ if (t === "ssh" || t === "scp" || t === "rsync") {
2628
+ foundCmd = true;
2629
+ continue;
2630
+ }
2631
+ if (!foundCmd) continue;
2632
+ if (t.startsWith("-")) {
2633
+ if (skipValueOpts.has(t) && i + 1 < tokens.length) {
2634
+ i++;
2635
+ }
2636
+ continue;
2637
+ }
2638
+ if (t.startsWith("/")) continue;
2639
+ return t;
2640
+ }
2641
+ return null;
2204
2642
  }
2205
2643
  function isLocalHost(host) {
2206
2644
  const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
2207
2645
  return localNames.includes(host.replace(/.*@/, ""));
2208
2646
  }
2209
2647
  function checkLateralMovement(actionDetail) {
2648
+ if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
2649
+ const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
2650
+ let hasRealCall = false;
2651
+ for (const seg of segments) {
2652
+ const parts = seg.split(/\s+/);
2653
+ const fw = (parts[0] || "").toLowerCase();
2654
+ if (fw === "echo" || fw === "printf") continue;
2655
+ if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
2656
+ hasRealCall = true;
2657
+ break;
2658
+ }
2659
+ }
2660
+ if (!hasRealCall) return null;
2210
2661
  if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
2211
2662
  const targetHost = extractRemoteHost(actionDetail);
2212
2663
  if (targetHost && !isLocalHost(targetHost)) {
@@ -2306,10 +2757,14 @@ var init_heuristic = __esm({
2306
2757
  { pattern: /mkfs\.\w+\s+\/dev\/(sd|hd|nvme|vd|loop|mapper|md|dm)\S+/i, severity: "critical", description: "\u683C\u5F0F\u5316\u5757\u8BBE\u5907 (mkfs /dev/\u5757\u8BBE\u5907)", riskType: "script_execution" },
2307
2758
  { pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
2308
2759
  // --- Critical: remote code execution ---
2309
- { pattern: /curl\s+.*\|\s*(bash|sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2760
+ // curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
2761
+ // 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
2762
+ // runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
2763
+ { pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
2764
+ { pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2310
2765
  { pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
2311
2766
  { pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
2312
- { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "critical", description: "base64 \u89E3\u7801\u6267\u884C", riskType: "script_execution" },
2767
+ { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
2313
2768
  // --- Critical: reverse shell ---
2314
2769
  { pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
2315
2770
  { pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
@@ -2320,13 +2775,25 @@ var init_heuristic = __esm({
2320
2775
  { pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
2321
2776
  // --- Critical: privilege escalation / container escape ---
2322
2777
  { pattern: /\/var\/run\/docker\.sock/i, severity: "critical", description: "\u8BBF\u95EE Docker socket\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5BB9\u5668\u9003\u9038", riskType: "privilege_escalation" },
2323
- // --- High: dangerous permissions / ownership ---
2324
- { pattern: /chmod\s+[0-7]*777[0-7]*/i, severity: "high", description: "\u5168\u6743\u9650\u8BBE\u7F6E (chmod 777)", riskType: "script_execution" },
2778
+ // --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
2779
+ // 修正「只拦 777、xx7 漏放」的缺口:
2780
+ // 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
2781
+ // 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
2782
+ // 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
2783
+ // 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
2784
+ // 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
2785
+ { 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" },
2786
+ { 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" },
2787
+ { pattern: /chmod\s+[ugoa]\+s\b/i, severity: "critical", description: "\u7B26\u53F7 setuid/setgid \u8BBE\u7F6E (chmod u+s/g+s/a+s, \u63D0\u6743)", riskType: "privilege_escalation" },
2325
2788
  { pattern: /chown\s+root|chown\s+-R\s+\w+:\w+\s+\//i, severity: "high", description: "\u4FEE\u6539\u6240\u6709\u8005\u4E3A root / \u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6240\u6709\u8005", riskType: "script_execution" },
2326
2789
  // sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
2327
2790
  { pattern: /sudo\s+(rm|chmod|chown|dd|mkfs|format|iptables|crontab|passwd|userdel|useradd|groupdel)\b/i, severity: "high", description: "\u63D0\u6743\u6267\u884C\u9AD8\u5371\u547D\u4EE4 (sudo + \u5371\u9669\u64CD\u4F5C)", riskType: "privilege_escalation" },
2328
2791
  // --- High: system config overwrite ---
2792
+ // > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
2793
+ // 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
2329
2794
  { pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
2795
+ { pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
2796
+ // 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
2330
2797
  // --- High: download script + execute risk ---
2331
2798
  { pattern: /(?:curl|wget)\s+.*(?:-o|--output|>)\s*\S+\.(?:sh|bash)\b/i, severity: "high", description: "\u8FDC\u7A0B\u4E0B\u8F7D\u811A\u672C\u5230\u672C\u5730", riskType: "script_execution" },
2332
2799
  { pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
@@ -2342,7 +2809,11 @@ var init_heuristic = __esm({
2342
2809
  // --- Medium: credential export ---
2343
2810
  { pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
2344
2811
  // --- High: shell rc file append (persistence) ---
2812
+ // 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
2813
+ // 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
2814
+ // /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
2345
2815
  { 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" },
2816
+ { pattern: />>\s*\S*\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
2346
2817
  // --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
2347
2818
  { pattern: />\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i, severity: "high", description: "\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6", riskType: "file_access" },
2348
2819
  // --- High: non-interactive password modification ---
@@ -2378,17 +2849,16 @@ __export(token_stats_exports, {
2378
2849
  recordTokenUsage: () => recordTokenUsage,
2379
2850
  resetTokenStats: () => resetTokenStats
2380
2851
  });
2381
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
2382
- import { join as join4 } from "path";
2383
- import { homedir as homedir3 } from "os";
2852
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
2384
2853
  function getStatsPath() {
2385
2854
  const envPath = process.env[ENV_STATS_PATH];
2386
- return envPath || STATS_FILE;
2855
+ if (envPath) return envPath;
2856
+ return getTokenStatsPath();
2387
2857
  }
2388
2858
  function loadAllRecords(statsPath) {
2389
2859
  if (!existsSync7(statsPath)) return [];
2390
2860
  try {
2391
- const data = JSON.parse(readFileSync7(statsPath, "utf-8"));
2861
+ const data = JSON.parse(readFileSync8(statsPath, "utf-8"));
2392
2862
  if (Array.isArray(data)) return data;
2393
2863
  if (data && Array.isArray(data.records)) return data.records;
2394
2864
  return [];
@@ -2528,13 +2998,12 @@ function resetTokenStats() {
2528
2998
  }
2529
2999
  }
2530
3000
  }
2531
- var STATS_DIR, STATS_FILE, ENV_STATS_PATH, MAX_RECORDS;
3001
+ var ENV_STATS_PATH, MAX_RECORDS;
2532
3002
  var init_token_stats = __esm({
2533
3003
  "src/engine/token-stats.ts"() {
2534
3004
  "use strict";
2535
3005
  init_esm_shims();
2536
- STATS_DIR = join4(homedir3(), ".config", "agentmoss");
2537
- STATS_FILE = join4(STATS_DIR, "agent_moss_token_stats.json");
3006
+ init_paths();
2538
3007
  ENV_STATS_PATH = "AGENT_MOSS_TOKEN_STATS_PATH";
2539
3008
  MAX_RECORDS = 1e4;
2540
3009
  }
@@ -2553,10 +3022,10 @@ init_skill_loader();
2553
3022
 
2554
3023
  // src/engine/template-loader.ts
2555
3024
  init_esm_shims();
2556
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
3025
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
2557
3026
  import { resolve as resolve3, dirname as dirname4 } from "path";
2558
- import { fileURLToPath as fileURLToPath5 } from "url";
2559
- var __filename4 = fileURLToPath5(import.meta.url);
3027
+ import { fileURLToPath as fileURLToPath6 } from "url";
3028
+ var __filename4 = fileURLToPath6(import.meta.url);
2560
3029
  var __dirname5 = dirname4(__filename4);
2561
3030
  var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
2562
3031
  var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
@@ -2574,7 +3043,7 @@ function loadTemplates() {
2574
3043
  const filePath = resolve3(dir, file);
2575
3044
  if (existsSync5(filePath)) {
2576
3045
  const key = file.replace(".txt", "").replace("_template", "");
2577
- cachedTemplates[key] = readFileSync4(filePath, "utf-8");
3046
+ cachedTemplates[key] = readFileSync5(filePath, "utf-8");
2578
3047
  }
2579
3048
  }
2580
3049
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedTemplates).length} templates from ${dir}`);
@@ -2583,8 +3052,8 @@ function loadTemplates() {
2583
3052
 
2584
3053
  // src/engine/script-analyzer.ts
2585
3054
  init_esm_shims();
2586
- import { readFileSync as readFileSync5 } from "fs";
2587
- import { resolve as resolve4, join as join2 } from "path";
3055
+ import { readFileSync as readFileSync6 } from "fs";
3056
+ import { resolve as resolve4, join as join3 } from "path";
2588
3057
  var SUSPICIOUS_PATTERNS = [
2589
3058
  // Network exfiltration (curl/wget POST in scripts is almost never legitimate)
2590
3059
  { name: "curl POST", pattern: /curl\s+(.*\s)?-X\s+POST/i },
@@ -2717,8 +3186,8 @@ function assessKeywordCombinationRisk(matchedKeywords, scriptContent) {
2717
3186
  }
2718
3187
  function readScriptContent(scriptPath, maxLines = 500) {
2719
3188
  try {
2720
- const expandedPath = scriptPath.startsWith("~") ? join2(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
2721
- const raw = readFileSync5(expandedPath, "utf-8");
3189
+ const expandedPath = scriptPath.startsWith("~") ? join3(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
3190
+ const raw = readFileSync6(expandedPath, "utf-8");
2722
3191
  const lines = raw.split("\n").slice(0, maxLines);
2723
3192
  return { content: lines.join("\n"), lineCount: lines.length, error: "" };
2724
3193
  } catch (e) {
@@ -2772,8 +3241,8 @@ ${result.scriptContent}
2772
3241
  init_esm_shims();
2773
3242
  init_heuristic();
2774
3243
  init_logic_rules();
2775
- import { readFileSync as readFileSync6, statSync, existsSync as existsSync6 } from "fs";
2776
- import { resolve as resolve5, join as join3, isAbsolute, dirname as dirname5 } from "path";
3244
+ import { readFileSync as readFileSync7, statSync, existsSync as existsSync6 } from "fs";
3245
+ import { resolve as resolve5, join as join4, isAbsolute, dirname as dirname5 } from "path";
2777
3246
  import { homedir as homedir2 } from "os";
2778
3247
  function emptyChain() {
2779
3248
  return {
@@ -3014,7 +3483,7 @@ function extractInlineScriptPath(actionDetail) {
3014
3483
  function resolvePath(p) {
3015
3484
  let expanded = p;
3016
3485
  if (expanded.startsWith("~/") || expanded === "~") {
3017
- expanded = join3(homedir2(), expanded.slice(1));
3486
+ expanded = join4(homedir2(), expanded.slice(1));
3018
3487
  }
3019
3488
  return resolve5(expanded);
3020
3489
  }
@@ -3027,7 +3496,7 @@ function readScriptContent2(scriptPath, maxFileSize = 2 * 1024 * 1024) {
3027
3496
  if (stat.size > maxFileSize) {
3028
3497
  return { content: "", lineCount: 0, fileSize: stat.size, error: `\u811A\u672C\u6587\u4EF6\u8FC7\u5927 (${stat.size} bytes)\uFF0C\u8DF3\u8FC7\u8BFB\u53D6` };
3029
3498
  }
3030
- const content = readFileSync6(path3, "utf-8");
3499
+ const content = readFileSync7(path3, "utf-8");
3031
3500
  const lineCount = content ? content.split("\n").length : 0;
3032
3501
  return { content, lineCount, fileSize: stat.size, error: "" };
3033
3502
  } catch (e) {
@@ -3044,7 +3513,7 @@ function extractChildScriptPaths(content, baseDir) {
3044
3513
  const pathStr = m[1];
3045
3514
  if (pathStr.startsWith("-") || pathStr.startsWith("$")) continue;
3046
3515
  if (pathStr.includes("/dev/") || pathStr.startsWith("|") || pathStr.startsWith(";")) continue;
3047
- const full = isAbsolute(pathStr) ? pathStr : join3(baseDir, pathStr);
3516
+ const full = isAbsolute(pathStr) ? pathStr : join4(baseDir, pathStr);
3048
3517
  const resolved = resolvePath(full);
3049
3518
  if (existsSync6(resolved)) {
3050
3519
  try {
@@ -3471,11 +3940,13 @@ async function analyzeWithLLM(prompt, config) {
3471
3940
  } catch {
3472
3941
  }
3473
3942
  clearTimeout(totalTimeoutTimer);
3943
+ const riskLevel = parsed.riskLevel ?? parsed.risk_level;
3944
+ const riskType = parsed.riskType ?? parsed.risk_type;
3474
3945
  return {
3475
3946
  allowed: parsed.allowed !== false,
3476
3947
  reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
3477
- riskLevel: ["low", "medium", "high", "critical"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",
3478
- riskType: parsed.riskType || "",
3948
+ riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
3949
+ riskType: riskType || "",
3479
3950
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
3480
3951
  source: "llm",
3481
3952
  violatedLayers: ["3"]
@@ -3593,7 +4064,10 @@ async function runAnalysis(input) {
3593
4064
  if (heuristicResult && !heuristicResult.allowed) {
3594
4065
  const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
3595
4066
  if (!skipInlineFileAccess) {
3596
- return heuristicResult;
4067
+ const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
4068
+ if (shouldDenyL1) {
4069
+ return heuristicResult;
4070
+ }
3597
4071
  }
3598
4072
  }
3599
4073
  let scriptChain = null;
@@ -3671,7 +4145,8 @@ async function runAnalysis(input) {
3671
4145
  ) && DANGEROUS_PATTERNS.some(
3672
4146
  (p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
3673
4147
  );
3674
- if (!fullCmdHit) {
4148
+ const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
4149
+ if (!fullCmdHit && !hasRedirectWrite) {
3675
4150
  return {
3676
4151
  allowed: true,
3677
4152
  reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,