@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/server.js CHANGED
@@ -42,6 +42,90 @@ var init_esm_shims = __esm({
42
42
  }
43
43
  });
44
44
 
45
+ // src/infra/paths.ts
46
+ import { join } from "path";
47
+ import { homedir } from "os";
48
+ import { readFileSync } from "fs";
49
+ function isRoot() {
50
+ const getuid = process.getuid;
51
+ return typeof getuid === "function" && getuid() === 0;
52
+ }
53
+ function resolveOwnerHome() {
54
+ for (const name of [process.env.SUDO_USER, process.env.LOGNAME, process.env.USER]) {
55
+ if (name && name !== "root" && name !== "") {
56
+ const home = homeOf(name);
57
+ if (home) return home;
58
+ }
59
+ }
60
+ try {
61
+ const passwd = readFileSync("/etc/passwd", "utf-8");
62
+ for (const line of passwd.split("\n")) {
63
+ if (!line || line.startsWith("#")) continue;
64
+ const parts = line.split(":");
65
+ if (parts.length < 6) continue;
66
+ const uid = parseInt(parts[2], 10);
67
+ const name = parts[0];
68
+ if (uid >= 1e3 && name && !name.startsWith("_")) {
69
+ return parts[5];
70
+ }
71
+ }
72
+ } catch {
73
+ }
74
+ return null;
75
+ }
76
+ function homeOf(name) {
77
+ try {
78
+ const passwd = readFileSync("/etc/passwd", "utf-8");
79
+ for (const line of passwd.split("\n")) {
80
+ if (!line || line.startsWith("#")) continue;
81
+ const parts = line.split(":");
82
+ if (parts.length >= 6 && parts[0] === name) {
83
+ return parts[5];
84
+ }
85
+ }
86
+ } catch {
87
+ }
88
+ return null;
89
+ }
90
+ function getDataRoot() {
91
+ const envPath = process.env[ENV_DATA_ROOT];
92
+ if (envPath) return envPath;
93
+ if (isRoot()) {
94
+ const ownerHome = resolveOwnerHome();
95
+ if (ownerHome) {
96
+ return join(ownerHome, ".config", "agentmoss");
97
+ }
98
+ }
99
+ return DEFAULT_DATA_ROOT;
100
+ }
101
+ function getInstance() {
102
+ const v = process.env[ENV_INSTANCE] || "";
103
+ return v.trim().replace(/^[/\\]+|[/\\]+$/g, "");
104
+ }
105
+ function getConfigDir() {
106
+ return getDataRoot();
107
+ }
108
+ function getRuntimeConfigDir() {
109
+ return getDataRoot();
110
+ }
111
+ function getTokenStatsPath() {
112
+ const instance = getInstance();
113
+ if (instance) {
114
+ return join(getDataRoot(), instance, "agent_moss_token_stats.json");
115
+ }
116
+ return join(getDataRoot(), "agent_moss_token_stats.json");
117
+ }
118
+ var ENV_DATA_ROOT, ENV_INSTANCE, DEFAULT_DATA_ROOT;
119
+ var init_paths = __esm({
120
+ "src/infra/paths.ts"() {
121
+ "use strict";
122
+ init_esm_shims();
123
+ ENV_DATA_ROOT = "AGENT_MOSS_DATA_DIR";
124
+ ENV_INSTANCE = "AGENT_MOSS_INSTANCE";
125
+ DEFAULT_DATA_ROOT = join(homedir(), ".config", "agentmoss");
126
+ }
127
+ });
128
+
45
129
  // src/config.ts
46
130
  var config_exports = {};
47
131
  __export(config_exports, {
@@ -55,11 +139,12 @@ __export(config_exports, {
55
139
  isHeuristicEnabled: () => isHeuristicEnabled,
56
140
  isLLMAvailable: () => isLLMAvailable,
57
141
  isLogicRulesEnabled: () => isLogicRulesEnabled,
142
+ probeLLMConnection: () => probeLLMConnection,
58
143
  updateLLMConfig: () => updateLLMConfig
59
144
  });
60
145
  import fs from "fs";
61
146
  import path2 from "path";
62
- import os from "os";
147
+ import { fileURLToPath as fileURLToPath2 } from "url";
63
148
  function loadConfigFile(filePath) {
64
149
  try {
65
150
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -71,7 +156,7 @@ function loadConfigFile(filePath) {
71
156
  function resolveConfigPath() {
72
157
  const envPath = process.env.AGENT_MOSS_CONFIG_PATH;
73
158
  if (envPath) return envPath;
74
- const userConfig = path2.join(os.homedir(), ".config", "agentmoss", "config.json");
159
+ const userConfig = path2.join(getConfigDir(), "config.json");
75
160
  if (fs.existsSync(userConfig)) return userConfig;
76
161
  const defaultConfig = path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example");
77
162
  if (fs.existsSync(defaultConfig)) {
@@ -122,16 +207,38 @@ function getTimeoutConfig() {
122
207
  function getRetryConfig() {
123
208
  return retryConfig;
124
209
  }
210
+ function wildcardToRegExp(pattern) {
211
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
212
+ }
125
213
  function loadCustomRules() {
214
+ const rules = [];
126
215
  try {
127
216
  const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
128
- if (!raw) return [];
129
- const parsed = JSON.parse(raw);
130
- if (!Array.isArray(parsed)) return [];
131
- return parsed.filter((r) => r.pattern && r.action);
217
+ if (raw) {
218
+ const parsed = JSON.parse(raw);
219
+ if (Array.isArray(parsed)) {
220
+ for (const r of parsed) {
221
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
222
+ }
223
+ }
224
+ }
132
225
  } catch {
133
- return [];
134
226
  }
227
+ for (const rulesPath of USER_RULES_PATHS) {
228
+ try {
229
+ if (!fs.existsSync(rulesPath)) continue;
230
+ const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
231
+ const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
232
+ for (const r of actions) {
233
+ const p = r?.pattern;
234
+ if (!p) continue;
235
+ rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
236
+ }
237
+ break;
238
+ } catch {
239
+ }
240
+ }
241
+ return rules;
135
242
  }
136
243
  function getLLMConfig() {
137
244
  return llmConfig;
@@ -146,6 +253,30 @@ function isLLMAvailable() {
146
253
  if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
147
254
  return llmConfig.apiKey.length > 0;
148
255
  }
256
+ async function probeLLMConnection(timeoutMs = 8e3) {
257
+ if (!isLLMAvailable()) return false;
258
+ const OpenAI2 = (await import("openai")).default;
259
+ const config = getLLMConfig();
260
+ const provider = detectProvider(config.baseUrl, config.provider);
261
+ const extraHeaders = getProviderHeaders(provider);
262
+ const client = new OpenAI2({
263
+ baseURL: config.baseUrl,
264
+ apiKey: config.apiKey,
265
+ timeout: timeoutMs,
266
+ maxRetries: 0,
267
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
268
+ });
269
+ try {
270
+ const resp = await client.chat.completions.create({
271
+ model: config.model,
272
+ messages: [{ role: "user", content: "ping" }],
273
+ max_tokens: 1
274
+ });
275
+ return !!(resp && resp.choices && resp.choices[0]);
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
149
280
  function getLLMFailMode() {
150
281
  const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
151
282
  return val === "fail_closed" ? "fail_closed" : "fail_open";
@@ -189,11 +320,12 @@ function isHeuristicEnabled() {
189
320
  function isLogicRulesEnabled() {
190
321
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
191
322
  }
192
- var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
323
+ var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
193
324
  var init_config = __esm({
194
325
  "src/config.ts"() {
195
326
  "use strict";
196
327
  init_esm_shims();
328
+ init_paths();
197
329
  llmConfig = (() => {
198
330
  const configPath = resolveConfigPath();
199
331
  const fileConfig = fs.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
@@ -211,6 +343,21 @@ var init_config = __esm({
211
343
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
212
344
  };
213
345
  })();
346
+ __configModuleDir = (() => {
347
+ try {
348
+ return path2.dirname(fileURLToPath2(import.meta.url));
349
+ } catch {
350
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
351
+ }
352
+ })();
353
+ USER_RULES_PATHS = [
354
+ path2.join(__configModuleDir, "rules", "user_rules.json"),
355
+ // 源在 ts/src/rules(一般不存在)
356
+ path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
357
+ // dist: dist/engine/../rules = dist/rules ✅
358
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
359
+ // dev src: ts/src/../../agent_moss
360
+ ];
214
361
  customRules = loadCustomRules();
215
362
  PROVIDER_HEADERS = {
216
363
  "openrouter": {
@@ -227,7 +374,17 @@ var init_config = __esm({
227
374
  // src/engine/inline-analyzer.ts
228
375
  function isLiteralTextCommand(command) {
229
376
  ECHO_CMD_RE.lastIndex = 0;
230
- return ECHO_CMD_RE.test(command);
377
+ if (!ECHO_CMD_RE.test(command)) return false;
378
+ if (command.includes(">")) {
379
+ let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
380
+ d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
381
+ d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
382
+ d = d.replace(/'[^']*'|"[^"]*"/g, "");
383
+ if (/>{1,2}\s*(?!&)\S/.test(d)) {
384
+ return false;
385
+ }
386
+ }
387
+ return true;
231
388
  }
232
389
  function extractInlineCode(command) {
233
390
  const parts = [];
@@ -304,7 +461,7 @@ function analyzeInlineScript(command) {
304
461
  riskType: "file_access"
305
462
  };
306
463
  }
307
- var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
464
+ var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SHELL_RC_FRAGMENTS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
308
465
  var init_inline_analyzer = __esm({
309
466
  "src/engine/inline-analyzer.ts"() {
310
467
  "use strict";
@@ -374,6 +531,13 @@ var init_inline_analyzer = __esm({
374
531
  // xargs bash / xargs sh
375
532
  /\|\s*xargs\s+(?:bash|sh|zsh)/gi
376
533
  ];
534
+ SHELL_RC_FRAGMENTS = [
535
+ ".bashrc",
536
+ ".zshrc",
537
+ ".profile",
538
+ ".bash_profile",
539
+ ".zprofile"
540
+ ];
377
541
  SENSITIVE_PATH_FRAGMENTS = [
378
542
  "/etc/shadow",
379
543
  "/etc/gshadow",
@@ -390,9 +554,10 @@ var init_inline_analyzer = __esm({
390
554
  "/dev/kmsg",
391
555
  ".ssh/id_rsa",
392
556
  ".ssh/id_ed25519",
393
- ".ssh/authorized_keys"
557
+ ".ssh/authorized_keys",
558
+ ...SHELL_RC_FRAGMENTS
394
559
  ];
395
- REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
560
+ REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
396
561
  DIR_TRAVERSAL_KEYWORDS = [
397
562
  // Python 目录遍历
398
563
  "os.listdir",
@@ -415,11 +580,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
415
580
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
416
581
  if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
417
582
  if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
583
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
584
+ const trimmed = seg.trim();
585
+ if (!trimmed) continue;
586
+ const parts = trimmed.split(/\s+/);
587
+ const fw = parts[0]?.toLowerCase() || "";
588
+ if (fw === "wget" || fw === "curl") {
589
+ const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
590
+ if (isStdout) continue;
591
+ 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);
592
+ if (isDevnull) continue;
593
+ if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
594
+ return true;
595
+ }
596
+ }
597
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
598
+ return true;
599
+ }
600
+ }
418
601
  if (actionDetail.includes(">")) {
419
- const detailWithoutDevnull = actionDetail.replace(DEVNULL_REDIRECT_RE, "");
420
- if (detailWithoutDevnull.includes(">")) {
421
- const firstWord = actionDetail.trim().split(/\s+/)[0]?.toLowerCase() || "";
422
- if (!READ_ONLY_COMMANDS.has(firstWord)) return true;
602
+ const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
603
+ const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
604
+ const detailUnquotedRedirect = detailWithoutDevnull.replace(
605
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
606
+ (_m, _q, p) => ">" + p
607
+ );
608
+ const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
609
+ if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
610
+ return true;
423
611
  }
424
612
  }
425
613
  return false;
@@ -443,27 +631,31 @@ function runLogicCheck(input) {
443
631
  if (rbwResult) return rbwResult;
444
632
  if (input.promptSession && input.promptSession.trim()) {
445
633
  if (!isExcludedType) {
446
- const promptLower = input.promptSession.toLowerCase();
447
- for (const pattern of INTENT_DEVIATION_PATTERNS) {
448
- const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
449
- const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
450
- if (intentMatched && dangerousMatched) {
451
- return {
452
- allowed: false,
453
- reason: pattern.reason,
454
- riskLevel: "high",
455
- riskType: "intent_deviation",
456
- confidence: 85,
457
- source: "logic_rule",
458
- violatedLayers: ["2"]
459
- };
634
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
635
+ } else {
636
+ const promptLower = input.promptSession.toLowerCase();
637
+ for (const pattern of INTENT_DEVIATION_PATTERNS) {
638
+ const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
639
+ const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
640
+ if (intentMatched && dangerousMatched) {
641
+ return {
642
+ allowed: false,
643
+ reason: pattern.reason,
644
+ riskLevel: "high",
645
+ riskType: "intent_deviation",
646
+ confidence: 85,
647
+ source: "logic_rule",
648
+ violatedLayers: ["2"]
649
+ };
650
+ }
460
651
  }
461
652
  }
462
653
  }
463
654
  }
464
655
  const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
465
656
  if (indirectResult) return indirectResult;
466
- if (actionDetail.includes("sudo") || actionDetail.includes("su -") || actionDetail.includes("chmod u+s") || actionDetail.includes("setuid")) {
657
+ if (INLINE_SCRIPT_RE.test(actionDetail)) {
658
+ } else if (containsPrivilegeEscalation(actionDetail)) {
467
659
  const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
468
660
  const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
469
661
  if (!isAdminCmd) {
@@ -493,6 +685,9 @@ function extractFilePaths(text) {
493
685
  const paths = [];
494
686
  const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
495
687
  if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
688
+ const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
689
+ const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
690
+ if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
496
691
  const quotedPaths = text.match(/["']\/([^"']+)["']/g);
497
692
  if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
498
693
  return [...new Set(paths)];
@@ -503,8 +698,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
503
698
  const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
504
699
  const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
505
700
  if (!isWrite) return null;
506
- const targetPaths = extractFilePaths(actionDetail);
507
- if (targetPaths.length === 0) return null;
701
+ const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
702
+ if (isBashLike) {
703
+ const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
704
+ const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
705
+ const detailUnquotedRedirect = detailNoDevnull.replace(
706
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
707
+ (_m, _q, p) => ">" + p
708
+ );
709
+ const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
710
+ const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
711
+ let hasWriteSeg = hasRedirectWrite;
712
+ if (!hasWriteSeg) {
713
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
714
+ const trimmed = seg.trim();
715
+ if (!trimmed) continue;
716
+ const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
717
+ if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
718
+ hasWriteSeg = true;
719
+ break;
720
+ }
721
+ if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
722
+ hasWriteSeg = true;
723
+ break;
724
+ }
725
+ if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
726
+ hasWriteSeg = true;
727
+ break;
728
+ }
729
+ }
730
+ }
731
+ if (!hasWriteSeg) return null;
732
+ }
733
+ const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
734
+ const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
735
+ const targetPaths = [];
736
+ for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
737
+ const trimmed = seg.trim();
738
+ if (!trimmed) continue;
739
+ const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
740
+ if (firstWord === "cd") {
741
+ continue;
742
+ }
743
+ if (READ_ONLY_COMMANDS.has(firstWord)) {
744
+ const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
745
+ const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
746
+ const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
747
+ const m = segNoQuotes.match(REDIRECT_TARGET_RE);
748
+ if (m) targetPaths.push(m[1] || m[2] || m[3]);
749
+ continue;
750
+ }
751
+ const segPaths = extractFilePaths(trimmed);
752
+ const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
753
+ if (SOURCE_ARG_CMDS.has(firstWord)) {
754
+ if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
755
+ } else if (firstWord === "dd") {
756
+ const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
757
+ const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
758
+ const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
759
+ const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
760
+ for (const sp of segPaths) {
761
+ if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
762
+ }
763
+ } else {
764
+ targetPaths.push(...segPathsFiltered);
765
+ }
766
+ }
767
+ const uniqueTargetPaths = [...new Set(targetPaths)];
768
+ if (uniqueTargetPaths.length === 0) return null;
508
769
  const historyReadPaths = /* @__PURE__ */ new Set();
509
770
  for (const action of actionHistory) {
510
771
  const histDetail = action.actionDetail.toLowerCase();
@@ -517,7 +778,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
517
778
  }
518
779
  }
519
780
  const unreadPaths = [];
520
- for (const path3 of targetPaths) {
781
+ for (const path3 of uniqueTargetPaths) {
521
782
  const hasRead = [...historyReadPaths].some(
522
783
  (rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
523
784
  );
@@ -580,6 +841,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
580
841
  violatedLayers: ["2"]
581
842
  };
582
843
  }
844
+ function containsPrivilegeEscalation(actionDetail) {
845
+ const ad = actionDetail.toLowerCase();
846
+ for (const seg of ad.split(/&&|\|\||;|\n/)) {
847
+ const trimmed = seg.trim();
848
+ if (!trimmed) continue;
849
+ const parts = trimmed.split(/\s+/);
850
+ const fw = parts[0]?.toLowerCase() || "";
851
+ if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
852
+ if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
853
+ }
854
+ return false;
855
+ }
583
856
  function checkIndirectFileAccess(actionType, actionDetail, cwd) {
584
857
  if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
585
858
  return null;
@@ -684,12 +957,13 @@ var init_logic_rules = __esm({
684
957
  { path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
685
958
  // 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
686
959
  { path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
687
- { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
960
+ { path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
688
961
  { path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
689
962
  // SSH 主机私钥
690
- { path: "/etc/ssh/ssh_host_rsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
691
- { path: "/etc/ssh/ssh_host_ed25519_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
692
- { path: "/etc/ssh/ssh_host_ecdsa_key", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
963
+ // SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
964
+ { path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
965
+ { path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
966
+ { path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
693
967
  ];
694
968
  HOME_SENSITIVE_PATHS = [
695
969
  { path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
@@ -730,7 +1004,7 @@ var init_logic_rules = __esm({
730
1004
  "cat"
731
1005
  ]);
732
1006
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
733
- WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362"];
1007
+ WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362", "edit"];
734
1008
  WRITE_COMMAND_RES = [
735
1009
  /\brm\b/,
736
1010
  /\bunlink\b/,
@@ -776,6 +1050,12 @@ var init_logic_rules = __esm({
776
1050
  "nl",
777
1051
  "tac",
778
1052
  "rev",
1053
+ // NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
1054
+ // read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
1055
+ // 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
1056
+ // echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
1057
+ "echo",
1058
+ "printf",
779
1059
  "lsblk",
780
1060
  "blockdev",
781
1061
  "smartctl",
@@ -828,7 +1108,7 @@ var init_logic_rules = __esm({
828
1108
  "modinfo",
829
1109
  "sysctl"
830
1110
  ]);
831
- DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/;
1111
+ DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
832
1112
  READ_KEYWORDS = ["read", "\u8BFB\u53D6", "cat", "head", "tail", "less", "more", "view", "\u67E5\u770B", "open", "\u6253\u5F00", "load", "\u52A0\u8F7D", "grep", "search", "\u641C\u7D22", "find", "\u67E5\u627E"];
833
1113
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
834
1114
  PASSWORD_MODIFY_PATTERNS = [
@@ -876,9 +1156,9 @@ var init_logic_rules = __esm({
876
1156
  });
877
1157
 
878
1158
  // src/engine/skill-loader.ts
879
- import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
1159
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
880
1160
  import { resolve, dirname } from "path";
881
- import { fileURLToPath as fileURLToPath2 } from "url";
1161
+ import { fileURLToPath as fileURLToPath3 } from "url";
882
1162
  function loadAllSkills() {
883
1163
  if (cachedSkills) return cachedSkills;
884
1164
  cachedSkills = {};
@@ -890,7 +1170,7 @@ function loadAllSkills() {
890
1170
  const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
891
1171
  for (const f of files) {
892
1172
  const name = f.replace(".md", "");
893
- cachedSkills[name] = readFileSync(resolve(dir, f), "utf-8");
1173
+ cachedSkills[name] = readFileSync2(resolve(dir, f), "utf-8");
894
1174
  }
895
1175
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedSkills).length} skills from ${dir}`);
896
1176
  return cachedSkills;
@@ -949,7 +1229,7 @@ var init_skill_loader = __esm({
949
1229
  "src/engine/skill-loader.ts"() {
950
1230
  "use strict";
951
1231
  init_esm_shims();
952
- __filename2 = fileURLToPath2(import.meta.url);
1232
+ __filename2 = fileURLToPath3(import.meta.url);
953
1233
  __dirname2 = dirname(__filename2);
954
1234
  SKILLS_DIR = resolve(__dirname2, "../skills");
955
1235
  SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
@@ -1021,11 +1301,13 @@ __export(runtime_config_exports, {
1021
1301
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1022
1302
  updateSkillEnabled: () => updateSkillEnabled
1023
1303
  });
1024
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1025
- import { join } from "path";
1026
- import { homedir } from "os";
1027
- import { fileURLToPath as fileURLToPath3 } from "url";
1304
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
1305
+ import { join as join2 } from "path";
1306
+ import { fileURLToPath as fileURLToPath4 } from "url";
1028
1307
  import { dirname as dirname2 } from "path";
1308
+ function runtimeDir() {
1309
+ return getRuntimeConfigDir();
1310
+ }
1029
1311
  function getRuntimeConfigPath() {
1030
1312
  const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
1031
1313
  if (envPath) return envPath;
@@ -1058,7 +1340,7 @@ function loadRuntimeConfig() {
1058
1340
  return seeded;
1059
1341
  }
1060
1342
  try {
1061
- const data = JSON.parse(readFileSync2(p, "utf-8"));
1343
+ const data = JSON.parse(readFileSync3(p, "utf-8"));
1062
1344
  const merged = mergeConfig(data, generateSourceDefaults());
1063
1345
  return {
1064
1346
  version: data.version || 1,
@@ -1128,7 +1410,7 @@ function hasRuntimeConfig() {
1128
1410
  const p = getRuntimeConfigPath();
1129
1411
  if (!existsSync3(p)) return false;
1130
1412
  try {
1131
- JSON.parse(readFileSync2(p, "utf-8"));
1413
+ JSON.parse(readFileSync3(p, "utf-8"));
1132
1414
  return true;
1133
1415
  } catch {
1134
1416
  return false;
@@ -1316,11 +1598,11 @@ function _agentRulesToDefaults() {
1316
1598
  return out;
1317
1599
  }
1318
1600
  for (const agentId of entries) {
1319
- const rulesFile = join(agentsDir, agentId, "rules.json");
1601
+ const rulesFile = join2(agentsDir, agentId, "rules.json");
1320
1602
  if (!existsSync3(rulesFile)) continue;
1321
1603
  let data;
1322
1604
  try {
1323
- data = JSON.parse(readFileSync2(rulesFile, "utf-8"));
1605
+ data = JSON.parse(readFileSync3(rulesFile, "utf-8"));
1324
1606
  } catch {
1325
1607
  continue;
1326
1608
  }
@@ -1736,12 +2018,12 @@ function deleteCustomRule(layer, category, ruleId) {
1736
2018
  function addCustomSkill(skillId, category, keywords, content) {
1737
2019
  _validateSkillId(skillId);
1738
2020
  const runtime = loadRuntimeConfig();
1739
- const skillsDir = join(RUNTIME_CONFIG_DIR, "agent_moss_skills");
2021
+ const skillsDir = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills");
1740
2022
  try {
1741
2023
  mkdirSync(skillsDir, { recursive: true });
1742
2024
  } catch {
1743
2025
  }
1744
- writeFileSync(join(skillsDir, `${skillId}.md`), content, "utf-8");
2026
+ writeFileSync(join2(skillsDir, `${skillId}.md`), content, "utf-8");
1745
2027
  const l3 = runtime.L3_skills || (runtime.L3_skills = {});
1746
2028
  const cat = l3[category] || (l3[category] = { category_enabled: true, category_desc: "", skills: [] });
1747
2029
  (cat.skills || (cat.skills = [])).push({ id: skillId, keywords, enabled: true, builtin: false });
@@ -1756,7 +2038,7 @@ function deleteCustomSkill(skillId, category) {
1756
2038
  if (idx >= 0) {
1757
2039
  if (skills[idx].builtin) return null;
1758
2040
  skills.splice(idx, 1);
1759
- const skillPath = join(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
2041
+ const skillPath = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
1760
2042
  try {
1761
2043
  if (existsSync3(skillPath)) unlinkSync(skillPath);
1762
2044
  } catch {
@@ -1872,18 +2154,19 @@ function updateAgentCategoryEnabled(agentId, category, enabled) {
1872
2154
  saveRuntimeConfig(runtime);
1873
2155
  return runtime;
1874
2156
  }
1875
- 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;
2157
+ 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;
1876
2158
  var init_runtime_config = __esm({
1877
2159
  "src/infra/runtime-config.ts"() {
1878
2160
  "use strict";
1879
2161
  init_esm_shims();
2162
+ init_paths();
1880
2163
  init_heuristic();
1881
2164
  init_logic_rules();
1882
2165
  init_skill_loader();
1883
- __dirname3 = dirname2(fileURLToPath3(import.meta.url));
1884
- RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
1885
- RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
2166
+ __dirname3 = dirname2(fileURLToPath4(import.meta.url));
1886
2167
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
2168
+ RUNTIME_CONFIG_DIR = runtimeDir();
2169
+ RUNTIME_CONFIG_PATH = join2(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
1887
2170
  LAYER_DISABLE_ENV = {
1888
2171
  L1_heuristic: "AGENT_MOSS_DISABLE_HEURISTIC",
1889
2172
  L2_logic_rules: "AGENT_MOSS_DISABLE_LOGIC_RULES",
@@ -1894,16 +2177,16 @@ var init_runtime_config = __esm({
1894
2177
  deny_read: "\u4EC5\u62E6\u622A\u8BFB\u53D6",
1895
2178
  deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
1896
2179
  };
1897
- _AGENTS_DIR_PROD = join(__dirname3, "agents");
1898
- _AGENTS_DIR_DEV = join(__dirname3, "..", "..", "..", "agent_moss", "agents");
2180
+ _AGENTS_DIR_PROD = join2(__dirname3, "agents");
2181
+ _AGENTS_DIR_DEV = join2(__dirname3, "..", "..", "..", "agent_moss", "agents");
1899
2182
  SKILL_ID_RE = /^[A-Za-z0-9_-]+$/;
1900
2183
  }
1901
2184
  });
1902
2185
 
1903
2186
  // src/agents/index.ts
1904
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2187
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1905
2188
  import { resolve as resolve2, dirname as dirname3 } from "path";
1906
- import { fileURLToPath as fileURLToPath4 } from "url";
2189
+ import { fileURLToPath as fileURLToPath5 } from "url";
1907
2190
  function resolveAgentsDir() {
1908
2191
  if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
1909
2192
  if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
@@ -1928,7 +2211,7 @@ function getAgentExtensions(agentId) {
1928
2211
  return null;
1929
2212
  }
1930
2213
  try {
1931
- const raw = readFileSync3(rulesFile, "utf-8");
2214
+ const raw = readFileSync4(rulesFile, "utf-8");
1932
2215
  ext = JSON.parse(raw);
1933
2216
  if (!ext.agent_id) {
1934
2217
  console.warn(`[AgentMoss agents] rules.json \u7F3A\u5C11 agent_id \u5B57\u6BB5: ${rulesFile}`);
@@ -1952,7 +2235,7 @@ var init_agents = __esm({
1952
2235
  "src/agents/index.ts"() {
1953
2236
  "use strict";
1954
2237
  init_esm_shims();
1955
- __filename3 = fileURLToPath4(import.meta.url);
2238
+ __filename3 = fileURLToPath5(import.meta.url);
1956
2239
  __dirname4 = dirname3(__filename3);
1957
2240
  AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
1958
2241
  AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
@@ -1972,19 +2255,25 @@ function runHeuristicCheck(input) {
1972
2255
  const re = new RegExp(rule.pattern);
1973
2256
  if (re.test(command)) {
1974
2257
  if (rule.action === "Deny") {
1975
- matched.push(`\u81EA\u5B9A\u4E49\u89C4\u5219: ${rule.pattern}`);
1976
- if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1977
- highestSeverity = rule.severity;
1978
- highestRiskType = "custom_rule";
1979
- }
2258
+ return {
2259
+ allowed: false,
2260
+ reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
2261
+ riskLevel: rule.severity,
2262
+ riskType: "sensitive_action",
2263
+ confidence: 90,
2264
+ source: "heuristic",
2265
+ violatedLayers: ["1.1"]
2266
+ };
1980
2267
  }
1981
2268
  }
1982
2269
  } catch {
1983
2270
  }
1984
2271
  }
2272
+ const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
2273
+ const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
1985
2274
  let topRiskType = "";
1986
2275
  for (const rule of DANGEROUS_PATTERNS) {
1987
- if (rule.pattern.test(command)) {
2276
+ if (rule.pattern.test(scanCommand)) {
1988
2277
  matched.push(rule.description);
1989
2278
  if (severityRank(rule.severity) > severityRank(highestSeverity)) {
1990
2279
  highestSeverity = rule.severity;
@@ -1998,7 +2287,7 @@ function runHeuristicCheck(input) {
1998
2287
  for (const rule of agentExt.extra_command_patterns) {
1999
2288
  try {
2000
2289
  const re = new RegExp(rule.pattern, "i");
2001
- if (re.test(command)) {
2290
+ if (re.test(scanCommand)) {
2002
2291
  matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
2003
2292
  if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
2004
2293
  highestSeverity = rule.risk_level;
@@ -2022,14 +2311,12 @@ function runHeuristicCheck(input) {
2022
2311
  const actionTypeL = input.aNext.actionType.toLowerCase();
2023
2312
  const detailL = input.aNext.actionDetail.toLowerCase();
2024
2313
  const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
2314
+ const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2315
+ if (dangerousResult) return dangerousResult;
2025
2316
  if (!isExcludedType) {
2026
2317
  const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
2027
2318
  if (pathResult) return pathResult;
2028
2319
  }
2029
- const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
2030
- if (dangerousResult) return dangerousResult;
2031
- const lateralResult = checkLateralMovement(detailL);
2032
- if (lateralResult) return lateralResult;
2033
2320
  if (matched.length > 0) {
2034
2321
  return {
2035
2322
  allowed: false,
@@ -2042,6 +2329,8 @@ function runHeuristicCheck(input) {
2042
2329
  // 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
2043
2330
  };
2044
2331
  }
2332
+ const lateralResult = checkLateralMovement(detailL);
2333
+ if (lateralResult) return lateralResult;
2045
2334
  return null;
2046
2335
  }
2047
2336
  function checkInjectionKeywords(text, extraKeywords) {
@@ -2064,6 +2353,68 @@ function isInlineScriptCommand(actionDetail) {
2064
2353
  const re = /(?:^|\s)(?:python|python3|python3\.\d+|perl|perl5|ruby|jruby|mruby|node|php|lua)\s+(?:-[a-zA-Z]*[cer][a-zA-Z]*\s+)/i;
2065
2354
  return re.test(actionDetail);
2066
2355
  }
2356
+ function stripLiteralTextSegments(actionDetail) {
2357
+ const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
2358
+ const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
2359
+ if (writeScriptExec || pipeToInterpreter) {
2360
+ return actionDetail;
2361
+ }
2362
+ const resultParts = [];
2363
+ for (const seg of splitCommandSegments(actionDetail)) {
2364
+ const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
2365
+ if (fw === "echo" || fw === "printf") {
2366
+ const segUnquotedRedirect = seg.replace(
2367
+ />{1,2}\s*(['"])([~./][^'"]+)\1/g,
2368
+ (_m, _q, p) => ">" + p
2369
+ );
2370
+ resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
2371
+ } else {
2372
+ resultParts.push(seg);
2373
+ }
2374
+ }
2375
+ return resultParts.join("");
2376
+ }
2377
+ function splitCommandSegments(actionDetail) {
2378
+ const segments = [];
2379
+ let cur = "";
2380
+ let quote = null;
2381
+ for (let i = 0; i < actionDetail.length; i++) {
2382
+ const ch = actionDetail[i];
2383
+ if (quote) {
2384
+ cur += ch;
2385
+ if (ch === quote) quote = null;
2386
+ continue;
2387
+ }
2388
+ if (ch === "'" || ch === '"') {
2389
+ quote = ch;
2390
+ cur += ch;
2391
+ continue;
2392
+ }
2393
+ if (ch === ";" || ch === "\n") {
2394
+ segments.push(cur);
2395
+ segments.push(ch);
2396
+ cur = "";
2397
+ continue;
2398
+ }
2399
+ if (ch === "&" && actionDetail[i + 1] === "&") {
2400
+ segments.push(cur);
2401
+ segments.push("&&");
2402
+ cur = "";
2403
+ i++;
2404
+ continue;
2405
+ }
2406
+ if (ch === "|" && actionDetail[i + 1] === "|") {
2407
+ segments.push(cur);
2408
+ segments.push("||");
2409
+ cur = "";
2410
+ i++;
2411
+ continue;
2412
+ }
2413
+ cur += ch;
2414
+ }
2415
+ if (cur) segments.push(cur);
2416
+ return segments;
2417
+ }
2067
2418
  function severityRank(s) {
2068
2419
  const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
2069
2420
  return ranks[s] || 0;
@@ -2071,13 +2422,18 @@ function severityRank(s) {
2071
2422
  function checkSensitivePath(command, cwd, agentExt) {
2072
2423
  const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
2073
2424
  const isWriteOpFlag = isWriteOperation("", command);
2425
+ const pathScanText = stripLiteralTextSegments(command);
2074
2426
  const pathMatches = (text, path3) => {
2075
- const escaped = path3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2427
+ const parts = path3.split(/(\(\?!.*?\))/);
2428
+ const escaped = parts.map((part) => {
2429
+ if (part.startsWith("(?!")) return part;
2430
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2431
+ }).join("");
2076
2432
  const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
2077
2433
  return re.test(text);
2078
2434
  };
2079
2435
  for (const sp of ROOT_SENSITIVE_PATHS) {
2080
- if (pathMatches(command, sp.path.toLowerCase())) {
2436
+ if (pathMatches(pathScanText, sp.path.toLowerCase())) {
2081
2437
  if (sp.path === "/dev/random") {
2082
2438
  if (isWriteOpFlag) {
2083
2439
  continue;
@@ -2111,7 +2467,7 @@ function checkSensitivePath(command, cwd, agentExt) {
2111
2467
  const expanded = `${expandedCwd}/${sp.path}`;
2112
2468
  const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2113
2469
  const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
2114
- if (pathRe.test(command) || expandedRe.test(command)) {
2470
+ if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
2115
2471
  if (!isWriteOpFlag && !sp.credential) {
2116
2472
  continue;
2117
2473
  }
@@ -2182,8 +2538,10 @@ function checkSensitivePath(command, cwd, agentExt) {
2182
2538
  }
2183
2539
  function checkDangerousPatterns(actionType, actionDetail) {
2184
2540
  if (actionType === "file_write" || actionType === "file_edit") return null;
2185
- const TEMP_DIRS = ["/tmp/", "/var/tmp/", "/run/", "/dev/shm/"];
2186
- const isTempPath = TEMP_DIRS.some((td) => actionDetail.includes(td));
2541
+ const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
2542
+ const isTempPath = TEMP_DIRS.some(
2543
+ (td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
2544
+ );
2187
2545
  if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
2188
2546
  return {
2189
2547
  allowed: false,
@@ -2195,7 +2553,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
2195
2553
  violatedLayers: ["1.1"]
2196
2554
  };
2197
2555
  }
2198
- if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail) || />\s*\/proc\//.test(actionDetail)) {
2556
+ const findWriteMatch = actionDetail.match(
2557
+ /\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
2558
+ );
2559
+ if (findWriteMatch) {
2560
+ const cmdHit = findWriteMatch[1] || "xargs";
2561
+ return {
2562
+ allowed: false,
2563
+ reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
2564
+ riskLevel: "high",
2565
+ riskType: "script_execution",
2566
+ confidence: 90,
2567
+ source: "heuristic",
2568
+ violatedLayers: ["1.1"]
2569
+ };
2570
+ }
2571
+ if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
2199
2572
  return {
2200
2573
  allowed: false,
2201
2574
  reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
@@ -2206,17 +2579,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
2206
2579
  violatedLayers: ["1.1"]
2207
2580
  };
2208
2581
  }
2582
+ if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
2583
+ return {
2584
+ allowed: false,
2585
+ reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
2586
+ riskLevel: "high",
2587
+ riskType: "file_access",
2588
+ confidence: 90,
2589
+ source: "heuristic",
2590
+ violatedLayers: ["1.1"]
2591
+ };
2592
+ }
2209
2593
  return null;
2210
2594
  }
2211
2595
  function extractRemoteHost(command) {
2212
- const m = command.match(/(?:ssh|scp|rsync)\s+(?:-[\w]+\s+)*(\S+@\S+|\b(?!-)[\w.-]+\b)/);
2213
- return m ? m[1] : null;
2596
+ const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
2597
+ for (const tok of tokens) {
2598
+ const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
2599
+ if (am && am[2] && !isLocalHost(am[2])) {
2600
+ return `${am[1]}@${am[2]}`;
2601
+ }
2602
+ }
2603
+ for (const tok of tokens) {
2604
+ const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
2605
+ if (hm && hm[1] && !isLocalHost(hm[1])) {
2606
+ return hm[1];
2607
+ }
2608
+ }
2609
+ const skipValueOpts = /* @__PURE__ */ new Set([
2610
+ "-p",
2611
+ "-l",
2612
+ "-i",
2613
+ "-o",
2614
+ "-b",
2615
+ "-c",
2616
+ "-m",
2617
+ "-F",
2618
+ "-e",
2619
+ "-S",
2620
+ "-W",
2621
+ "-J",
2622
+ "-I",
2623
+ "-T",
2624
+ "-B",
2625
+ "-L",
2626
+ "-R",
2627
+ "-D",
2628
+ "-E",
2629
+ "-M",
2630
+ "-O",
2631
+ "-P",
2632
+ "-q"
2633
+ ]);
2634
+ let foundCmd = false;
2635
+ for (let i = 0; i < tokens.length; i++) {
2636
+ const t = tokens[i];
2637
+ if (t === "ssh" || t === "scp" || t === "rsync") {
2638
+ foundCmd = true;
2639
+ continue;
2640
+ }
2641
+ if (!foundCmd) continue;
2642
+ if (t.startsWith("-")) {
2643
+ if (skipValueOpts.has(t) && i + 1 < tokens.length) {
2644
+ i++;
2645
+ }
2646
+ continue;
2647
+ }
2648
+ if (t.startsWith("/")) continue;
2649
+ return t;
2650
+ }
2651
+ return null;
2214
2652
  }
2215
2653
  function isLocalHost(host) {
2216
2654
  const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
2217
2655
  return localNames.includes(host.replace(/.*@/, ""));
2218
2656
  }
2219
2657
  function checkLateralMovement(actionDetail) {
2658
+ if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
2659
+ const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
2660
+ let hasRealCall = false;
2661
+ for (const seg of segments) {
2662
+ const parts = seg.split(/\s+/);
2663
+ const fw = (parts[0] || "").toLowerCase();
2664
+ if (fw === "echo" || fw === "printf") continue;
2665
+ if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
2666
+ hasRealCall = true;
2667
+ break;
2668
+ }
2669
+ }
2670
+ if (!hasRealCall) return null;
2220
2671
  if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
2221
2672
  const targetHost = extractRemoteHost(actionDetail);
2222
2673
  if (targetHost && !isLocalHost(targetHost)) {
@@ -2316,10 +2767,14 @@ var init_heuristic = __esm({
2316
2767
  { pattern: /mkfs\.\w+\s+\/dev\/(sd|hd|nvme|vd|loop|mapper|md|dm)\S+/i, severity: "critical", description: "\u683C\u5F0F\u5316\u5757\u8BBE\u5907 (mkfs /dev/\u5757\u8BBE\u5907)", riskType: "script_execution" },
2317
2768
  { pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
2318
2769
  // --- Critical: remote code execution ---
2319
- { pattern: /curl\s+.*\|\s*(bash|sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2770
+ // curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
2771
+ // 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
2772
+ // runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
2773
+ { pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
2774
+ { pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
2320
2775
  { pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
2321
2776
  { pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
2322
- { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "critical", description: "base64 \u89E3\u7801\u6267\u884C", riskType: "script_execution" },
2777
+ { pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
2323
2778
  // --- Critical: reverse shell ---
2324
2779
  { pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
2325
2780
  { pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
@@ -2330,13 +2785,25 @@ var init_heuristic = __esm({
2330
2785
  { pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
2331
2786
  // --- Critical: privilege escalation / container escape ---
2332
2787
  { pattern: /\/var\/run\/docker\.sock/i, severity: "critical", description: "\u8BBF\u95EE Docker socket\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5BB9\u5668\u9003\u9038", riskType: "privilege_escalation" },
2333
- // --- High: dangerous permissions / ownership ---
2334
- { pattern: /chmod\s+[0-7]*777[0-7]*/i, severity: "high", description: "\u5168\u6743\u9650\u8BBE\u7F6E (chmod 777)", riskType: "script_execution" },
2788
+ // --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
2789
+ // 修正「只拦 777、xx7 漏放」的缺口:
2790
+ // 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
2791
+ // 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
2792
+ // 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
2793
+ // 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
2794
+ // 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
2795
+ { 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" },
2796
+ { 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" },
2797
+ { pattern: /chmod\s+[ugoa]\+s\b/i, severity: "critical", description: "\u7B26\u53F7 setuid/setgid \u8BBE\u7F6E (chmod u+s/g+s/a+s, \u63D0\u6743)", riskType: "privilege_escalation" },
2335
2798
  { pattern: /chown\s+root|chown\s+-R\s+\w+:\w+\s+\//i, severity: "high", description: "\u4FEE\u6539\u6240\u6709\u8005\u4E3A root / \u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6240\u6709\u8005", riskType: "script_execution" },
2336
2799
  // sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
2337
2800
  { pattern: /sudo\s+(rm|chmod|chown|dd|mkfs|format|iptables|crontab|passwd|userdel|useradd|groupdel)\b/i, severity: "high", description: "\u63D0\u6743\u6267\u884C\u9AD8\u5371\u547D\u4EE4 (sudo + \u5371\u9669\u64CD\u4F5C)", riskType: "privilege_escalation" },
2338
2801
  // --- High: system config overwrite ---
2802
+ // > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
2803
+ // 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
2339
2804
  { pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
2805
+ { pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
2806
+ // 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
2340
2807
  // --- High: download script + execute risk ---
2341
2808
  { pattern: /(?:curl|wget)\s+.*(?:-o|--output|>)\s*\S+\.(?:sh|bash)\b/i, severity: "high", description: "\u8FDC\u7A0B\u4E0B\u8F7D\u811A\u672C\u5230\u672C\u5730", riskType: "script_execution" },
2342
2809
  { pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
@@ -2352,7 +2819,11 @@ var init_heuristic = __esm({
2352
2819
  // --- Medium: credential export ---
2353
2820
  { pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
2354
2821
  // --- High: shell rc file append (persistence) ---
2822
+ // 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
2823
+ // 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
2824
+ // /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
2355
2825
  { 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" },
2826
+ { pattern: />>\s*\S*\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
2356
2827
  // --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
2357
2828
  { pattern: />\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i, severity: "high", description: "\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6", riskType: "file_access" },
2358
2829
  // --- High: non-interactive password modification ---
@@ -2388,17 +2859,16 @@ __export(token_stats_exports, {
2388
2859
  recordTokenUsage: () => recordTokenUsage,
2389
2860
  resetTokenStats: () => resetTokenStats
2390
2861
  });
2391
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
2392
- import { join as join4 } from "path";
2393
- import { homedir as homedir3 } from "os";
2862
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
2394
2863
  function getStatsPath() {
2395
2864
  const envPath = process.env[ENV_STATS_PATH];
2396
- return envPath || STATS_FILE;
2865
+ if (envPath) return envPath;
2866
+ return getTokenStatsPath();
2397
2867
  }
2398
2868
  function loadAllRecords(statsPath) {
2399
2869
  if (!existsSync7(statsPath)) return [];
2400
2870
  try {
2401
- const data = JSON.parse(readFileSync7(statsPath, "utf-8"));
2871
+ const data = JSON.parse(readFileSync8(statsPath, "utf-8"));
2402
2872
  if (Array.isArray(data)) return data;
2403
2873
  if (data && Array.isArray(data.records)) return data.records;
2404
2874
  return [];
@@ -2538,13 +3008,12 @@ function resetTokenStats() {
2538
3008
  }
2539
3009
  }
2540
3010
  }
2541
- var STATS_DIR, STATS_FILE, ENV_STATS_PATH, MAX_RECORDS;
3011
+ var ENV_STATS_PATH, MAX_RECORDS;
2542
3012
  var init_token_stats = __esm({
2543
3013
  "src/engine/token-stats.ts"() {
2544
3014
  "use strict";
2545
3015
  init_esm_shims();
2546
- STATS_DIR = join4(homedir3(), ".config", "agentmoss");
2547
- STATS_FILE = join4(STATS_DIR, "agent_moss_token_stats.json");
3016
+ init_paths();
2548
3017
  ENV_STATS_PATH = "AGENT_MOSS_TOKEN_STATS_PATH";
2549
3018
  MAX_RECORDS = 1e4;
2550
3019
  }
@@ -2632,7 +3101,7 @@ var require_bindings = __commonJS({
2632
3101
  init_esm_shims();
2633
3102
  var fs4 = __require("fs");
2634
3103
  var path3 = __require("path");
2635
- var fileURLToPath7 = require_file_uri_to_path();
3104
+ var fileURLToPath8 = require_file_uri_to_path();
2636
3105
  var join6 = path3.join;
2637
3106
  var dirname7 = path3.dirname;
2638
3107
  var exists = fs4.accessSync && function(path4) {
@@ -2744,7 +3213,7 @@ var require_bindings = __commonJS({
2744
3213
  Error.stackTraceLimit = origSTL;
2745
3214
  var fileSchema = "file://";
2746
3215
  if (fileName.indexOf(fileSchema) === 0) {
2747
- fileName = fileURLToPath7(fileName);
3216
+ fileName = fileURLToPath8(fileName);
2748
3217
  }
2749
3218
  return fileName;
2750
3219
  };
@@ -3409,10 +3878,10 @@ init_skill_loader();
3409
3878
 
3410
3879
  // src/engine/template-loader.ts
3411
3880
  init_esm_shims();
3412
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
3881
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
3413
3882
  import { resolve as resolve3, dirname as dirname4 } from "path";
3414
- import { fileURLToPath as fileURLToPath5 } from "url";
3415
- var __filename4 = fileURLToPath5(import.meta.url);
3883
+ import { fileURLToPath as fileURLToPath6 } from "url";
3884
+ var __filename4 = fileURLToPath6(import.meta.url);
3416
3885
  var __dirname5 = dirname4(__filename4);
3417
3886
  var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
3418
3887
  var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
@@ -3430,7 +3899,7 @@ function loadTemplates() {
3430
3899
  const filePath = resolve3(dir, file);
3431
3900
  if (existsSync5(filePath)) {
3432
3901
  const key = file.replace(".txt", "").replace("_template", "");
3433
- cachedTemplates[key] = readFileSync4(filePath, "utf-8");
3902
+ cachedTemplates[key] = readFileSync5(filePath, "utf-8");
3434
3903
  }
3435
3904
  }
3436
3905
  console.log(`[AgentMoss] Loaded ${Object.keys(cachedTemplates).length} templates from ${dir}`);
@@ -3439,8 +3908,8 @@ function loadTemplates() {
3439
3908
 
3440
3909
  // src/engine/script-analyzer.ts
3441
3910
  init_esm_shims();
3442
- import { readFileSync as readFileSync5 } from "fs";
3443
- import { resolve as resolve4, join as join2 } from "path";
3911
+ import { readFileSync as readFileSync6 } from "fs";
3912
+ import { resolve as resolve4, join as join3 } from "path";
3444
3913
  var SUSPICIOUS_PATTERNS = [
3445
3914
  // Network exfiltration (curl/wget POST in scripts is almost never legitimate)
3446
3915
  { name: "curl POST", pattern: /curl\s+(.*\s)?-X\s+POST/i },
@@ -3573,8 +4042,8 @@ function assessKeywordCombinationRisk(matchedKeywords, scriptContent) {
3573
4042
  }
3574
4043
  function readScriptContent(scriptPath, maxLines = 500) {
3575
4044
  try {
3576
- const expandedPath = scriptPath.startsWith("~") ? join2(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
3577
- const raw = readFileSync5(expandedPath, "utf-8");
4045
+ const expandedPath = scriptPath.startsWith("~") ? join3(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
4046
+ const raw = readFileSync6(expandedPath, "utf-8");
3578
4047
  const lines = raw.split("\n").slice(0, maxLines);
3579
4048
  return { content: lines.join("\n"), lineCount: lines.length, error: "" };
3580
4049
  } catch (e) {
@@ -3628,8 +4097,8 @@ ${result.scriptContent}
3628
4097
  init_esm_shims();
3629
4098
  init_heuristic();
3630
4099
  init_logic_rules();
3631
- import { readFileSync as readFileSync6, statSync, existsSync as existsSync6 } from "fs";
3632
- import { resolve as resolve5, join as join3, isAbsolute, dirname as dirname5 } from "path";
4100
+ import { readFileSync as readFileSync7, statSync, existsSync as existsSync6 } from "fs";
4101
+ import { resolve as resolve5, join as join4, isAbsolute, dirname as dirname5 } from "path";
3633
4102
  import { homedir as homedir2 } from "os";
3634
4103
  function emptyChain() {
3635
4104
  return {
@@ -3870,7 +4339,7 @@ function extractInlineScriptPath(actionDetail) {
3870
4339
  function resolvePath(p) {
3871
4340
  let expanded = p;
3872
4341
  if (expanded.startsWith("~/") || expanded === "~") {
3873
- expanded = join3(homedir2(), expanded.slice(1));
4342
+ expanded = join4(homedir2(), expanded.slice(1));
3874
4343
  }
3875
4344
  return resolve5(expanded);
3876
4345
  }
@@ -3883,7 +4352,7 @@ function readScriptContent2(scriptPath, maxFileSize = 2 * 1024 * 1024) {
3883
4352
  if (stat.size > maxFileSize) {
3884
4353
  return { content: "", lineCount: 0, fileSize: stat.size, error: `\u811A\u672C\u6587\u4EF6\u8FC7\u5927 (${stat.size} bytes)\uFF0C\u8DF3\u8FC7\u8BFB\u53D6` };
3885
4354
  }
3886
- const content = readFileSync6(path3, "utf-8");
4355
+ const content = readFileSync7(path3, "utf-8");
3887
4356
  const lineCount = content ? content.split("\n").length : 0;
3888
4357
  return { content, lineCount, fileSize: stat.size, error: "" };
3889
4358
  } catch (e) {
@@ -3900,7 +4369,7 @@ function extractChildScriptPaths(content, baseDir) {
3900
4369
  const pathStr = m[1];
3901
4370
  if (pathStr.startsWith("-") || pathStr.startsWith("$")) continue;
3902
4371
  if (pathStr.includes("/dev/") || pathStr.startsWith("|") || pathStr.startsWith(";")) continue;
3903
- const full = isAbsolute(pathStr) ? pathStr : join3(baseDir, pathStr);
4372
+ const full = isAbsolute(pathStr) ? pathStr : join4(baseDir, pathStr);
3904
4373
  const resolved = resolvePath(full);
3905
4374
  if (existsSync6(resolved)) {
3906
4375
  try {
@@ -4327,11 +4796,13 @@ async function analyzeWithLLM(prompt, config) {
4327
4796
  } catch {
4328
4797
  }
4329
4798
  clearTimeout(totalTimeoutTimer);
4799
+ const riskLevel = parsed.riskLevel ?? parsed.risk_level;
4800
+ const riskType = parsed.riskType ?? parsed.risk_type;
4330
4801
  return {
4331
4802
  allowed: parsed.allowed !== false,
4332
4803
  reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
4333
- riskLevel: ["low", "medium", "high", "critical"].includes(parsed.riskLevel) ? parsed.riskLevel : "low",
4334
- riskType: parsed.riskType || "",
4804
+ riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
4805
+ riskType: riskType || "",
4335
4806
  confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
4336
4807
  source: "llm",
4337
4808
  violatedLayers: ["3"]
@@ -4449,7 +4920,10 @@ async function runAnalysis(input) {
4449
4920
  if (heuristicResult && !heuristicResult.allowed) {
4450
4921
  const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
4451
4922
  if (!skipInlineFileAccess) {
4452
- return heuristicResult;
4923
+ const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
4924
+ if (shouldDenyL1) {
4925
+ return heuristicResult;
4926
+ }
4453
4927
  }
4454
4928
  }
4455
4929
  let scriptChain = null;
@@ -4527,7 +5001,8 @@ async function runAnalysis(input) {
4527
5001
  ) && DANGEROUS_PATTERNS.some(
4528
5002
  (p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
4529
5003
  );
4530
- if (!fullCmdHit) {
5004
+ const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
5005
+ if (!fullCmdHit && !hasRedirectWrite) {
4531
5006
  return {
4532
5007
  allowed: true,
4533
5008
  reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
@@ -4852,10 +5327,10 @@ init_esm_shims();
4852
5327
  init_runtime_config();
4853
5328
  init_token_stats();
4854
5329
  import { Hono } from "hono";
4855
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
5330
+ import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
4856
5331
  import { join as join5, dirname as dirname6 } from "path";
4857
- import { fileURLToPath as fileURLToPath6 } from "url";
4858
- var __dirname6 = dirname6(fileURLToPath6(import.meta.url));
5332
+ import { fileURLToPath as fileURLToPath7 } from "url";
5333
+ var __dirname6 = dirname6(fileURLToPath7(import.meta.url));
4859
5334
  var STATIC_DIR = join5(__dirname6, "static");
4860
5335
  var CONSOLE_TOKEN_ENV = "AGENT_MOSS_CONSOLE_TOKEN";
4861
5336
  function getConsoleToken() {
@@ -4882,7 +5357,7 @@ function createConsoleRouter() {
4882
5357
  const serveIndex = (c) => {
4883
5358
  const indexFile = join5(STATIC_DIR, "index.html");
4884
5359
  if (existsSync9(indexFile)) {
4885
- return c.html(readFileSync8(indexFile, "utf-8"));
5360
+ return c.html(readFileSync9(indexFile, "utf-8"));
4886
5361
  }
4887
5362
  return c.html("<h1>AgentMoss Policy Console</h1><p>Frontend not built. Use /console/api/* endpoints.</p>");
4888
5363
  };
@@ -4890,12 +5365,12 @@ function createConsoleRouter() {
4890
5365
  console2.get("", serveIndex);
4891
5366
  console2.get("/api/layers", (c) => {
4892
5367
  const rc = loadRuntimeConfig();
4893
- return c.json({ layers: rc.layers });
5368
+ return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
4894
5369
  });
4895
5370
  console2.put("/api/layers", async (c) => {
4896
5371
  const body = await c.req.json();
4897
5372
  const rc = updateLayerEnabled(body.layer_key, body.enabled);
4898
- return c.json({ layers: rc.layers });
5373
+ return c.json({ layers: rc.layers, env_overrides: getEnvOverrides() });
4899
5374
  });
4900
5375
  console2.get("/api/rules", (c) => {
4901
5376
  const rc = loadRuntimeConfig();
@@ -4904,22 +5379,22 @@ function createConsoleRouter() {
4904
5379
  console2.put("/api/rules/enabled", async (c) => {
4905
5380
  const b = await c.req.json();
4906
5381
  const rc = updateRuleEnabled(b.layer, b.category, b.rule_id, b.enabled);
4907
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5382
+ return c.json(rc);
4908
5383
  });
4909
5384
  console2.put("/api/rules/deny_mode", async (c) => {
4910
5385
  const b = await c.req.json();
4911
5386
  const rc = updateRuleDenyMode(b.layer, b.category, b.rule_id, b.deny_mode);
4912
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5387
+ return c.json(rc);
4913
5388
  });
4914
5389
  console2.put("/api/rules/skip_l3", async (c) => {
4915
5390
  const b = await c.req.json();
4916
5391
  const rc = updateRuleSkipL3(b.layer, b.category, b.rule_id, b.skip_l3_on_disabled);
4917
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5392
+ return c.json(rc);
4918
5393
  });
4919
5394
  console2.put("/api/categories/enabled", async (c) => {
4920
5395
  const b = await c.req.json();
4921
5396
  const rc = updateCategoryEnabled(b.layer, b.category, b.enabled);
4922
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5397
+ return c.json(rc);
4923
5398
  });
4924
5399
  console2.post("/api/rules", async (c) => {
4925
5400
  const b = await c.req.json();
@@ -4935,38 +5410,38 @@ function createConsoleRouter() {
4935
5410
  credential: b.credential,
4936
5411
  deny_mode: b.deny_mode
4937
5412
  });
4938
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5413
+ return c.json(rc);
4939
5414
  });
4940
5415
  console2.delete("/api/rules", async (c) => {
4941
5416
  const b = await c.req.json();
4942
5417
  deleteCustomRule(b.layer, b.category, b.rule_id);
4943
5418
  const rc = loadRuntimeConfig();
4944
- return c.json({ L1_rules: rc.L1_rules, L2_rules: rc.L2_rules });
5419
+ return c.json(rc);
4945
5420
  });
4946
5421
  console2.get("/api/skills", (c) => {
4947
5422
  const rc = loadRuntimeConfig();
4948
- return c.json({ L3_skills: rc.L3_skills });
5423
+ return c.json(rc.L3_skills);
4949
5424
  });
4950
5425
  console2.put("/api/skills/enabled", async (c) => {
4951
5426
  const b = await c.req.json();
4952
5427
  const rc = updateSkillEnabled(b.skill_id, b.enabled);
4953
- return c.json({ L3_skills: rc.L3_skills });
5428
+ return c.json(rc);
4954
5429
  });
4955
5430
  console2.put("/api/skill-categories/enabled", async (c) => {
4956
5431
  const b = await c.req.json();
4957
5432
  const rc = updateSkillCategoryEnabled(b.category, b.enabled);
4958
- return c.json({ L3_skills: rc.L3_skills });
5433
+ return c.json(rc);
4959
5434
  });
4960
5435
  console2.post("/api/skills", async (c) => {
4961
5436
  const b = await c.req.json();
4962
5437
  const rc = addCustomSkill(b.skill_id, b.category || "user_custom", b.keywords || [], b.content);
4963
- return c.json({ L3_skills: rc.L3_skills });
5438
+ return c.json(rc);
4964
5439
  });
4965
5440
  console2.delete("/api/skills", async (c) => {
4966
5441
  const b = await c.req.json();
4967
5442
  deleteCustomSkill(b.skill_id, b.category);
4968
5443
  const rc = loadRuntimeConfig();
4969
- return c.json({ L3_skills: rc.L3_skills });
5444
+ return c.json(rc);
4970
5445
  });
4971
5446
  console2.get("/api/skills/:skill_id/content", async (c) => {
4972
5447
  const skillId = c.req.param("skill_id");
@@ -4976,10 +5451,10 @@ function createConsoleRouter() {
4976
5451
  const skillPath = join5(dirname6(getRuntimeConfigPath()), "agent_moss_skills", `${skillId}.md`);
4977
5452
  if (!existsSync9(skillPath)) {
4978
5453
  const builtinPath = join5(__dirname6, "..", "..", "agent_moss", "skills", `${skillId}.md`);
4979
- if (existsSync9(builtinPath)) return c.text(readFileSync8(builtinPath, "utf-8"));
5454
+ if (existsSync9(builtinPath)) return c.text(readFileSync9(builtinPath, "utf-8"));
4980
5455
  return c.json({ error: "skill not found" }, 404);
4981
5456
  }
4982
- return c.text(readFileSync8(skillPath, "utf-8"));
5457
+ return c.text(readFileSync9(skillPath, "utf-8"));
4983
5458
  });
4984
5459
  console2.get("/api/agents", (c) => {
4985
5460
  const rc = loadRuntimeConfig();
@@ -5021,7 +5496,7 @@ function createConsoleRouter() {
5021
5496
  });
5022
5497
  console2.get("/api/config", (c) => {
5023
5498
  const rc = loadRuntimeConfig();
5024
- return c.json({ config: rc, config_path: getRuntimeConfigPath() });
5499
+ return c.json(rc);
5025
5500
  });
5026
5501
  console2.get("/api/env-overrides", (c) => {
5027
5502
  return c.json({ overrides: getEnvOverrides() });
@@ -5029,7 +5504,7 @@ function createConsoleRouter() {
5029
5504
  console2.post("/api/reset", (c) => {
5030
5505
  const defaults = generateSourceDefaults();
5031
5506
  saveRuntimeConfig(defaults);
5032
- return c.json({ config: defaults });
5507
+ return c.json(defaults);
5033
5508
  });
5034
5509
  console2.get("/api/token-stats", (c) => {
5035
5510
  const days = parseInt(c.req.query("days") || "7", 10);
@@ -5037,7 +5512,7 @@ function createConsoleRouter() {
5037
5512
  });
5038
5513
  console2.get("/api/token-stats/recent", (c) => {
5039
5514
  const limit = parseInt(c.req.query("limit") || "20", 10);
5040
- return c.json({ records: getRecentRecords(limit) });
5515
+ return c.json(getRecentRecords(limit));
5041
5516
  });
5042
5517
  console2.get("/api/token-stats/trend", (c) => {
5043
5518
  const days = parseInt(c.req.query("days") || "7", 10);