@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.
package/dist/cli.js CHANGED
@@ -43,6 +43,90 @@ var init_esm_shims = __esm({
43
43
  }
44
44
  });
45
45
 
46
+ // src/infra/paths.ts
47
+ import { join } from "path";
48
+ import { homedir } from "os";
49
+ import { readFileSync } from "fs";
50
+ function isRoot() {
51
+ const getuid = process.getuid;
52
+ return typeof getuid === "function" && getuid() === 0;
53
+ }
54
+ function resolveOwnerHome() {
55
+ for (const name of [process.env.SUDO_USER, process.env.LOGNAME, process.env.USER]) {
56
+ if (name && name !== "root" && name !== "") {
57
+ const home = homeOf(name);
58
+ if (home) return home;
59
+ }
60
+ }
61
+ try {
62
+ const passwd = readFileSync("/etc/passwd", "utf-8");
63
+ for (const line of passwd.split("\n")) {
64
+ if (!line || line.startsWith("#")) continue;
65
+ const parts = line.split(":");
66
+ if (parts.length < 6) continue;
67
+ const uid = parseInt(parts[2], 10);
68
+ const name = parts[0];
69
+ if (uid >= 1e3 && name && !name.startsWith("_")) {
70
+ return parts[5];
71
+ }
72
+ }
73
+ } catch {
74
+ }
75
+ return null;
76
+ }
77
+ function homeOf(name) {
78
+ try {
79
+ const passwd = readFileSync("/etc/passwd", "utf-8");
80
+ for (const line of passwd.split("\n")) {
81
+ if (!line || line.startsWith("#")) continue;
82
+ const parts = line.split(":");
83
+ if (parts.length >= 6 && parts[0] === name) {
84
+ return parts[5];
85
+ }
86
+ }
87
+ } catch {
88
+ }
89
+ return null;
90
+ }
91
+ function getDataRoot() {
92
+ const envPath = process.env[ENV_DATA_ROOT];
93
+ if (envPath) return envPath;
94
+ if (isRoot()) {
95
+ const ownerHome = resolveOwnerHome();
96
+ if (ownerHome) {
97
+ return join(ownerHome, ".config", "agentmoss");
98
+ }
99
+ }
100
+ return DEFAULT_DATA_ROOT;
101
+ }
102
+ function getInstance() {
103
+ const v = process.env[ENV_INSTANCE] || "";
104
+ return v.trim().replace(/^[/\\]+|[/\\]+$/g, "");
105
+ }
106
+ function getConfigDir() {
107
+ return getDataRoot();
108
+ }
109
+ function getRuntimeConfigDir() {
110
+ return getDataRoot();
111
+ }
112
+ function getTokenStatsPath() {
113
+ const instance = getInstance();
114
+ if (instance) {
115
+ return join(getDataRoot(), instance, "agent_moss_token_stats.json");
116
+ }
117
+ return join(getDataRoot(), "agent_moss_token_stats.json");
118
+ }
119
+ var ENV_DATA_ROOT, ENV_INSTANCE, DEFAULT_DATA_ROOT;
120
+ var init_paths = __esm({
121
+ "src/infra/paths.ts"() {
122
+ "use strict";
123
+ init_esm_shims();
124
+ ENV_DATA_ROOT = "AGENT_MOSS_DATA_DIR";
125
+ ENV_INSTANCE = "AGENT_MOSS_INSTANCE";
126
+ DEFAULT_DATA_ROOT = join(homedir(), ".config", "agentmoss");
127
+ }
128
+ });
129
+
46
130
  // src/config.ts
47
131
  var config_exports = {};
48
132
  __export(config_exports, {
@@ -56,11 +140,12 @@ __export(config_exports, {
56
140
  isHeuristicEnabled: () => isHeuristicEnabled,
57
141
  isLLMAvailable: () => isLLMAvailable,
58
142
  isLogicRulesEnabled: () => isLogicRulesEnabled,
143
+ probeLLMConnection: () => probeLLMConnection,
59
144
  updateLLMConfig: () => updateLLMConfig
60
145
  });
61
146
  import fs from "fs";
62
147
  import path2 from "path";
63
- import os from "os";
148
+ import { fileURLToPath as fileURLToPath2 } from "url";
64
149
  function loadConfigFile(filePath) {
65
150
  try {
66
151
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -72,7 +157,7 @@ function loadConfigFile(filePath) {
72
157
  function resolveConfigPath() {
73
158
  const envPath = process.env.AGENT_MOSS_CONFIG_PATH;
74
159
  if (envPath) return envPath;
75
- const userConfig = path2.join(os.homedir(), ".config", "agentmoss", "config.json");
160
+ const userConfig = path2.join(getConfigDir(), "config.json");
76
161
  if (fs.existsSync(userConfig)) return userConfig;
77
162
  const defaultConfig = path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example");
78
163
  if (fs.existsSync(defaultConfig)) {
@@ -123,16 +208,38 @@ function getTimeoutConfig() {
123
208
  function getRetryConfig() {
124
209
  return retryConfig;
125
210
  }
211
+ function wildcardToRegExp(pattern) {
212
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
213
+ }
126
214
  function loadCustomRules() {
215
+ const rules = [];
127
216
  try {
128
217
  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);
218
+ if (raw) {
219
+ const parsed = JSON.parse(raw);
220
+ if (Array.isArray(parsed)) {
221
+ for (const r of parsed) {
222
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
223
+ }
224
+ }
225
+ }
133
226
  } catch {
134
- return [];
135
227
  }
228
+ for (const rulesPath of USER_RULES_PATHS) {
229
+ try {
230
+ if (!fs.existsSync(rulesPath)) continue;
231
+ const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
232
+ const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
233
+ for (const r of actions) {
234
+ const p = r?.pattern;
235
+ if (!p) continue;
236
+ rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
237
+ }
238
+ break;
239
+ } catch {
240
+ }
241
+ }
242
+ return rules;
136
243
  }
137
244
  function getLLMConfig() {
138
245
  return llmConfig;
@@ -147,6 +254,30 @@ function isLLMAvailable() {
147
254
  if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
148
255
  return llmConfig.apiKey.length > 0;
149
256
  }
257
+ async function probeLLMConnection(timeoutMs = 8e3) {
258
+ if (!isLLMAvailable()) return false;
259
+ const OpenAI2 = (await import("openai")).default;
260
+ const config = getLLMConfig();
261
+ const provider = detectProvider(config.baseUrl, config.provider);
262
+ const extraHeaders = getProviderHeaders(provider);
263
+ const client = new OpenAI2({
264
+ baseURL: config.baseUrl,
265
+ apiKey: config.apiKey,
266
+ timeout: timeoutMs,
267
+ maxRetries: 0,
268
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
269
+ });
270
+ try {
271
+ const resp = await client.chat.completions.create({
272
+ model: config.model,
273
+ messages: [{ role: "user", content: "ping" }],
274
+ max_tokens: 1
275
+ });
276
+ return !!(resp && resp.choices && resp.choices[0]);
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
150
281
  function getLLMFailMode() {
151
282
  const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
152
283
  return val === "fail_closed" ? "fail_closed" : "fail_open";
@@ -190,11 +321,12 @@ function isHeuristicEnabled() {
190
321
  function isLogicRulesEnabled() {
191
322
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
192
323
  }
193
- var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
324
+ var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
194
325
  var init_config = __esm({
195
326
  "src/config.ts"() {
196
327
  "use strict";
197
328
  init_esm_shims();
329
+ init_paths();
198
330
  llmConfig = (() => {
199
331
  const configPath = resolveConfigPath();
200
332
  const fileConfig = fs.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
@@ -212,6 +344,21 @@ var init_config = __esm({
212
344
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
213
345
  };
214
346
  })();
347
+ __configModuleDir = (() => {
348
+ try {
349
+ return path2.dirname(fileURLToPath2(import.meta.url));
350
+ } catch {
351
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
352
+ }
353
+ })();
354
+ USER_RULES_PATHS = [
355
+ path2.join(__configModuleDir, "rules", "user_rules.json"),
356
+ // 源在 ts/src/rules(一般不存在)
357
+ path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
358
+ // dist: dist/engine/../rules = dist/rules ✅
359
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
360
+ // dev src: ts/src/../../agent_moss
361
+ ];
215
362
  customRules = loadCustomRules();
216
363
  PROVIDER_HEADERS = {
217
364
  "openrouter": {
@@ -228,7 +375,17 @@ var init_config = __esm({
228
375
  // src/engine/inline-analyzer.ts
229
376
  function isLiteralTextCommand(command) {
230
377
  ECHO_CMD_RE.lastIndex = 0;
231
- return ECHO_CMD_RE.test(command);
378
+ if (!ECHO_CMD_RE.test(command)) return false;
379
+ if (command.includes(">")) {
380
+ let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
381
+ d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
382
+ d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
383
+ d = d.replace(/'[^']*'|"[^"]*"/g, "");
384
+ if (/>{1,2}\s*(?!&)\S/.test(d)) {
385
+ return false;
386
+ }
387
+ }
388
+ return true;
232
389
  }
233
390
  function extractInlineCode(command) {
234
391
  const parts = [];
@@ -305,7 +462,7 @@ function analyzeInlineScript(command) {
305
462
  riskType: "file_access"
306
463
  };
307
464
  }
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;
465
+ 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
466
  var init_inline_analyzer = __esm({
310
467
  "src/engine/inline-analyzer.ts"() {
311
468
  "use strict";
@@ -375,6 +532,13 @@ var init_inline_analyzer = __esm({
375
532
  // xargs bash / xargs sh
376
533
  /\|\s*xargs\s+(?:bash|sh|zsh)/gi
377
534
  ];
535
+ SHELL_RC_FRAGMENTS = [
536
+ ".bashrc",
537
+ ".zshrc",
538
+ ".profile",
539
+ ".bash_profile",
540
+ ".zprofile"
541
+ ];
378
542
  SENSITIVE_PATH_FRAGMENTS = [
379
543
  "/etc/shadow",
380
544
  "/etc/gshadow",
@@ -391,9 +555,10 @@ var init_inline_analyzer = __esm({
391
555
  "/dev/kmsg",
392
556
  ".ssh/id_rsa",
393
557
  ".ssh/id_ed25519",
394
- ".ssh/authorized_keys"
558
+ ".ssh/authorized_keys",
559
+ ...SHELL_RC_FRAGMENTS
395
560
  ];
396
- REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
561
+ REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
397
562
  DIR_TRAVERSAL_KEYWORDS = [
398
563
  // Python 目录遍历
399
564
  "os.listdir",
@@ -416,11 +581,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
416
581
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
417
582
  if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
418
583
  if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
584
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
585
+ const trimmed = seg.trim();
586
+ if (!trimmed) continue;
587
+ const parts = trimmed.split(/\s+/);
588
+ const fw = parts[0]?.toLowerCase() || "";
589
+ if (fw === "wget" || fw === "curl") {
590
+ const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
591
+ if (isStdout) continue;
592
+ 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);
593
+ if (isDevnull) continue;
594
+ if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
595
+ return true;
596
+ }
597
+ }
598
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
599
+ return true;
600
+ }
601
+ }
419
602
  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;
603
+ const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
604
+ const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
605
+ const detailUnquotedRedirect = detailWithoutDevnull.replace(
606
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
607
+ (_m, _q, p) => ">" + p
608
+ );
609
+ const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
610
+ if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
611
+ return true;
424
612
  }
425
613
  }
426
614
  return false;
@@ -444,27 +632,31 @@ function runLogicCheck(input) {
444
632
  if (rbwResult) return rbwResult;
445
633
  if (input.promptSession && input.promptSession.trim()) {
446
634
  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
- };
635
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
636
+ } else {
637
+ const promptLower = input.promptSession.toLowerCase();
638
+ for (const pattern of INTENT_DEVIATION_PATTERNS) {
639
+ const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
640
+ const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
641
+ if (intentMatched && dangerousMatched) {
642
+ return {
643
+ allowed: false,
644
+ reason: pattern.reason,
645
+ riskLevel: "high",
646
+ riskType: "intent_deviation",
647
+ confidence: 85,
648
+ source: "logic_rule",
649
+ violatedLayers: ["2"]
650
+ };
651
+ }
461
652
  }
462
653
  }
463
654
  }
464
655
  }
465
656
  const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
466
657
  if (indirectResult) return indirectResult;
467
- if (actionDetail.includes("sudo") || actionDetail.includes("su -") || actionDetail.includes("chmod u+s") || actionDetail.includes("setuid")) {
658
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
659
+ } else if (containsPrivilegeEscalation(actionDetail)) {
468
660
  const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
469
661
  const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
470
662
  if (!isAdminCmd) {
@@ -494,6 +686,9 @@ function extractFilePaths(text) {
494
686
  const paths = [];
495
687
  const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
496
688
  if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
689
+ const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
690
+ const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
691
+ if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
497
692
  const quotedPaths = text.match(/["']\/([^"']+)["']/g);
498
693
  if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
499
694
  return [...new Set(paths)];
@@ -504,8 +699,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
504
699
  const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
505
700
  const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
506
701
  if (!isWrite) return null;
507
- const targetPaths = extractFilePaths(actionDetail);
508
- if (targetPaths.length === 0) return null;
702
+ const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
703
+ if (isBashLike) {
704
+ const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
705
+ const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
706
+ const detailUnquotedRedirect = detailNoDevnull.replace(
707
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
708
+ (_m, _q, p) => ">" + p
709
+ );
710
+ const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
711
+ const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
712
+ let hasWriteSeg = hasRedirectWrite;
713
+ if (!hasWriteSeg) {
714
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
715
+ const trimmed = seg.trim();
716
+ if (!trimmed) continue;
717
+ const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
718
+ if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
719
+ hasWriteSeg = true;
720
+ break;
721
+ }
722
+ if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
723
+ hasWriteSeg = true;
724
+ break;
725
+ }
726
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
727
+ hasWriteSeg = true;
728
+ break;
729
+ }
730
+ }
731
+ }
732
+ if (!hasWriteSeg) return null;
733
+ }
734
+ const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
735
+ const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
736
+ const targetPaths = [];
737
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
738
+ const trimmed = seg.trim();
739
+ if (!trimmed) continue;
740
+ const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
741
+ if (firstWord === "cd") {
742
+ continue;
743
+ }
744
+ if (READ_ONLY_COMMANDS.has(firstWord)) {
745
+ const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
746
+ const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
747
+ const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
748
+ const m = segNoQuotes.match(REDIRECT_TARGET_RE);
749
+ if (m) targetPaths.push(m[1] || m[2] || m[3]);
750
+ continue;
751
+ }
752
+ const segPaths = extractFilePaths(trimmed);
753
+ const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
754
+ if (SOURCE_ARG_CMDS.has(firstWord)) {
755
+ if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
756
+ } else if (firstWord === "dd") {
757
+ const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
758
+ const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
759
+ const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
760
+ const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
761
+ for (const sp of segPaths) {
762
+ if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
763
+ }
764
+ } else {
765
+ targetPaths.push(...segPathsFiltered);
766
+ }
767
+ }
768
+ const uniqueTargetPaths = [...new Set(targetPaths)];
769
+ if (uniqueTargetPaths.length === 0) return null;
509
770
  const historyReadPaths = /* @__PURE__ */ new Set();
510
771
  for (const action of actionHistory) {
511
772
  const histDetail = action.actionDetail.toLowerCase();
@@ -518,7 +779,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
518
779
  }
519
780
  }
520
781
  const unreadPaths = [];
521
- for (const path3 of targetPaths) {
782
+ for (const path3 of uniqueTargetPaths) {
522
783
  const hasRead = [...historyReadPaths].some(
523
784
  (rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
524
785
  );
@@ -581,6 +842,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
581
842
  violatedLayers: ["2"]
582
843
  };
583
844
  }
845
+ function containsPrivilegeEscalation(actionDetail) {
846
+ const ad = actionDetail.toLowerCase();
847
+ for (const seg of ad.split(/&&|\|\||;|\n/)) {
848
+ const trimmed = seg.trim();
849
+ if (!trimmed) continue;
850
+ const parts = trimmed.split(/\s+/);
851
+ const fw = parts[0]?.toLowerCase() || "";
852
+ if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
853
+ if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
854
+ }
855
+ return false;
856
+ }
584
857
  function checkIndirectFileAccess(actionType, actionDetail, cwd) {
585
858
  if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
586
859
  return null;
@@ -685,12 +958,13 @@ var init_logic_rules = __esm({
685
958
  { path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
686
959
  // 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
687
960
  { 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" },
961
+ { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
689
962
  { path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
690
963
  // 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 }
964
+ // SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
965
+ { path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
966
+ { path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
967
+ { path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
694
968
  ];
695
969
  HOME_SENSITIVE_PATHS = [
696
970
  { path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
@@ -731,7 +1005,7 @@ var init_logic_rules = __esm({
731
1005
  "cat"
732
1006
  ]);
733
1007
  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"];
1008
+ 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
1009
  WRITE_COMMAND_RES = [
736
1010
  /\brm\b/,
737
1011
  /\bunlink\b/,
@@ -777,6 +1051,12 @@ var init_logic_rules = __esm({
777
1051
  "nl",
778
1052
  "tac",
779
1053
  "rev",
1054
+ // NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
1055
+ // read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
1056
+ // 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
1057
+ // echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
1058
+ "echo",
1059
+ "printf",
780
1060
  "lsblk",
781
1061
  "blockdev",
782
1062
  "smartctl",
@@ -829,7 +1109,7 @@ var init_logic_rules = __esm({
829
1109
  "modinfo",
830
1110
  "sysctl"
831
1111
  ]);
832
- DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/;
1112
+ DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
833
1113
  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
1114
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
835
1115
  PASSWORD_MODIFY_PATTERNS = [
@@ -877,9 +1157,9 @@ var init_logic_rules = __esm({
877
1157
  });
878
1158
 
879
1159
  // src/engine/skill-loader.ts
880
- import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
1160
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
881
1161
  import { resolve, dirname } from "path";
882
- import { fileURLToPath as fileURLToPath2 } from "url";
1162
+ import { fileURLToPath as fileURLToPath3 } from "url";
883
1163
  function loadAllSkills() {
884
1164
  if (cachedSkills) return cachedSkills;
885
1165
  cachedSkills = {};
@@ -891,7 +1171,7 @@ function loadAllSkills() {
891
1171
  const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
892
1172
  for (const f of files) {
893
1173
  const name = f.replace(".md", "");
894
- cachedSkills[name] = readFileSync(resolve(dir, f), "utf-8");
1174
+ cachedSkills[name] = readFileSync2(resolve(dir, f), "utf-8");
895
1175
  }
896
1176
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedSkills).length} skills from ${dir}`);
897
1177
  return cachedSkills;
@@ -950,7 +1230,7 @@ var init_skill_loader = __esm({
950
1230
  "src/engine/skill-loader.ts"() {
951
1231
  "use strict";
952
1232
  init_esm_shims();
953
- __filename2 = fileURLToPath2(import.meta.url);
1233
+ __filename2 = fileURLToPath3(import.meta.url);
954
1234
  __dirname2 = dirname(__filename2);
955
1235
  SKILLS_DIR = resolve(__dirname2, "../skills");
956
1236
  SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
@@ -1022,11 +1302,13 @@ __export(runtime_config_exports, {
1022
1302
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1023
1303
  updateSkillEnabled: () => updateSkillEnabled
1024
1304
  });
1025
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1026
- import { join } from "path";
1027
- import { homedir } from "os";
1028
- import { fileURLToPath as fileURLToPath3 } from "url";
1305
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1306
+ import { join as join2 } from "path";
1307
+ import { fileURLToPath as fileURLToPath4 } from "url";
1029
1308
  import { dirname as dirname2 } from "path";
1309
+ function runtimeDir() {
1310
+ return getRuntimeConfigDir();
1311
+ }
1030
1312
  function getRuntimeConfigPath() {
1031
1313
  const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
1032
1314
  if (envPath) return envPath;
@@ -1059,7 +1341,7 @@ function loadRuntimeConfig() {
1059
1341
  return seeded;
1060
1342
  }
1061
1343
  try {
1062
- const data = JSON.parse(readFileSync2(p, "utf-8"));
1344
+ const data = JSON.parse(readFileSync3(p, "utf-8"));
1063
1345
  const merged = mergeConfig(data, generateSourceDefaults());
1064
1346
  return {
1065
1347
  version: data.version || 1,
@@ -1129,7 +1411,7 @@ function hasRuntimeConfig() {
1129
1411
  const p = getRuntimeConfigPath();
1130
1412
  if (!existsSync3(p)) return false;
1131
1413
  try {
1132
- JSON.parse(readFileSync2(p, "utf-8"));
1414
+ JSON.parse(readFileSync3(p, "utf-8"));
1133
1415
  return true;
1134
1416
  } catch {
1135
1417
  return false;
@@ -1317,11 +1599,11 @@ function _agentRulesToDefaults() {
1317
1599
  return out;
1318
1600
  }
1319
1601
  for (const agentId of entries) {
1320
- const rulesFile = join(agentsDir, agentId, "rules.json");
1602
+ const rulesFile = join2(agentsDir, agentId, "rules.json");
1321
1603
  if (!existsSync3(rulesFile)) continue;
1322
1604
  let data;
1323
1605
  try {
1324
- data = JSON.parse(readFileSync2(rulesFile, "utf-8"));
1606
+ data = JSON.parse(readFileSync3(rulesFile, "utf-8"));
1325
1607
  } catch {
1326
1608
  continue;
1327
1609
  }
@@ -1737,12 +2019,12 @@ function deleteCustomRule(layer, category, ruleId) {
1737
2019
  function addCustomSkill(skillId, category, keywords, content) {
1738
2020
  _validateSkillId(skillId);
1739
2021
  const runtime = loadRuntimeConfig();
1740
- const skillsDir = join(RUNTIME_CONFIG_DIR, "agent_moss_skills");
2022
+ const skillsDir = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills");
1741
2023
  try {
1742
2024
  mkdirSync(skillsDir, { recursive: true });
1743
2025
  } catch {
1744
2026
  }
1745
- writeFileSync(join(skillsDir, `${skillId}.md`), content, "utf-8");
2027
+ writeFileSync(join2(skillsDir, `${skillId}.md`), content, "utf-8");
1746
2028
  const l3 = runtime.L3_skills || (runtime.L3_skills = {});
1747
2029
  const cat = l3[category] || (l3[category] = { category_enabled: true, category_desc: "", skills: [] });
1748
2030
  (cat.skills || (cat.skills = [])).push({ id: skillId, keywords, enabled: true, builtin: false });
@@ -1757,7 +2039,7 @@ function deleteCustomSkill(skillId, category) {
1757
2039
  if (idx >= 0) {
1758
2040
  if (skills[idx].builtin) return null;
1759
2041
  skills.splice(idx, 1);
1760
- const skillPath = join(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
2042
+ const skillPath = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
1761
2043
  try {
1762
2044
  if (existsSync3(skillPath)) unlinkSync(skillPath);
1763
2045
  } catch {
@@ -1873,18 +2155,19 @@ function updateAgentCategoryEnabled(agentId, category, enabled) {
1873
2155
  saveRuntimeConfig(runtime);
1874
2156
  return runtime;
1875
2157
  }
1876
- 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;
2158
+ 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;
1877
2159
  var init_runtime_config = __esm({
1878
2160
  "src/infra/runtime-config.ts"() {
1879
2161
  "use strict";
1880
2162
  init_esm_shims();
2163
+ init_paths();
1881
2164
  init_heuristic();
1882
2165
  init_logic_rules();
1883
2166
  init_skill_loader();
1884
- __dirname3 = dirname2(fileURLToPath3(import.meta.url));
1885
- RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
1886
- RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
2167
+ __dirname3 = dirname2(fileURLToPath4(import.meta.url));
1887
2168
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
2169
+ RUNTIME_CONFIG_DIR = runtimeDir();
2170
+ RUNTIME_CONFIG_PATH = join2(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
1888
2171
  LAYER_DISABLE_ENV = {
1889
2172
  L1_heuristic: "AGENT_MOSS_DISABLE_HEURISTIC",
1890
2173
  L2_logic_rules: "AGENT_MOSS_DISABLE_LOGIC_RULES",
@@ -1895,16 +2178,16 @@ var init_runtime_config = __esm({
1895
2178
  deny_read: "\u4EC5\u62E6\u622A\u8BFB\u53D6",
1896
2179
  deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
1897
2180
  };
1898
- _AGENTS_DIR_PROD = join(__dirname3, "agents");
1899
- _AGENTS_DIR_DEV = join(__dirname3, "..", "..", "..", "agent_moss", "agents");
2181
+ _AGENTS_DIR_PROD = join2(__dirname3, "agents");
2182
+ _AGENTS_DIR_DEV = join2(__dirname3, "..", "..", "..", "agent_moss", "agents");
1900
2183
  SKILL_ID_RE = /^[A-Za-z0-9_-]+$/;
1901
2184
  }
1902
2185
  });
1903
2186
 
1904
2187
  // src/agents/index.ts
1905
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2188
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1906
2189
  import { resolve as resolve2, dirname as dirname3 } from "path";
1907
- import { fileURLToPath as fileURLToPath4 } from "url";
2190
+ import { fileURLToPath as fileURLToPath5 } from "url";
1908
2191
  function resolveAgentsDir() {
1909
2192
  if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
1910
2193
  if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
@@ -1929,7 +2212,7 @@ function getAgentExtensions(agentId) {
1929
2212
  return null;
1930
2213
  }
1931
2214
  try {
1932
- const raw = readFileSync3(rulesFile, "utf-8");
2215
+ const raw = readFileSync4(rulesFile, "utf-8");
1933
2216
  ext = JSON.parse(raw);
1934
2217
  if (!ext.agent_id) {
1935
2218
  console.warn(`[AgentMoss agents] rules.json \u7F3A\u5C11 agent_id \u5B57\u6BB5: ${rulesFile}`);
@@ -1953,7 +2236,7 @@ var init_agents = __esm({
1953
2236
  "src/agents/index.ts"() {
1954
2237
  "use strict";
1955
2238
  init_esm_shims();
1956
- __filename3 = fileURLToPath4(import.meta.url);
2239
+ __filename3 = fileURLToPath5(import.meta.url);
1957
2240
  __dirname4 = dirname3(__filename3);
1958
2241
  AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
1959
2242
  AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
@@ -1973,19 +2256,25 @@ function runHeuristicCheck(input) {
1973
2256
  const re = new RegExp(rule.pattern);
1974
2257
  if (re.test(command)) {
1975
2258
  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
- }
2259
+ return {
2260
+ allowed: false,
2261
+ reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
2262
+ riskLevel: rule.severity,
2263
+ riskType: "sensitive_action",
2264
+ confidence: 90,
2265
+ source: "heuristic",
2266
+ violatedLayers: ["1.1"]
2267
+ };
1981
2268
  }
1982
2269
  }
1983
2270
  } catch {
1984
2271
  }
1985
2272
  }
2273
+ const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
2274
+ const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
1986
2275
  let topRiskType = "";
1987
2276
  for (const rule of DANGEROUS_PATTERNS) {
1988
- if (rule.pattern.test(command)) {
2277
+ if (rule.pattern.test(scanCommand)) {
1989
2278
  matched.push(rule.description);
1990
2279
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1991
2280
  highestSeverity = rule.severity;
@@ -1999,7 +2288,7 @@ function runHeuristicCheck(input) {
1999
2288
  for (const rule of agentExt.extra_command_patterns) {
2000
2289
  try {
2001
2290
  const re = new RegExp(rule.pattern, "i");
2002
- if (re.test(command)) {
2291
+ if (re.test(scanCommand)) {
2003
2292
  matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
2004
2293
  if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
2005
2294
  highestSeverity = rule.risk_level;
@@ -2023,14 +2312,12 @@ function runHeuristicCheck(input) {
2023
2312
  const actionTypeL = input.aNext.actionType.toLowerCase();
2024
2313
  const detailL = input.aNext.actionDetail.toLowerCase();
2025
2314
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2315
+ const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2316
+ if (dangerousResult) return dangerousResult;
2026
2317
  if (!isExcludedType) {
2027
2318
  const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2028
2319
  if (pathResult) return pathResult;
2029
2320
  }
2030
- const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2031
- if (dangerousResult) return dangerousResult;
2032
- const lateralResult = checkLateralMovement(detailL);
2033
- if (lateralResult) return lateralResult;
2034
2321
  if (matched.length > 0) {
2035
2322
  return {
2036
2323
  allowed: false,
@@ -2043,6 +2330,8 @@ function runHeuristicCheck(input) {
2043
2330
  // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2044
2331
  };
2045
2332
  }
2333
+ const lateralResult = checkLateralMovement(detailL);
2334
+ if (lateralResult) return lateralResult;
2046
2335
  return null;
2047
2336
  }
2048
2337
  function checkInjectionKeywords(text, extraKeywords) {
@@ -2065,6 +2354,68 @@ function isInlineScriptCommand(actionDetail) {
2065
2354
  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
2355
  return re.test(actionDetail);
2067
2356
  }
2357
+ function stripLiteralTextSegments(actionDetail) {
2358
+ const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
2359
+ const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
2360
+ if (writeScriptExec || pipeToInterpreter) {
2361
+ return actionDetail;
2362
+ }
2363
+ const resultParts = [];
2364
+ for (const seg of splitCommandSegments(actionDetail)) {
2365
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
2366
+ if (fw === "echo" || fw === "printf") {
2367
+ const segUnquotedRedirect = seg.replace(
2368
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
2369
+ (_m, _q, p) => ">" + p
2370
+ );
2371
+ resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
2372
+ } else {
2373
+ resultParts.push(seg);
2374
+ }
2375
+ }
2376
+ return resultParts.join("");
2377
+ }
2378
+ function splitCommandSegments(actionDetail) {
2379
+ const segments = [];
2380
+ let cur = "";
2381
+ let quote = null;
2382
+ for (let i = 0; i < actionDetail.length; i++) {
2383
+ const ch = actionDetail[i];
2384
+ if (quote) {
2385
+ cur += ch;
2386
+ if (ch === quote) quote = null;
2387
+ continue;
2388
+ }
2389
+ if (ch === "'" || ch === '"') {
2390
+ quote = ch;
2391
+ cur += ch;
2392
+ continue;
2393
+ }
2394
+ if (ch === ";" || ch === "\n") {
2395
+ segments.push(cur);
2396
+ segments.push(ch);
2397
+ cur = "";
2398
+ continue;
2399
+ }
2400
+ if (ch === "&" && actionDetail[i + 1] === "&") {
2401
+ segments.push(cur);
2402
+ segments.push("&&");
2403
+ cur = "";
2404
+ i++;
2405
+ continue;
2406
+ }
2407
+ if (ch === "|" && actionDetail[i + 1] === "|") {
2408
+ segments.push(cur);
2409
+ segments.push("||");
2410
+ cur = "";
2411
+ i++;
2412
+ continue;
2413
+ }
2414
+ cur += ch;
2415
+ }
2416
+ if (cur) segments.push(cur);
2417
+ return segments;
2418
+ }
2068
2419
  function severityRank(s) {
2069
2420
  const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
2070
2421
  return ranks[s] || 0;
@@ -2072,13 +2423,18 @@ function severityRank(s) {
2072
2423
  function checkSensitivePath(command, cwd, agentExt) {
2073
2424
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2074
2425
  const isWriteOpFlag = isWriteOperation("", command);
2426
+ const pathScanText = stripLiteralTextSegments(command);
2075
2427
  const pathMatches = (text, path3) => {
2076
- const escaped = path3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2428
+ const parts = path3.split(/(\(\?!.*?\))/);
2429
+ const escaped = parts.map((part) => {
2430
+ if (part.startsWith("(?!")) return part;
2431
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2432
+ }).join("");
2077
2433
  const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
2078
2434
  return re.test(text);
2079
2435
  };
2080
2436
  for (const sp of ROOT_SENSITIVE_PATHS) {
2081
- if (pathMatches(command, sp.path.toLowerCase())) {
2437
+ if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2082
2438
  if (sp.path === "/dev/random") {
2083
2439
  if (isWriteOpFlag) {
2084
2440
  continue;
@@ -2112,7 +2468,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2112
2468
  const expanded = `${expandedCwd}/${sp.path}`;
2113
2469
  const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2114
2470
  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)) {
2471
+ if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2116
2472
  if (!isWriteOpFlag && !sp.credential) {
2117
2473
  continue;
2118
2474
  }
@@ -2183,8 +2539,10 @@ function checkSensitivePath(command, cwd, agentExt) {
2183
2539
  }
2184
2540
  function checkDangerousPatterns(actionType, actionDetail) {
2185
2541
  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));
2542
+ const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2543
+ const isTempPath = TEMP_DIRS.some(
2544
+ (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
2545
+ );
2188
2546
  if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
2189
2547
  return {
2190
2548
  allowed: false,
@@ -2196,7 +2554,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
2196
2554
  violatedLayers: ["1.1"]
2197
2555
  };
2198
2556
  }
2199
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail) || />\s*\/proc\//.test(actionDetail)) {
2557
+ const findWriteMatch = actionDetail.match(
2558
+ /\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
2559
+ );
2560
+ if (findWriteMatch) {
2561
+ const cmdHit = findWriteMatch[1] || "xargs";
2562
+ return {
2563
+ allowed: false,
2564
+ reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
2565
+ riskLevel: "high",
2566
+ riskType: "script_execution",
2567
+ confidence: 90,
2568
+ source: "heuristic",
2569
+ violatedLayers: ["1.1"]
2570
+ };
2571
+ }
2572
+ if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
2200
2573
  return {
2201
2574
  allowed: false,
2202
2575
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -2207,17 +2580,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
2207
2580
  violatedLayers: ["1.1"]
2208
2581
  };
2209
2582
  }
2583
+ if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
2584
+ return {
2585
+ allowed: false,
2586
+ reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
2587
+ riskLevel: "high",
2588
+ riskType: "file_access",
2589
+ confidence: 90,
2590
+ source: "heuristic",
2591
+ violatedLayers: ["1.1"]
2592
+ };
2593
+ }
2210
2594
  return null;
2211
2595
  }
2212
2596
  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;
2597
+ const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
2598
+ for (const tok of tokens) {
2599
+ const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
2600
+ if (am && am[2] && !isLocalHost(am[2])) {
2601
+ return `${am[1]}@${am[2]}`;
2602
+ }
2603
+ }
2604
+ for (const tok of tokens) {
2605
+ const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
2606
+ if (hm && hm[1] && !isLocalHost(hm[1])) {
2607
+ return hm[1];
2608
+ }
2609
+ }
2610
+ const skipValueOpts = /* @__PURE__ */ new Set([
2611
+ "-p",
2612
+ "-l",
2613
+ "-i",
2614
+ "-o",
2615
+ "-b",
2616
+ "-c",
2617
+ "-m",
2618
+ "-F",
2619
+ "-e",
2620
+ "-S",
2621
+ "-W",
2622
+ "-J",
2623
+ "-I",
2624
+ "-T",
2625
+ "-B",
2626
+ "-L",
2627
+ "-R",
2628
+ "-D",
2629
+ "-E",
2630
+ "-M",
2631
+ "-O",
2632
+ "-P",
2633
+ "-q"
2634
+ ]);
2635
+ let foundCmd = false;
2636
+ for (let i = 0; i < tokens.length; i++) {
2637
+ const t = tokens[i];
2638
+ if (t === "ssh" || t === "scp" || t === "rsync") {
2639
+ foundCmd = true;
2640
+ continue;
2641
+ }
2642
+ if (!foundCmd) continue;
2643
+ if (t.startsWith("-")) {
2644
+ if (skipValueOpts.has(t) && i + 1 < tokens.length) {
2645
+ i++;
2646
+ }
2647
+ continue;
2648
+ }
2649
+ if (t.startsWith("/")) continue;
2650
+ return t;
2651
+ }
2652
+ return null;
2215
2653
  }
2216
2654
  function isLocalHost(host) {
2217
2655
  const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
2218
2656
  return localNames.includes(host.replace(/.*@/, ""));
2219
2657
  }
2220
2658
  function checkLateralMovement(actionDetail) {
2659
+ if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
2660
+ const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
2661
+ let hasRealCall = false;
2662
+ for (const seg of segments) {
2663
+ const parts = seg.split(/\s+/);
2664
+ const fw = (parts[0] || "").toLowerCase();
2665
+ if (fw === "echo" || fw === "printf") continue;
2666
+ if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
2667
+ hasRealCall = true;
2668
+ break;
2669
+ }
2670
+ }
2671
+ if (!hasRealCall) return null;
2221
2672
  if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
2222
2673
  const targetHost = extractRemoteHost(actionDetail);
2223
2674
  if (targetHost && !isLocalHost(targetHost)) {
@@ -2317,10 +2768,14 @@ var init_heuristic = __esm({
2317
2768
  { 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
2769
  { pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
2319
2770
  // --- 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" },
2771
+ // curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
2772
+ // 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
2773
+ // runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
2774
+ { pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
2775
+ { pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2321
2776
  { pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
2322
2777
  { 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" },
2778
+ { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
2324
2779
  // --- Critical: reverse shell ---
2325
2780
  { pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
2326
2781
  { pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
@@ -2331,13 +2786,25 @@ var init_heuristic = __esm({
2331
2786
  { pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
2332
2787
  // --- Critical: privilege escalation / container escape ---
2333
2788
  { 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" },
2789
+ // --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
2790
+ // 修正「只拦 777、xx7 漏放」的缺口:
2791
+ // 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
2792
+ // 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
2793
+ // 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
2794
+ // 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
2795
+ // 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
2796
+ { 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" },
2797
+ { 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" },
2798
+ { 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
2799
  { 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
2800
  // sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
2338
2801
  { 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
2802
  // --- High: system config overwrite ---
2803
+ // > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
2804
+ // 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
2340
2805
  { pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
2806
+ { pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
2807
+ // 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
2341
2808
  // --- High: download script + execute risk ---
2342
2809
  { 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
2810
  { 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 +2820,11 @@ var init_heuristic = __esm({
2353
2820
  // --- Medium: credential export ---
2354
2821
  { pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
2355
2822
  // --- High: shell rc file append (persistence) ---
2823
+ // 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
2824
+ // 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
2825
+ // /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
2356
2826
  { 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" },
2827
+ { 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
2828
  // --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
2358
2829
  { 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
2830
  // --- High: non-interactive password modification ---
@@ -2381,9 +2852,9 @@ var init_heuristic = __esm({
2381
2852
  });
2382
2853
 
2383
2854
  // src/engine/template-loader.ts
2384
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2855
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
2385
2856
  import { resolve as resolve3, dirname as dirname4 } from "path";
2386
- import { fileURLToPath as fileURLToPath5 } from "url";
2857
+ import { fileURLToPath as fileURLToPath6 } from "url";
2387
2858
  function loadTemplates() {
2388
2859
  if (cachedTemplates) return cachedTemplates;
2389
2860
  cachedTemplates = {};
@@ -2396,7 +2867,7 @@ function loadTemplates() {
2396
2867
  const filePath = resolve3(dir, file);
2397
2868
  if (existsSync5(filePath)) {
2398
2869
  const key = file.replace(".txt", "").replace("_template", "");
2399
- cachedTemplates[key] = readFileSync4(filePath, "utf-8");
2870
+ cachedTemplates[key] = readFileSync5(filePath, "utf-8");
2400
2871
  }
2401
2872
  }
2402
2873
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedTemplates).length} templates from ${dir}`);
@@ -2407,7 +2878,7 @@ var init_template_loader = __esm({
2407
2878
  "src/engine/template-loader.ts"() {
2408
2879
  "use strict";
2409
2880
  init_esm_shims();
2410
- __filename4 = fileURLToPath5(import.meta.url);
2881
+ __filename4 = fileURLToPath6(import.meta.url);
2411
2882
  __dirname5 = dirname4(__filename4);
2412
2883
  TEMPLATES_DIR = resolve3(__dirname5, "../templates");
2413
2884
  TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
@@ -2417,8 +2888,8 @@ var init_template_loader = __esm({
2417
2888
  });
2418
2889
 
2419
2890
  // src/engine/script-analyzer.ts
2420
- import { readFileSync as readFileSync5 } from "fs";
2421
- import { resolve as resolve4, join as join2 } from "path";
2891
+ import { readFileSync as readFileSync6 } from "fs";
2892
+ import { resolve as resolve4, join as join3 } from "path";
2422
2893
  function emptyResult() {
2423
2894
  return { scriptPath: "", scriptContent: "", matchedKeywords: [], lineCount: 0, isSuspicious: false, error: "", riskLevel: "", riskReason: "" };
2424
2895
  }
@@ -2471,8 +2942,8 @@ function assessKeywordCombinationRisk(matchedKeywords, scriptContent) {
2471
2942
  }
2472
2943
  function readScriptContent(scriptPath, maxLines = 500) {
2473
2944
  try {
2474
- const expandedPath = scriptPath.startsWith("~") ? join2(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
2475
- const raw = readFileSync5(expandedPath, "utf-8");
2945
+ const expandedPath = scriptPath.startsWith("~") ? join3(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
2946
+ const raw = readFileSync6(expandedPath, "utf-8");
2476
2947
  const lines = raw.split("\n").slice(0, maxLines);
2477
2948
  return { content: lines.join("\n"), lineCount: lines.length, error: "" };
2478
2949
  } catch (e) {
@@ -2610,8 +3081,8 @@ var init_script_analyzer = __esm({
2610
3081
  });
2611
3082
 
2612
3083
  // src/engine/script-content-analyzer.ts
2613
- import { readFileSync as readFileSync6, statSync, existsSync as existsSync6 } from "fs";
2614
- import { resolve as resolve5, join as join3, isAbsolute, dirname as dirname5 } from "path";
3084
+ import { readFileSync as readFileSync7, statSync, existsSync as existsSync6 } from "fs";
3085
+ import { resolve as resolve5, join as join4, isAbsolute, dirname as dirname5 } from "path";
2615
3086
  import { homedir as homedir2 } from "os";
2616
3087
  function emptyChain() {
2617
3088
  return {
@@ -2770,7 +3241,7 @@ function extractInlineScriptPath(actionDetail) {
2770
3241
  function resolvePath(p) {
2771
3242
  let expanded = p;
2772
3243
  if (expanded.startsWith("~/") || expanded === "~") {
2773
- expanded = join3(homedir2(), expanded.slice(1));
3244
+ expanded = join4(homedir2(), expanded.slice(1));
2774
3245
  }
2775
3246
  return resolve5(expanded);
2776
3247
  }
@@ -2783,7 +3254,7 @@ function readScriptContent2(scriptPath, maxFileSize = 2 * 1024 * 1024) {
2783
3254
  if (stat.size > maxFileSize) {
2784
3255
  return { content: "", lineCount: 0, fileSize: stat.size, error: `\u811A\u672C\u6587\u4EF6\u8FC7\u5927 (${stat.size} bytes)\uFF0C\u8DF3\u8FC7\u8BFB\u53D6` };
2785
3256
  }
2786
- const content = readFileSync6(path3, "utf-8");
3257
+ const content = readFileSync7(path3, "utf-8");
2787
3258
  const lineCount = content ? content.split("\n").length : 0;
2788
3259
  return { content, lineCount, fileSize: stat.size, error: "" };
2789
3260
  } catch (e) {
@@ -2800,7 +3271,7 @@ function extractChildScriptPaths(content, baseDir) {
2800
3271
  const pathStr = m[1];
2801
3272
  if (pathStr.startsWith("-") || pathStr.startsWith("$")) continue;
2802
3273
  if (pathStr.includes("/dev/") || pathStr.startsWith("|") || pathStr.startsWith(";")) continue;
2803
- const full = isAbsolute(pathStr) ? pathStr : join3(baseDir, pathStr);
3274
+ const full = isAbsolute(pathStr) ? pathStr : join4(baseDir, pathStr);
2804
3275
  const resolved = resolvePath(full);
2805
3276
  if (existsSync6(resolved)) {
2806
3277
  try {
@@ -3153,17 +3624,16 @@ __export(token_stats_exports, {
3153
3624
  recordTokenUsage: () => recordTokenUsage,
3154
3625
  resetTokenStats: () => resetTokenStats
3155
3626
  });
3156
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
3157
- import { join as join4 } from "path";
3158
- import { homedir as homedir3 } from "os";
3627
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
3159
3628
  function getStatsPath() {
3160
3629
  const envPath = process.env[ENV_STATS_PATH];
3161
- return envPath || STATS_FILE;
3630
+ if (envPath) return envPath;
3631
+ return getTokenStatsPath();
3162
3632
  }
3163
3633
  function loadAllRecords(statsPath) {
3164
3634
  if (!existsSync7(statsPath)) return [];
3165
3635
  try {
3166
- const data = JSON.parse(readFileSync7(statsPath, "utf-8"));
3636
+ const data = JSON.parse(readFileSync8(statsPath, "utf-8"));
3167
3637
  if (Array.isArray(data)) return data;
3168
3638
  if (data && Array.isArray(data.records)) return data.records;
3169
3639
  return [];
@@ -3303,13 +3773,12 @@ function resetTokenStats() {
3303
3773
  }
3304
3774
  }
3305
3775
  }
3306
- var STATS_DIR, STATS_FILE, ENV_STATS_PATH, MAX_RECORDS;
3776
+ var ENV_STATS_PATH, MAX_RECORDS;
3307
3777
  var init_token_stats = __esm({
3308
3778
  "src/engine/token-stats.ts"() {
3309
3779
  "use strict";
3310
3780
  init_esm_shims();
3311
- STATS_DIR = join4(homedir3(), ".config", "agentmoss");
3312
- STATS_FILE = join4(STATS_DIR, "agent_moss_token_stats.json");
3781
+ init_paths();
3313
3782
  ENV_STATS_PATH = "AGENT_MOSS_TOKEN_STATS_PATH";
3314
3783
  MAX_RECORDS = 1e4;
3315
3784
  }
@@ -3487,11 +3956,13 @@ async function analyzeWithLLM(prompt, config) {
3487
3956
  } catch {
3488
3957
  }
3489
3958
  clearTimeout(totalTimeoutTimer);
3959
+ const riskLevel = parsed.riskLevel ?? parsed.risk_level;
3960
+ const riskType = parsed.riskType ?? parsed.risk_type;
3490
3961
  return {
3491
3962
  allowed: parsed.allowed !== false,
3492
3963
  reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
3493
- riskLevel: ["low", "medium", "high", "critical"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",
3494
- riskType: parsed.riskType || "",
3964
+ riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
3965
+ riskType: riskType || "",
3495
3966
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
3496
3967
  source: "llm",
3497
3968
  violatedLayers: ["3"]
@@ -3560,7 +4031,10 @@ async function runAnalysis(input) {
3560
4031
  if (heuristicResult && !heuristicResult.allowed) {
3561
4032
  const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
3562
4033
  if (!skipInlineFileAccess) {
3563
- return heuristicResult;
4034
+ const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
4035
+ if (shouldDenyL1) {
4036
+ return heuristicResult;
4037
+ }
3564
4038
  }
3565
4039
  }
3566
4040
  let scriptChain = null;
@@ -3638,7 +4112,8 @@ async function runAnalysis(input) {
3638
4112
  ) && DANGEROUS_PATTERNS.some(
3639
4113
  (p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
3640
4114
  );
3641
- if (!fullCmdHit) {
4115
+ const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
4116
+ if (!fullCmdHit && !hasRedirectWrite) {
3642
4117
  return {
3643
4118
  allowed: true,
3644
4119
  reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
@@ -3937,7 +4412,7 @@ var require_bindings = __commonJS({
3937
4412
  init_esm_shims();
3938
4413
  var fs4 = __require("fs");
3939
4414
  var path3 = __require("path");
3940
- var fileURLToPath8 = require_file_uri_to_path();
4415
+ var fileURLToPath9 = require_file_uri_to_path();
3941
4416
  var join6 = path3.join;
3942
4417
  var dirname7 = path3.dirname;
3943
4418
  var exists = fs4.accessSync && function(path4) {
@@ -4049,7 +4524,7 @@ var require_bindings = __commonJS({
4049
4524
  Error.stackTraceLimit = origSTL;
4050
4525
  var fileSchema = "file://";
4051
4526
  if (fileName.indexOf(fileSchema) === 0) {
4052
- fileName = fileURLToPath8(fileName);
4527
+ fileName = fileURLToPath9(fileName);
4053
4528
  }
4054
4529
  return fileName;
4055
4530
  };
@@ -4895,10 +5370,10 @@ init_esm_shims();
4895
5370
  init_runtime_config();
4896
5371
  init_token_stats();
4897
5372
  import { Hono } from "hono";
4898
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
5373
+ import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
4899
5374
  import { join as join5, dirname as dirname6 } from "path";
4900
- import { fileURLToPath as fileURLToPath6 } from "url";
4901
- var __dirname6 = dirname6(fileURLToPath6(import.meta.url));
5375
+ import { fileURLToPath as fileURLToPath7 } from "url";
5376
+ var __dirname6 = dirname6(fileURLToPath7(import.meta.url));
4902
5377
  var STATIC_DIR = join5(__dirname6, "static");
4903
5378
  var CONSOLE_TOKEN_ENV = "AGENT_MOSS_CONSOLE_TOKEN";
4904
5379
  function getConsoleToken() {
@@ -4925,7 +5400,7 @@ function createConsoleRouter() {
4925
5400
  const serveIndex = (c) => {
4926
5401
  const indexFile = join5(STATIC_DIR, "index.html");
4927
5402
  if (existsSync9(indexFile)) {
4928
- return c.html(readFileSync8(indexFile, "utf-8"));
5403
+ return c.html(readFileSync9(indexFile, "utf-8"));
4929
5404
  }
4930
5405
  return c.html("<h1>AgentMoss Policy Console</h1><p>Frontend not built. Use /console/api/* endpoints.</p>");
4931
5406
  };
@@ -4933,12 +5408,12 @@ function createConsoleRouter() {
4933
5408
  console2.get("", serveIndex);
4934
5409
  console2.get("/api/layers", (c) => {
4935
5410
  const rc = loadRuntimeConfig();
4936
- return c.json({ layers: rc.layers });
5411
+ return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
4937
5412
  });
4938
5413
  console2.put("/api/layers", async (c) => {
4939
5414
  const body = await c.req.json();
4940
5415
  const rc = updateLayerEnabled(body.layer_key, body.enabled);
4941
- return c.json({ layers: rc.layers });
5416
+ return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
4942
5417
  });
4943
5418
  console2.get("/api/rules", (c) => {
4944
5419
  const rc = loadRuntimeConfig();
@@ -4947,22 +5422,22 @@ function createConsoleRouter() {
4947
5422
  console2.put("/api/rules/enabled", async (c) => {
4948
5423
  const b = await c.req.json();
4949
5424
  const rc = updateRuleEnabled(b.layer, b.category, b.rule_id, b.enabled);
4950
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5425
+ return c.json(rc);
4951
5426
  });
4952
5427
  console2.put("/api/rules/deny_mode", async (c) => {
4953
5428
  const b = await c.req.json();
4954
5429
  const rc = updateRuleDenyMode(b.layer, b.category, b.rule_id, b.deny_mode);
4955
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5430
+ return c.json(rc);
4956
5431
  });
4957
5432
  console2.put("/api/rules/skip_l3", async (c) => {
4958
5433
  const b = await c.req.json();
4959
5434
  const rc = updateRuleSkipL3(b.layer, b.category, b.rule_id, b.skip_l3_on_disabled);
4960
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5435
+ return c.json(rc);
4961
5436
  });
4962
5437
  console2.put("/api/categories/enabled", async (c) => {
4963
5438
  const b = await c.req.json();
4964
5439
  const rc = updateCategoryEnabled(b.layer, b.category, b.enabled);
4965
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5440
+ return c.json(rc);
4966
5441
  });
4967
5442
  console2.post("/api/rules", async (c) => {
4968
5443
  const b = await c.req.json();
@@ -4978,38 +5453,38 @@ function createConsoleRouter() {
4978
5453
  credential: b.credential,
4979
5454
  deny_mode: b.deny_mode
4980
5455
  });
4981
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5456
+ return c.json(rc);
4982
5457
  });
4983
5458
  console2.delete("/api/rules", async (c) => {
4984
5459
  const b = await c.req.json();
4985
5460
  deleteCustomRule(b.layer, b.category, b.rule_id);
4986
5461
  const rc = loadRuntimeConfig();
4987
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5462
+ return c.json(rc);
4988
5463
  });
4989
5464
  console2.get("/api/skills", (c) => {
4990
5465
  const rc = loadRuntimeConfig();
4991
- return c.json({ L3_skills: rc.L3_skills });
5466
+ return c.json(rc.L3_skills);
4992
5467
  });
4993
5468
  console2.put("/api/skills/enabled", async (c) => {
4994
5469
  const b = await c.req.json();
4995
5470
  const rc = updateSkillEnabled(b.skill_id, b.enabled);
4996
- return c.json({ L3_skills: rc.L3_skills });
5471
+ return c.json(rc);
4997
5472
  });
4998
5473
  console2.put("/api/skill-categories/enabled", async (c) => {
4999
5474
  const b = await c.req.json();
5000
5475
  const rc = updateSkillCategoryEnabled(b.category, b.enabled);
5001
- return c.json({ L3_skills: rc.L3_skills });
5476
+ return c.json(rc);
5002
5477
  });
5003
5478
  console2.post("/api/skills", async (c) => {
5004
5479
  const b = await c.req.json();
5005
5480
  const rc = addCustomSkill(b.skill_id, b.category || "user_custom", b.keywords || [], b.content);
5006
- return c.json({ L3_skills: rc.L3_skills });
5481
+ return c.json(rc);
5007
5482
  });
5008
5483
  console2.delete("/api/skills", async (c) => {
5009
5484
  const b = await c.req.json();
5010
5485
  deleteCustomSkill(b.skill_id, b.category);
5011
5486
  const rc = loadRuntimeConfig();
5012
- return c.json({ L3_skills: rc.L3_skills });
5487
+ return c.json(rc);
5013
5488
  });
5014
5489
  console2.get("/api/skills/:skill_id/content", async (c) => {
5015
5490
  const skillId = c.req.param("skill_id");
@@ -5019,10 +5494,10 @@ function createConsoleRouter() {
5019
5494
  const skillPath = join5(dirname6(getRuntimeConfigPath()), "agent_moss_skills", `${skillId}.md`);
5020
5495
  if (!existsSync9(skillPath)) {
5021
5496
  const builtinPath = join5(__dirname6, "..", "..", "agent_moss", "skills", `${skillId}.md`);
5022
- if (existsSync9(builtinPath)) return c.text(readFileSync8(builtinPath, "utf-8"));
5497
+ if (existsSync9(builtinPath)) return c.text(readFileSync9(builtinPath, "utf-8"));
5023
5498
  return c.json({ error: "skill not found" }, 404);
5024
5499
  }
5025
- return c.text(readFileSync8(skillPath, "utf-8"));
5500
+ return c.text(readFileSync9(skillPath, "utf-8"));
5026
5501
  });
5027
5502
  console2.get("/api/agents", (c) => {
5028
5503
  const rc = loadRuntimeConfig();
@@ -5064,7 +5539,7 @@ function createConsoleRouter() {
5064
5539
  });
5065
5540
  console2.get("/api/config", (c) => {
5066
5541
  const rc = loadRuntimeConfig();
5067
- return c.json({ config: rc, config_path: getRuntimeConfigPath() });
5542
+ return c.json(rc);
5068
5543
  });
5069
5544
  console2.get("/api/env-overrides", (c) => {
5070
5545
  return c.json({ overrides: getEnvOverrides() });
@@ -5072,7 +5547,7 @@ function createConsoleRouter() {
5072
5547
  console2.post("/api/reset", (c) => {
5073
5548
  const defaults = generateSourceDefaults();
5074
5549
  saveRuntimeConfig(defaults);
5075
- return c.json({ config: defaults });
5550
+ return c.json(defaults);
5076
5551
  });
5077
5552
  console2.get("/api/token-stats", (c) => {
5078
5553
  const days = parseInt(c.req.query("days") || "7", 10);
@@ -5080,7 +5555,7 @@ function createConsoleRouter() {
5080
5555
  });
5081
5556
  console2.get("/api/token-stats/recent", (c) => {
5082
5557
  const limit = parseInt(c.req.query("limit") || "20", 10);
5083
- return c.json({ records: getRecentRecords(limit) });
5558
+ return c.json(getRecentRecords(limit));
5084
5559
  });
5085
5560
  console2.get("/api/token-stats/trend", (c) => {
5086
5561
  const days = parseInt(c.req.query("days") || "7", 10);
@@ -5136,7 +5611,7 @@ async function startServer(host = "127.0.0.1", port = 9090) {
5136
5611
  }
5137
5612
 
5138
5613
  // src/cli.ts
5139
- import { fileURLToPath as fileURLToPath7 } from "url";
5614
+ import { fileURLToPath as fileURLToPath8 } from "url";
5140
5615
  import { realpathSync } from "fs";
5141
5616
  function printHelp() {
5142
5617
  console.log(`AgentMoss security analysis engine (TypeScript v${version})
@@ -5227,7 +5702,7 @@ async function main() {
5227
5702
  const { execSync } = await import("child_process");
5228
5703
  const fs4 = await import("fs");
5229
5704
  const path3 = await import("path");
5230
- const { fileURLToPath: fileURLToPath8 } = await import("url");
5705
+ const { fileURLToPath: fileURLToPath9 } = await import("url");
5231
5706
  if (process.getuid?.() !== 0) {
5232
5707
  try {
5233
5708
  execSync("sudo -n true", { stdio: "ignore", timeout: 5e3 });
@@ -5242,7 +5717,7 @@ async function main() {
5242
5717
  execSync(sudoArgs.join(" "), { stdio: "inherit" });
5243
5718
  process.exit(0);
5244
5719
  }
5245
- const __filename5 = fileURLToPath8(import.meta.url);
5720
+ const __filename5 = fileURLToPath9(import.meta.url);
5246
5721
  const __dirname7 = path3.dirname(__filename5);
5247
5722
  let srcPath = path3.join(__dirname7, "data", "agent_moss.service");
5248
5723
  if (!fs4.existsSync(srcPath)) {
@@ -5384,9 +5859,9 @@ async function main() {
5384
5859
  var isMain = (() => {
5385
5860
  try {
5386
5861
  if (!process.argv[1]) return false;
5387
- const thisFile = realpathSync(fileURLToPath7(import.meta.url));
5862
+ const thisFile = realpathSync(fileURLToPath8(import.meta.url));
5388
5863
  const argv1 = realpathSync(
5389
- process.argv[1].startsWith("file://") ? fileURLToPath7(process.argv[1]) : process.argv[1]
5864
+ process.argv[1].startsWith("file://") ? fileURLToPath8(process.argv[1]) : process.argv[1]
5390
5865
  );
5391
5866
  return thisFile === argv1;
5392
5867
  } catch {