@kenhkl/agent-moss 0.16.4 → 0.16.6

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.
@@ -110,18 +110,41 @@ function hasCommandSubstitution(text) {
110
110
  return CMD_SUBSTITUTION.test(text);
111
111
  }
112
112
  function splitCommandSegments(command) {
113
- return command.split(CMD_SEPARATORS).map((s) => s.trim()).filter((s) => s.length > 0);
113
+ const segments = [];
114
+ let cur = "";
115
+ let quote = null;
116
+ for (let i = 0; i < command.length; i++) {
117
+ const ch = command[i];
118
+ if (quote) {
119
+ cur += ch;
120
+ if (ch === quote) quote = null;
121
+ continue;
122
+ }
123
+ if (ch === "'" || ch === '"') {
124
+ quote = ch;
125
+ cur += ch;
126
+ continue;
127
+ }
128
+ if (/\||&|;|\n/.test(ch)) {
129
+ segments.push(cur);
130
+ segments.push(ch);
131
+ cur = "";
132
+ continue;
133
+ }
134
+ cur += ch;
135
+ }
136
+ if (cur) segments.push(cur);
137
+ return segments.map((s) => s.trim()).filter((s) => s.length > 0 && !/^(?:\|\||&&|[|;&])$/.test(s));
114
138
  }
115
139
  function extractFirstCommand(command) {
116
140
  const segments = splitCommandSegments(command);
117
141
  return segments.length ? segments[0] : "";
118
142
  }
119
- var CMD_SEPARATORS, CMD_SUBSTITUTION;
143
+ var CMD_SUBSTITUTION;
120
144
  var init_shell_split = __esm({
121
145
  "src/engine/shell-split.ts"() {
122
146
  "use strict";
123
147
  init_esm_shims();
124
- CMD_SEPARATORS = /\|\||&&|[|;&\n]/;
125
148
  CMD_SUBSTITUTION = /\$\(|`|</;
126
149
  }
127
150
  });
@@ -372,20 +395,25 @@ var init_runtime_config_bridge = __esm({
372
395
  });
373
396
 
374
397
  // src/infra/rules-loader.ts
375
- import { readFileSync as readFileSync2, existsSync } from "fs";
376
- import { resolve, dirname } from "path";
398
+ import { readFileSync as readFileSync2, existsSync, readdirSync, statSync } from "fs";
399
+ import { resolve, dirname, join as join2 } from "path";
377
400
  import { fileURLToPath as fileURLToPath2 } from "url";
378
401
  function dataRootDir() {
379
402
  return getDataRoot();
380
403
  }
381
- function overrideRulesDir() {
382
- return resolve(dataRootDir(), "rules");
404
+ function parseDirEnv(envName) {
405
+ const raw = (process.env[envName] || "").trim();
406
+ if (!raw) return [];
407
+ return raw.split(/[,;]/).map((s) => s.trim()).filter(Boolean).map((s) => resolve(s));
408
+ }
409
+ function overrideRulesDirs() {
410
+ return [...parseDirEnv("AGENT_MOSS_OVERRIDE_RULES_DIR"), resolve(dataRootDir(), "rules")];
383
411
  }
384
412
  function warnOverride(name, path5, data) {
385
413
  if (warnedOverride.has(path5)) return;
386
414
  warnedOverride.add(path5);
387
415
  console.warn(
388
- `[AgentMoss] \u89C4\u5219\u6587\u4EF6\u88AB\u7528\u6237\u6570\u636E\u76EE\u5F55\u8986\u76D6: ${path5}\uFF08\u5185\u7F6E\u9ED8\u8BA4\u88AB\u5FFD\u7565\uFF09\u3002\u8BE5\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4 \u2014\u2014 **\u5185\u7F6E\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4E0A\u9762**\uFF1B\u5220\u9664\u8BE5\u6587\u4EF6\u5373\u6062\u590D\u5185\u7F6E\u89C4\u5219\u3002\u82E5\u53EA\u60F3\u9010\u6761\u542F\u505C/\u589E\u5220\uFF0C\u8BF7\u6539\u7528 runtime config\uFF08Console\uFF09\u3002`
416
+ `[AgentMoss] \u89C4\u5219\u6587\u4EF6\u88AB\u8986\u76D6\u76EE\u5F55\u8986\u76D6: ${path5}\uFF08\u5185\u7F6E\u9ED8\u8BA4\u88AB\u5FFD\u7565\uFF09\u3002\u8BE5\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4 \u2014\u2014 **\u5185\u7F6E\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4E0A\u9762**\uFF1B\u5220\u9664\u8BE5\u6587\u4EF6\uFF08\u6216\u6E05\u7A7A AGENT_MOSS_OVERRIDE_RULES_DIR\uFF09\u5373\u6062\u590D\u5185\u7F6E\u89C4\u5219\u3002\u82E5\u53EA\u60F3\u9010\u6761\u542F\u505C/\u589E\u5220\uFF0C\u8BF7\u6539\u7528 runtime config\uFF08Console\uFF09\u3002`
389
417
  );
390
418
  const missing = (EXPECTED_KEYS[name] || []).filter((k) => !(k in data));
391
419
  if (missing.length > 0) {
@@ -395,18 +423,23 @@ function warnOverride(name, path5, data) {
395
423
  }
396
424
  }
397
425
  function findRulesFile(name) {
398
- const override = resolve(overrideRulesDir(), name);
399
- if (existsSync(override)) return override;
426
+ for (const dir of overrideRulesDirs()) {
427
+ const p = resolve(dir, name);
428
+ if (existsSync(p)) return p;
429
+ }
400
430
  for (const dir of RULES_DIR_CANDS) {
401
431
  const p = resolve(dir, name);
402
432
  if (existsSync(p)) return p;
403
433
  }
404
434
  return null;
405
435
  }
436
+ function isOverridePath(name, p) {
437
+ return overrideRulesDirs().some((dir) => resolve(dir, name) === p);
438
+ }
406
439
  function loadJson(name) {
407
440
  const p = findRulesFile(name);
408
441
  if (!p) return {};
409
- const isOverride = p === resolve(overrideRulesDir(), name);
442
+ const isOverride = isOverridePath(name, p);
410
443
  let data;
411
444
  try {
412
445
  data = JSON.parse(readFileSync2(p, "utf-8")) || {};
@@ -417,6 +450,14 @@ function loadJson(name) {
417
450
  if (isOverride) warnOverride(name, p, data);
418
451
  return data;
419
452
  }
453
+ function resolveScenarioRulesFile(scenarioId) {
454
+ const rel = join2("scenarios", scenarioId, "rules.json");
455
+ for (const dir of overrideRulesDirs()) {
456
+ const p = resolve(dir, rel);
457
+ if (existsSync(p)) return p;
458
+ }
459
+ return null;
460
+ }
420
461
  function loadL1() {
421
462
  return loadJson("l1_rules.json");
422
463
  }
@@ -449,7 +490,138 @@ function loadMC() {
449
490
  function loadToolRegistry() {
450
491
  return loadJson("tool_registry.json");
451
492
  }
452
- var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS;
493
+ function extraRulesDirs() {
494
+ return parseDirEnv("AGENT_MOSS_EXTRA_RULES_DIR");
495
+ }
496
+ function listJsonFilesDeep(dir) {
497
+ const out = [];
498
+ let entries;
499
+ try {
500
+ entries = readdirSync(dir, { withFileTypes: true }).map((d) => d.name);
501
+ } catch {
502
+ return out;
503
+ }
504
+ for (const name of entries.sort()) {
505
+ const p = resolve(dir, name);
506
+ let isDir = false;
507
+ try {
508
+ isDir = statSync(p).isDirectory();
509
+ } catch {
510
+ continue;
511
+ }
512
+ if (isDir) out.push(...listJsonFilesDeep(p));
513
+ else if (name.toLowerCase().endsWith(".json")) out.push(p);
514
+ }
515
+ return out;
516
+ }
517
+ function scanExtraFiles(files) {
518
+ const data = {};
519
+ const loaded = [];
520
+ const seenIds = /* @__PURE__ */ new Map();
521
+ const classifyFlat = (r) => {
522
+ for (const [keys, cat] of EXTRA_FLAT_FIELD) {
523
+ if (r && typeof r === "object" && keys.every((k) => r[k])) return cat;
524
+ }
525
+ return null;
526
+ };
527
+ for (const p of files) {
528
+ let parsed;
529
+ try {
530
+ parsed = JSON.parse(readFileSync2(p, "utf-8"));
531
+ } catch (e) {
532
+ console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6\u635F\u574F/\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u8DF3\u8FC7: ${p}\uFF08${e.message}\uFF09`);
533
+ continue;
534
+ }
535
+ let pairs = [];
536
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
537
+ pairs = Object.entries(parsed);
538
+ } else if (Array.isArray(parsed)) {
539
+ let flatBad = 0;
540
+ for (const r of parsed) {
541
+ const cat = classifyFlat(r);
542
+ if (cat === null) {
543
+ flatBad += 1;
544
+ continue;
545
+ }
546
+ pairs.push([cat, [r]]);
547
+ }
548
+ if (flatBad > 0) {
549
+ console.warn(
550
+ `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u6709 ${flatBad} \u6761\u5E73\u94FA\u6761\u76EE\u65E0\u6CD5\u5F52\u7C7B\uFF08\u88F8 pattern \u4E0D\u5E26 risk_level/risk_type \u65F6\u533A\u5206\u4E0D\u4E86 L1 \u5371\u9669\u547D\u4EE4\u4E0E L2 \u6388\u6743\u7C7B\uFF09\u2014\u2014 \u8BF7\u6539\u7528\u5BF9\u8C61\u5F62\u5F0F\uFF1A{"\u7C7B\u522B\u952E": [\u6761\u76EE...]}\u3002`
551
+ );
552
+ }
553
+ } else {
554
+ console.warn(
555
+ `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u9876\u5C42\u5E94\u662F\u5BF9\u8C61\uFF08\u7C7B\u522B\u952E\u2192\u6761\u76EE\u6570\u7EC4\uFF09\u6216\u6570\u7EC4\uFF0C\u5DF2\u8DF3\u8FC7\u3002`
556
+ );
557
+ continue;
558
+ }
559
+ let nLoaded = 0;
560
+ for (const [cat, rules] of pairs) {
561
+ const required = EXTRA_CATEGORIES[cat];
562
+ if (!required) {
563
+ console.warn(
564
+ `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u542B\u672A\u77E5\u7C7B\u522B\u952E "${cat}"\uFF08\u53EF\u7528\uFF1A${Object.keys(EXTRA_CATEGORIES).join(", ")}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u7C7B\u522B\u3002`
565
+ );
566
+ continue;
567
+ }
568
+ if (!Array.isArray(rules)) {
569
+ console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u7C7B\u522B ${cat} \u7684\u503C\u5E94\u662F\u6570\u7EC4 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
570
+ continue;
571
+ }
572
+ for (const r of rules) {
573
+ if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
574
+ console.warn(
575
+ `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u7C7B\u522B ${cat} \u6709\u6761\u76EE\u7F3A\u5FC5\u9700\u5B57\u6BB5 [${required.join(", ")}] \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`
576
+ );
577
+ continue;
578
+ }
579
+ const rid = r && typeof r === "object" ? String(r.id || "") : "";
580
+ if (rid) {
581
+ if (seenIds.has(rid)) {
582
+ console.warn(
583
+ `[AgentMoss] \u8FFD\u52A0\u89C4\u5219 id "${rid}" \u91CD\u590D\uFF08\u5148\u89C1\u4E8E ${seenIds.get(rid)}\uFF0C\u672C\u6761\u6765\u81EA ${p}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u672C\u6761\u3002`
584
+ );
585
+ continue;
586
+ }
587
+ seenIds.set(rid, p);
588
+ }
589
+ (data[cat] = data[cat] || []).push(r);
590
+ nLoaded += 1;
591
+ }
592
+ }
593
+ if (nLoaded > 0) {
594
+ loaded.push(p);
595
+ console.info(`[AgentMoss] \u5DF2\u52A0\u8F7D\u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p}\uFF1A${nLoaded} \u6761`);
596
+ }
597
+ }
598
+ return { data, loaded };
599
+ }
600
+ function loadExtraRules() {
601
+ const raw = (process.env.AGENT_MOSS_EXTRA_RULES_DIR || "").trim();
602
+ const dirs = extraRulesDirs();
603
+ let files = [];
604
+ for (const d of dirs) {
605
+ if (existsSync(d)) files.push(...listJsonFilesDeep(d));
606
+ else console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${d}\uFF08AGENT_MOSS_EXTRA_RULES_DIR\uFF09`);
607
+ }
608
+ files = files.sort();
609
+ const fp = files.map((p) => {
610
+ try {
611
+ const st = statSync(p);
612
+ return [p, st.mtimeMs, st.size];
613
+ } catch {
614
+ return [p, -1, -1];
615
+ }
616
+ });
617
+ if (extraCache && extraCache.raw === raw && JSON.stringify(extraCache.fp) === JSON.stringify(fp)) {
618
+ return extraCache.data;
619
+ }
620
+ const { data, loaded } = scanExtraFiles(files);
621
+ extraCache = { raw, fp, data, loaded };
622
+ return data;
623
+ }
624
+ var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS, EXTRA_CATEGORIES, EXTRA_FLAT_FIELD, extraCache;
453
625
  var init_rules_loader = __esm({
454
626
  "src/infra/rules-loader.ts"() {
455
627
  "use strict";
@@ -484,6 +656,22 @@ var init_rules_loader = __esm({
484
656
  ];
485
657
  return cands.filter((c) => existsSync(c));
486
658
  })();
659
+ EXTRA_CATEGORIES = {
660
+ dangerous_commands: ["pattern"],
661
+ injection_detection: ["keyword", "lang"],
662
+ sensitive_path_access: ["path"],
663
+ intent_consistency: ["intent_keywords"],
664
+ password_modify_consent: ["pattern"],
665
+ user_deletion_consent: ["pattern"]
666
+ };
667
+ EXTRA_FLAT_FIELD = [
668
+ [["path"], "sensitive_path_access"],
669
+ [["keyword", "lang"], "injection_detection"],
670
+ [["intent_keywords"], "intent_consistency"],
671
+ [["pattern", "risk_level"], "dangerous_commands"],
672
+ [["pattern", "risk_type"], "dangerous_commands"]
673
+ ];
674
+ extraCache = null;
487
675
  }
488
676
  });
489
677
 
@@ -700,7 +888,9 @@ var init_platform_traits = __esm({
700
888
  /^ag(?:\s|$)/i,
701
889
  /^rg(?:\s|$)/i,
702
890
  /^awk\s+/i,
703
- /^sed\s+/i,
891
+ // NEW-FN-P(M03):sed -i/--in-place 是原地写,挡出白名单走 L2 read_before_write。
892
+ // 前瞻放最前(外层 ^sed\s+ 会消费 -i 前的空格)。与 Python profiles/linux.py 对齐。
893
+ /^(?!.*(?:\s|['"])-[a-zA-Z]*i[a-zA-Z]*\b)(?!.*(?:\s|['"])--in-place)sed\s+/i,
704
894
  /^cut\s+/i,
705
895
  /^sort(?:\s|$)/i,
706
896
  /^uniq(?:\s|$)/i,
@@ -936,8 +1126,8 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
936
1126
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
937
1127
  const { writeCommandRes } = getPlatformTraits(osType);
938
1128
  if (writeCommandRes.some((rx) => rx.test(actionDetail))) return true;
939
- if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
940
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1129
+ if (SED_INPLACE_RE.test(actionDetail)) return true;
1130
+ for (const seg of splitCommandSegments(actionDetail)) {
941
1131
  const trimmed = seg.trim();
942
1132
  if (!trimmed) continue;
943
1133
  const parts = trimmed.split(/\s+/);
@@ -1165,11 +1355,11 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1165
1355
  const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
1166
1356
  let hasWriteSeg = hasRedirectWrite;
1167
1357
  if (!hasWriteSeg) {
1168
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1358
+ for (const seg of splitCommandSegments(actionDetail)) {
1169
1359
  const trimmed = seg.trim();
1170
1360
  if (!trimmed) continue;
1171
1361
  const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1172
- if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
1362
+ if (WRITE_CMDS.has(fw) || SED_INPLACE_RE.test(trimmed)) {
1173
1363
  hasWriteSeg = true;
1174
1364
  break;
1175
1365
  }
@@ -1188,14 +1378,15 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1188
1378
  const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
1189
1379
  const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
1190
1380
  const targetPaths = [];
1191
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1381
+ for (const seg of splitCommandSegments(actionDetail)) {
1192
1382
  const trimmed = seg.trim();
1193
1383
  if (!trimmed) continue;
1194
1384
  const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1195
1385
  if (firstWord === "cd") {
1196
1386
  continue;
1197
1387
  }
1198
- if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1388
+ if (firstWord === "sed" && SED_INPLACE_RE.test(trimmed)) {
1389
+ } else if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1199
1390
  const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
1200
1391
  const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
1201
1392
  const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
@@ -1310,7 +1501,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory, osTyp
1310
1501
  }
1311
1502
  function containsPrivilegeEscalation(actionDetail) {
1312
1503
  const ad = actionDetail.toLowerCase();
1313
- for (const seg of ad.split(/&&|\|\||;|\n/)) {
1504
+ for (const seg of splitCommandSegments(ad)) {
1314
1505
  const trimmed = seg.trim();
1315
1506
  if (!trimmed) continue;
1316
1507
  const parts = trimmed.split(/\s+/);
@@ -1399,7 +1590,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd, osType) {
1399
1590
  violatedLayers: ["L2.1"]
1400
1591
  };
1401
1592
  }
1402
- var ROOT_SENSITIVE_PATHS, HOME_SENSITIVE_PATHS, CREDENTIAL_PATHS, ROOT_SENSITIVE_PATHS_WINDOWS, HOME_SENSITIVE_PATHS_WINDOWS, CREDENTIAL_PATHS_WINDOWS, ROOT_SENSITIVE_PATHS_MACOS, HOME_SENSITIVE_PATHS_MACOS, CREDENTIAL_PATHS_MACOS, ROOT_SENSITIVE_PATHS_HARMONY, HOME_SENSITIVE_PATHS_HARMONY, CREDENTIAL_PATHS_HARMONY, _scopeIndex, NON_WRITE_TOOLS, NON_WRITE_TOOL_PREFIXES, WRITE_KEYWORDS, DEVNULL_REDIRECT_RE, CREATE_KEYWORDS, PASSWORD_MODIFY_PATTERNS, USER_DELETION_PATTERNS, INTENT_DEVIATION_PATTERNS, COMBO_STAGES, COMBO_CHAINS, INLINE_SCRIPT_RE, DIR_TRAVERSAL_KW;
1593
+ var ROOT_SENSITIVE_PATHS, HOME_SENSITIVE_PATHS, CREDENTIAL_PATHS, ROOT_SENSITIVE_PATHS_WINDOWS, HOME_SENSITIVE_PATHS_WINDOWS, CREDENTIAL_PATHS_WINDOWS, ROOT_SENSITIVE_PATHS_MACOS, HOME_SENSITIVE_PATHS_MACOS, CREDENTIAL_PATHS_MACOS, ROOT_SENSITIVE_PATHS_HARMONY, HOME_SENSITIVE_PATHS_HARMONY, CREDENTIAL_PATHS_HARMONY, _scopeIndex, NON_WRITE_TOOLS, NON_WRITE_TOOL_PREFIXES, WRITE_KEYWORDS, SED_INPLACE_RE, DEVNULL_REDIRECT_RE, CREATE_KEYWORDS, PASSWORD_MODIFY_PATTERNS, USER_DELETION_PATTERNS, INTENT_DEVIATION_PATTERNS, COMBO_STAGES, COMBO_CHAINS, INLINE_SCRIPT_RE, DIR_TRAVERSAL_KW;
1403
1594
  var init_logic_rules = __esm({
1404
1595
  "src/engine/logic-rules.ts"() {
1405
1596
  "use strict";
@@ -1408,6 +1599,7 @@ var init_logic_rules = __esm({
1408
1599
  init_runtime_config_bridge();
1409
1600
  init_rules_loader();
1410
1601
  init_platform_traits();
1602
+ init_shell_split();
1411
1603
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1412
1604
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1413
1605
  CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path5, riskLevel, desc, dirDelOnly }) => ({ path: path5, riskLevel, desc, credential: true, dirDelOnly }));
@@ -1442,6 +1634,7 @@ var init_logic_rules = __esm({
1442
1634
  ]);
1443
1635
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
1444
1636
  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"];
1637
+ SED_INPLACE_RE = /\bsed\s+(?:-\S+\s+)*(?:-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/i;
1445
1638
  DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
1446
1639
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
1447
1640
  PASSWORD_MODIFY_PATTERNS = (() => {
@@ -1518,7 +1711,7 @@ var init_asset_dir = __esm({
1518
1711
  });
1519
1712
 
1520
1713
  // src/engine/skill-loader.ts
1521
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
1714
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
1522
1715
  import { resolve as resolve3, dirname as dirname2 } from "path";
1523
1716
  import { fileURLToPath as fileURLToPath3 } from "url";
1524
1717
  function loadAllSkills() {
@@ -1530,7 +1723,7 @@ function loadAllSkills() {
1530
1723
  console.warn("[AgentMoss] Skills directory not found:", primary, "and fallback:", fallback);
1531
1724
  return cachedSkills;
1532
1725
  }
1533
- const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
1726
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".md"));
1534
1727
  for (const f of files) {
1535
1728
  const name = f.replace(".md", "");
1536
1729
  cachedSkills[name] = readFileSync3(resolve3(dir, f), "utf-8");
@@ -1709,7 +1902,7 @@ __export(runtime_config_exports, {
1709
1902
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1710
1903
  updateSkillEnabled: () => updateSkillEnabled
1711
1904
  });
1712
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync3, statSync } from "fs";
1905
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1713
1906
  import { join as join3 } from "path";
1714
1907
  import { fileURLToPath as fileURLToPath4 } from "url";
1715
1908
  import { dirname as dirname3 } from "path";
@@ -1755,7 +1948,7 @@ function loadRuntimeConfig() {
1755
1948
  return seeded;
1756
1949
  }
1757
1950
  try {
1758
- const st = statSync(p);
1951
+ const st = statSync2(p);
1759
1952
  const cacheKey = `${p}|${st.mtimeMs}|${st.size}`;
1760
1953
  if (runtimeConfigCache && runtimeConfigCache.key === cacheKey) {
1761
1954
  return runtimeConfigCache.value;
@@ -1836,9 +2029,18 @@ function _mergeUserCustom(primary, runtime, layers, key) {
1836
2029
  const extra = _userCustomRules(runtime, layers, key);
1837
2030
  return extra.length > 0 ? [...primary, ...extra] : [...primary];
1838
2031
  }
2032
+ function _mergeExtraDirRules(primary, category) {
2033
+ try {
2034
+ const extra = loadExtraRules()[category] || [];
2035
+ return extra.length > 0 ? [...primary, ...extra] : [...primary];
2036
+ } catch {
2037
+ return [...primary];
2038
+ }
2039
+ }
1839
2040
  function getEnabledL1CommandPatterns(runtime, osType) {
1840
2041
  const cat = runtime.L1_rules?.dangerous_commands || { category_enabled: true, rules: [] };
1841
2042
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2043
+ rules = _mergeExtraDirRules(rules, "dangerous_commands");
1842
2044
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "pattern");
1843
2045
  if (osType) rules = _filterRulesByOs(rules, osType);
1844
2046
  return rules.map((r) => ({
@@ -1854,6 +2056,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
1854
2056
  cat = runtime.L2_rules?.sensitive_path_access || { category_enabled: true, rules: [] };
1855
2057
  }
1856
2058
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2059
+ rules = _mergeExtraDirRules(rules, "sensitive_path_access");
1857
2060
  rules = _mergeUserCustom(rules, runtime, ["L1_rules", "L2_rules"], "path");
1858
2061
  if (osType) rules = _filterRulesByOs(rules, osType);
1859
2062
  return rules.map((r) => {
@@ -1868,7 +2071,7 @@ function hasRuntimeConfig() {
1868
2071
  const p = getRuntimeConfigPath();
1869
2072
  let key;
1870
2073
  try {
1871
- const st = statSync(p);
2074
+ const st = statSync2(p);
1872
2075
  key = `${p}|${st.mtimeMs}|${st.size}`;
1873
2076
  } catch {
1874
2077
  return false;
@@ -2098,12 +2301,12 @@ function _scenarioRulesToDefaults() {
2098
2301
  if (!existsSync5(scenDir)) return out;
2099
2302
  let entries;
2100
2303
  try {
2101
- entries = readdirSync3(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2304
+ entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2102
2305
  } catch {
2103
2306
  return out;
2104
2307
  }
2105
2308
  for (const scenarioId of entries) {
2106
- const rulesFile = join3(scenDir, scenarioId, "rules.json");
2309
+ const rulesFile = resolveScenarioRulesFile(scenarioId) ?? join3(scenDir, scenarioId, "rules.json");
2107
2310
  if (!existsSync5(rulesFile)) continue;
2108
2311
  let data;
2109
2312
  try {
@@ -2147,7 +2350,7 @@ function _agentRulesToDefaults() {
2147
2350
  if (!existsSync5(agentsDir)) return out;
2148
2351
  let entries;
2149
2352
  try {
2150
- entries = readdirSync3(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2353
+ entries = readdirSync4(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2151
2354
  } catch {
2152
2355
  return out;
2153
2356
  }
@@ -2485,6 +2688,7 @@ function _migrateCategoriesAcrossLayers(cfg, srcLayer, dstLayer, catNames) {
2485
2688
  function getEnabledL1InjectionKeywords(runtime, osType) {
2486
2689
  const cat = runtime.L1_rules?.injection_detection || { category_enabled: true, rules: [] };
2487
2690
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2691
+ rules = _mergeExtraDirRules(rules, "injection_detection");
2488
2692
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "keyword");
2489
2693
  if (osType) rules = _filterRulesByOs(rules, osType);
2490
2694
  return rules.map((r) => ({
@@ -2496,6 +2700,7 @@ function getEnabledL1InjectionKeywords(runtime, osType) {
2496
2700
  function getEnabledL2IntentPatterns(runtime, osType) {
2497
2701
  const cat = runtime.L2_rules?.intent_consistency || { category_enabled: true, rules: [] };
2498
2702
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2703
+ rules = _mergeExtraDirRules(rules, "intent_consistency");
2499
2704
  rules = _mergeUserCustom(rules, runtime, "L2_rules", "intent_keywords");
2500
2705
  if (osType) rules = _filterRulesByOs(rules, osType);
2501
2706
  return rules.map((r) => ({
@@ -2506,12 +2711,22 @@ function getEnabledL2IntentPatterns(runtime, osType) {
2506
2711
  }
2507
2712
  function getEnabledL2PasswordPatterns(runtime) {
2508
2713
  const cat = runtime.L2_rules?.password_modify_consent || { category_enabled: true, rules: [] };
2509
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2714
+ const rules = _mergeUserCustom(
2715
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2716
+ runtime,
2717
+ "L2_rules",
2718
+ "pattern"
2719
+ );
2510
2720
  return rules.map((r) => r.pattern || "");
2511
2721
  }
2512
2722
  function getEnabledL2UserDeletionPatterns(runtime) {
2513
2723
  const cat = runtime.L2_rules?.user_deletion_consent || { category_enabled: true, rules: [] };
2514
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2724
+ const rules = _mergeUserCustom(
2725
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2726
+ runtime,
2727
+ "L2_rules",
2728
+ "pattern"
2729
+ );
2515
2730
  return rules.map((r) => r.pattern || "");
2516
2731
  }
2517
2732
  function isL2CategoryEnabled(runtime, categoryKey) {
@@ -2543,28 +2758,48 @@ function getEnabledL1CommandPatternsOrDefault(runtime, osType) {
2543
2758
  if (_hasRulesForOs(cat?.rules, osType)) {
2544
2759
  return getEnabledL1CommandPatterns(runtime, osType);
2545
2760
  }
2546
- return _mergeUserCustom(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), runtime, "L1_rules", "pattern");
2761
+ return _mergeUserCustom(
2762
+ _mergeExtraDirRules(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2763
+ runtime,
2764
+ "L1_rules",
2765
+ "pattern"
2766
+ );
2547
2767
  }
2548
2768
  function getEnabledL1InjectionKeywordsOrDefault(runtime, osType) {
2549
2769
  const cat = runtime.L1_rules?.injection_detection;
2550
2770
  if (_hasRulesForOs(cat?.rules, osType)) {
2551
2771
  return getEnabledL1InjectionKeywords(runtime, osType);
2552
2772
  }
2553
- return _mergeUserCustom(_l1InjectionKeywordsToRules(), runtime, "L1_rules", "keyword");
2773
+ return _mergeUserCustom(
2774
+ _mergeExtraDirRules(_l1InjectionKeywordsToRules(), "injection_detection"),
2775
+ runtime,
2776
+ "L1_rules",
2777
+ "keyword"
2778
+ );
2554
2779
  }
2555
2780
  function getEnabledL2SensitivePathsOrDefault(runtime, osType) {
2556
2781
  const spCat = _sensitivePathCategoryOf(runtime);
2557
2782
  if (_hasRulesForOs(spCat?.rules, osType)) {
2558
2783
  return getEnabledL2SensitivePaths(runtime, osType);
2559
2784
  }
2560
- return _mergeUserCustom(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), runtime, ["L1_rules", "L2_rules"], "path");
2785
+ return _mergeUserCustom(
2786
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2787
+ runtime,
2788
+ ["L1_rules", "L2_rules"],
2789
+ "path"
2790
+ );
2561
2791
  }
2562
2792
  function getEnabledL2IntentPatternsOrDefault(runtime, osType) {
2563
2793
  const cat = runtime.L2_rules?.intent_consistency;
2564
2794
  if (_hasRulesForOs(cat?.rules, osType)) {
2565
2795
  return getEnabledL2IntentPatterns(runtime, osType);
2566
2796
  }
2567
- return _mergeUserCustom(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), runtime, "L2_rules", "intent_keywords");
2797
+ return _mergeUserCustom(
2798
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2799
+ runtime,
2800
+ "L2_rules",
2801
+ "intent_keywords"
2802
+ );
2568
2803
  }
2569
2804
  function getEnabledL2PasswordPatternsOrDefault(runtime, osType) {
2570
2805
  const cat = runtime.L2_rules?.password_modify_consent;
@@ -2960,6 +3195,7 @@ var init_runtime_config = __esm({
2960
3195
  init_heuristic();
2961
3196
  init_logic_rules();
2962
3197
  init_skill_loader();
3198
+ init_rules_loader();
2963
3199
  init_runtime_config_bridge();
2964
3200
  __dirname4 = dirname3(fileURLToPath4(import.meta.url));
2965
3201
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
@@ -3020,7 +3256,9 @@ __export(config_exports, {
3020
3256
  isLLMAvailable: () => isLLMAvailable,
3021
3257
  isLogicRulesEnabled: () => isLogicRulesEnabled,
3022
3258
  probeLLMConnection: () => probeLLMConnection,
3023
- updateLLMConfig: () => updateLLMConfig
3259
+ updateLLMConfig: () => updateLLMConfig,
3260
+ userRuleVariantPattern: () => userRuleVariantPattern,
3261
+ wildcardToRegExp: () => wildcardToRegExp
3024
3262
  });
3025
3263
  import fs2 from "fs";
3026
3264
  import path2 from "path";
@@ -3117,6 +3355,8 @@ function loadCustomRules() {
3117
3355
  const p = r?.pattern;
3118
3356
  if (!p) continue;
3119
3357
  rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
3358
+ const variant = userRuleVariantPattern(p);
3359
+ if (variant) rules.push({ pattern: wildcardToRegExp(variant), action: "Deny", severity: "high" });
3120
3360
  }
3121
3361
  break;
3122
3362
  } catch {
@@ -3124,6 +3364,15 @@ function loadCustomRules() {
3124
3364
  }
3125
3365
  return rules;
3126
3366
  }
3367
+ function userRuleVariantPattern(pattern) {
3368
+ const tokens = pattern.split(/\s+/);
3369
+ if (tokens.length >= 3 && tokens[1] === "*" && tokens[2] !== "*") {
3370
+ let variant = [tokens[0], ...tokens.slice(2)].join(" ");
3371
+ if (!variant.endsWith("*")) variant += " *";
3372
+ return variant;
3373
+ }
3374
+ return null;
3375
+ }
3127
3376
  function getLLMConfig() {
3128
3377
  return llmConfig;
3129
3378
  }
@@ -3360,15 +3609,27 @@ function getScenarioExtensions(scenarioId) {
3360
3609
  if (!scenarioId) return null;
3361
3610
  if (cache2.has(scenarioId)) return cache2.get(scenarioId) ?? null;
3362
3611
  let ext = null;
3612
+ let overrideHit = null;
3363
3613
  try {
3364
- if (hasRuntimeConfig()) {
3365
- ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3366
- }
3614
+ overrideHit = resolveScenarioRulesFile(scenarioId);
3367
3615
  } catch {
3368
3616
  }
3617
+ if (!overrideHit) {
3618
+ try {
3619
+ if (hasRuntimeConfig()) {
3620
+ ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3621
+ }
3622
+ } catch {
3623
+ }
3624
+ }
3369
3625
  if (ext === null) {
3370
- const scenariosDir = resolveScenariosDir();
3371
- const rulesFile = resolve5(scenariosDir, scenarioId, "rules.json");
3626
+ const builtinFile = resolve5(resolveScenariosDir(), scenarioId, "rules.json");
3627
+ const rulesFile = overrideHit ?? builtinFile;
3628
+ if (overrideHit) {
3629
+ console.warn(
3630
+ `[AgentMoss] \u26A0\uFE0F \u573A\u666F ${scenarioId} \u89C4\u5219\u88AB\u8986\u76D6\u76EE\u5F55\u8986\u76D6: ${overrideHit}\uFF08\u5185\u7F6E\u9ED8\u8BA4 ${builtinFile} \u88AB\u5FFD\u7565\uFF09\u3002\u8BE5\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4\uFF0C\u5185\u7F6E\u573A\u666F\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4E0A\u9762\u3002`
3631
+ );
3632
+ }
3372
3633
  if (!existsSync7(rulesFile)) {
3373
3634
  cache2.set(scenarioId, null);
3374
3635
  return null;
@@ -3420,6 +3681,7 @@ var init_scenarios = __esm({
3420
3681
  init_esm_shims();
3421
3682
  init_agents();
3422
3683
  init_runtime_config();
3684
+ init_rules_loader();
3423
3685
  __filename5 = fileURLToPath7(import.meta.url);
3424
3686
  __dirname6 = dirname5(__filename5);
3425
3687
  SCENARIOS_DIR_PROD = resolve5(__dirname6, "scenarios");
@@ -3485,16 +3747,41 @@ function scanDangerousCommandPatterns(actionType, actionDetail, osType) {
3485
3747
  violatedLayers: ["L1.1"]
3486
3748
  };
3487
3749
  }
3750
+ function allUrlsLoopback(actionDetail) {
3751
+ const hosts = actionDetail.match(new RegExp(LOOPBACK_URL_RE.source, "gi"));
3752
+ if (!hosts || hosts.length === 0) return false;
3753
+ return hosts.every((u) => {
3754
+ const raw = u.replace(/^https?:\/\//i, "");
3755
+ const host = raw.split("@").pop().split(":")[0].replace(/^\[|\]$/g, "").toLowerCase();
3756
+ return LOOPBACK_HOSTS.has(host) || host.startsWith("127.");
3757
+ });
3758
+ }
3759
+ function stripEchoParamsForUserRules(actionDetail) {
3760
+ return splitCommandSegments2(actionDetail).map((seg) => {
3761
+ const m = seg.match(/^(echo|printf)(?![a-zA-Z0-9_])\s*/i);
3762
+ if (!m) return seg;
3763
+ const head = m[1].toLowerCase();
3764
+ const rest = seg.slice(m[0].length);
3765
+ const cut = rest.search(/>+|\|/);
3766
+ return `${head} ` + (cut >= 0 ? rest.slice(cut) : "");
3767
+ }).join(" ");
3768
+ }
3488
3769
  function runHeuristicCheck(input) {
3489
3770
  const command = `${input.aNext.actionType}: ${input.aNext.actionDetail}`;
3490
3771
  const matched = [];
3491
3772
  let highestSeverity = "low";
3492
3773
  let highestRiskType = "";
3493
3774
  const userRules = getCustomRules();
3775
+ const atLower = input.aNext.actionType.toLowerCase();
3776
+ const isBashLike = ["bash", "executebash", "terminal-executebash"].includes(atLower);
3777
+ const urlsAllLoopback = allUrlsLoopback(input.aNext.actionDetail);
3778
+ const userRuleScanDetail = isBashLike ? stripEchoParamsForUserRules(input.aNext.actionDetail) : input.aNext.actionDetail;
3779
+ const scanCommand = `${input.aNext.actionType}: ${userRuleScanDetail}`;
3494
3780
  for (const rule of userRules) {
3495
3781
  try {
3496
3782
  const re = new RegExp(rule.pattern);
3497
- if (re.test(command)) {
3783
+ if (re.test(command) || re.test(scanCommand)) {
3784
+ if (urlsAllLoopback) continue;
3498
3785
  if (rule.action === "Deny") {
3499
3786
  return {
3500
3787
  allowed: false,
@@ -3677,6 +3964,23 @@ function buildPathContext(actionDetail) {
3677
3964
  if (!lines.length) return "";
3678
3965
  return "## \u8DEF\u5F84\u89E3\u6790\u4E8B\u5B9E\uFF08\u8BCD\u6CD5\u5206\u6790\uFF0C\u786E\u5B9A\u6027\u7ED3\u8BBA\uFF09\n" + lines.join("\n") + "\n\n## \u8FD0\u884C\u65F6\u672A\u9A8C\u8BC1\uFF08\u4F9B\u53C2\u8003\uFF0C\u975E\u4E8B\u5B9E\uFF09\n- \u5206\u6790\u5668\u4E0D\u8BFB\u6587\u4EF6\u7CFB\u7EDF\uFF1A\u65E0\u6CD5\u786E\u8BA4\u8DEF\u5F84\u4E2D\u7684\u4E2D\u95F4\u76EE\u5F55\u662F\u5426\u4E3A\u7B26\u53F7\u94FE\u63A5\uFF1B\u82E5\u4E2D\u95F4\u6BB5\u5728\u6267\u884C\u65F6\u662F\uFF08\u6216\u88AB\u66FF\u6362\u4E3A\uFF09\u6307\u5411\u4E3B\u76EE\u5F55\u5916\u7684\u8F6F\u94FE\uFF0C\u5B9E\u9645\u64CD\u4F5C\u76EE\u6807\u53EF\u80FD\u968F\u4E4B\u6539\u53D8\u3002\u8BCD\u6CD5\u89E3\u6790\u7ED3\u679C\u57FA\u4E8E\u6807\u51C6\u8DEF\u5F84\u8BED\u4E49\uFF0C\u4E0D\u5E94\u56E0\u731C\u6D4B\u300C\u89E3\u6790\u4F9D\u8D56\u67D0\u6BB5\u662F\u5426\u5B58\u5728\u300D\u800C\u5426\u5B9A\u5B83\u3002";
3679
3966
  }
3967
+ function isPurePrintCode(code) {
3968
+ if (code.includes("\\")) return false;
3969
+ if (PY_EXEC_IO_RE.test(code) || PY_BIGNUM_RE.test(code)) return false;
3970
+ return PY_PRINT_RE.test(code) || !/\w+\s*\(/.test(code);
3971
+ }
3972
+ function stripInlinePythonLiterals(text) {
3973
+ const subst = (_m, g1, g2, g3) => isPurePrintCode(g2 ?? g3 ?? "") ? "$(<<pylit>>)" : _m;
3974
+ const plain = (_m, g1, g2, g3) => {
3975
+ if (!isPurePrintCode(g2 ?? g3 ?? "")) return _m;
3976
+ const q = _m.trimEnd().endsWith('"') ? '"' : "'";
3977
+ return `${g1} -c ${q}<<pylit>>${q}`;
3978
+ };
3979
+ let out = text;
3980
+ out = out.replace(new RegExp(PY_CMD_SUBST_C_RE.source, PY_CMD_SUBST_C_RE.flags), subst);
3981
+ out = out.replace(new RegExp(PY_INLINE_C_RE.source, PY_INLINE_C_RE.flags), plain);
3982
+ return out;
3983
+ }
3680
3984
  function stripLiteralTextSegments(actionDetail) {
3681
3985
  const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
3682
3986
  const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node|crontab|at)\b/.test(actionDetail);
@@ -3687,13 +3991,14 @@ function stripLiteralTextSegments(actionDetail) {
3687
3991
  for (const seg of splitCommandSegments2(actionDetail)) {
3688
3992
  const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3689
3993
  if (fw === "echo" || fw === "printf") {
3690
- const segUnquotedRedirect = seg.replace(
3994
+ const segPy = stripInlinePythonLiterals(seg);
3995
+ const segUnquotedRedirect = segPy.replace(
3691
3996
  />{1,2}\s*(['"])([~./][^'"]+)\1/g,
3692
3997
  (_m, _q, p) => ">" + p
3693
3998
  );
3694
3999
  resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
3695
4000
  } else {
3696
- resultParts.push(seg);
4001
+ resultParts.push(stripInlinePythonLiterals(seg));
3697
4002
  }
3698
4003
  }
3699
4004
  return resultParts.join("");
@@ -4217,7 +4522,7 @@ function checkLateralMovement(actionDetail) {
4217
4522
  }
4218
4523
  return null;
4219
4524
  }
4220
- var INJECTION_KEYWORDS, DANGEROUS_PATTERNS2, DANGEROUS_PATTERNS_WINDOWS, DANGEROUS_PATTERNS_MACOS, DANGEROUS_PATTERNS_HARMONY, PATH_CONTEXT_TRIGGER_RE, HOME_PREFIX_RE, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES, HOME_TARGET_RE, HOME_ASSIGN_RE;
4525
+ var INJECTION_KEYWORDS, DANGEROUS_PATTERNS2, DANGEROUS_PATTERNS_WINDOWS, DANGEROUS_PATTERNS_MACOS, DANGEROUS_PATTERNS_HARMONY, LOOPBACK_URL_RE, LOOPBACK_HOSTS, PATH_CONTEXT_TRIGGER_RE, HOME_PREFIX_RE, PY_EXEC_IO_RE, PY_PRINT_RE, PY_BIGNUM_RE, PY_INLINE_C_RE, PY_CMD_SUBST_C_RE, SENSITIVE_EXCLUDED_PREFIXES, SEARCH_ACTION_PREFIXES, HOME_TARGET_RE, HOME_ASSIGN_RE;
4221
4526
  var init_heuristic = __esm({
4222
4527
  "src/engine/heuristic.ts"() {
4223
4528
  "use strict";
@@ -4241,8 +4546,15 @@ var init_heuristic = __esm({
4241
4546
  DANGEROUS_PATTERNS_WINDOWS = _loadDangerousPatterns("windows");
4242
4547
  DANGEROUS_PATTERNS_MACOS = _loadDangerousPatterns("macos");
4243
4548
  DANGEROUS_PATTERNS_HARMONY = _loadDangerousPatterns("harmony");
4549
+ LOOPBACK_URL_RE = /https?:\/\/([^\/\s;'"|&)]+)/i;
4550
+ LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "::1", "0.0.0.0", "[::1]"]);
4244
4551
  PATH_CONTEXT_TRIGGER_RE = /(?:^|[;&|]\s*)(?:rm|rmdir|unlink|ln|mv|cp|dd|truncate|shred|chmod|chown)\b/i;
4245
4552
  HOME_PREFIX_RE = /(?:~|~(?=\/)|\$HOME\b|\$\{HOME\}|\/home\/[a-z0-9_.-]+)/gi;
4553
+ PY_EXEC_IO_RE = /os\.system|os\.popen|subprocess|eval\s*\(|exec\s*\(|__import__|open\s*\(|socket|urllib|requests\.|shutil|os\.(?:remove|unlink|rename|makedirs?|listdir|scandir|walk|chmod|chown|kill|environ)|iterdir|glob\s*\(|path\(/i;
4554
+ PY_PRINT_RE = /\b(?:print|sys\.stdout\.write)\s*\(/;
4555
+ PY_BIGNUM_RE = /\d{6,}/;
4556
+ PY_INLINE_C_RE = /(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"|'([^']*)')/i;
4557
+ PY_CMD_SUBST_C_RE = /\$\(\s*(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"'|'([^']*)')\s*\)/i;
4246
4558
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
4247
4559
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
4248
4560
  HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
@@ -4661,7 +4973,7 @@ init_heuristic();
4661
4973
  init_config();
4662
4974
  init_logic_rules();
4663
4975
  init_inline_analyzer();
4664
- import { statSync as statSync3, existsSync as existsSync10 } from "fs";
4976
+ import { statSync as statSync4, existsSync as existsSync10 } from "fs";
4665
4977
  import { resolve as resolve9, join as join6, isAbsolute as isAbsolute2 } from "path";
4666
4978
  import { homedir as homedir3 } from "os";
4667
4979
 
@@ -5901,6 +6213,28 @@ function parseIf(content, args) {
5901
6213
  ${chosen}
5902
6214
  ${content.slice(fiPos + 2)}`;
5903
6215
  }
6216
+ var HEREDOC_RE = /<<-?\s*['"]?(\w+)['"]?/;
6217
+ function stripNonExecutableLines(content) {
6218
+ if (!content) return content;
6219
+ const out = [];
6220
+ let heredocEnd = null;
6221
+ const lines = content.split("\n");
6222
+ for (let i = 0; i < lines.length; i++) {
6223
+ const line = lines[i];
6224
+ if (heredocEnd !== null) {
6225
+ if (line.trim() === heredocEnd) heredocEnd = null;
6226
+ out.push(line);
6227
+ continue;
6228
+ }
6229
+ const m = HEREDOC_RE.exec(line);
6230
+ if (m) heredocEnd = m[1];
6231
+ const stripped = line.replace(/^\s+/, "");
6232
+ if (i === 0 && stripped.startsWith("#!")) continue;
6233
+ if (stripped.startsWith("#")) continue;
6234
+ out.push(line);
6235
+ }
6236
+ return out.join("\n");
6237
+ }
5904
6238
  function markLoopsReachable(content) {
5905
6239
  return content;
5906
6240
  }
@@ -5951,6 +6285,7 @@ var ShellBackend = class {
5951
6285
  bound = symOut;
5952
6286
  }
5953
6287
  }
6288
+ bound = stripNonExecutableLines(bound);
5954
6289
  const loopMarked = markLoopsReachable(bound);
5955
6290
  const calls = extractCallsFromContent(loopMarked);
5956
6291
  return {
@@ -6343,7 +6678,7 @@ function extractInlineScriptPath(actionDetail) {
6343
6678
  try {
6344
6679
  const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
6345
6680
  if (existsSync10(resolved)) {
6346
- const stat = statSync3(resolved);
6681
+ const stat = statSync4(resolved);
6347
6682
  if (stat.isFile()) return resolved;
6348
6683
  }
6349
6684
  } catch {
@@ -6357,7 +6692,7 @@ function extractInlineScriptPath(actionDetail) {
6357
6692
  try {
6358
6693
  const resolved = resolvePath2(p);
6359
6694
  if (existsSync10(resolved)) {
6360
- const stat = statSync3(resolved);
6695
+ const stat = statSync4(resolved);
6361
6696
  if (stat.isFile()) return resolved;
6362
6697
  }
6363
6698
  } catch {
@@ -6373,7 +6708,7 @@ function extractInlineScriptPath(actionDetail) {
6373
6708
  try {
6374
6709
  resolved = resolvePath2(p);
6375
6710
  if (existsSync10(resolved)) {
6376
- const stat = statSync3(resolved);
6711
+ const stat = statSync4(resolved);
6377
6712
  if (stat.isFile()) return resolved;
6378
6713
  }
6379
6714
  } catch {
@@ -6417,7 +6752,30 @@ function resolvePath2(p) {
6417
6752
  }
6418
6753
  return resolve9(expanded);
6419
6754
  }
6420
- function scanScriptChainWithL1(chain, input) {
6755
+ function scanExtensionPatterns(ext, content) {
6756
+ const pats = ext?.extra_command_patterns;
6757
+ if (!pats || pats.length === 0) return null;
6758
+ for (const p of pats) {
6759
+ const src = typeof p === "string" ? p : p?.pattern;
6760
+ if (!src) continue;
6761
+ let re;
6762
+ try {
6763
+ re = new RegExp(src, "i");
6764
+ } catch {
6765
+ continue;
6766
+ }
6767
+ if (re.test(content)) {
6768
+ const risk = typeof p === "string" ? "high" : p.risk_level || "high";
6769
+ return {
6770
+ riskLevel: risk,
6771
+ riskType: typeof p === "string" ? "sensitive_action" : p.risk_type || "sensitive_action",
6772
+ reason: typeof p === "string" ? `\u547D\u4E2D\u6269\u5C55\u89C4\u5219: ${src}` : p.reason || `\u547D\u4E2D\u6269\u5C55\u89C4\u5219: ${src}`
6773
+ };
6774
+ }
6775
+ }
6776
+ return null;
6777
+ }
6778
+ function scanScriptChainWithL1(chain, input, ext) {
6421
6779
  for (const node of chain.nodes) {
6422
6780
  if (node.error || !node.preprocessedContent || node.l1Hit) continue;
6423
6781
  const virtualInput = {
@@ -6425,6 +6783,19 @@ function scanScriptChainWithL1(chain, input) {
6425
6783
  aNext: { actionType: "bash", actionDetail: node.preprocessedContent }
6426
6784
  };
6427
6785
  const l1 = scanDangerousCommandPatterns("bash", node.preprocessedContent, input.osType);
6786
+ const extHit = scanExtensionPatterns(ext, node.preprocessedContent);
6787
+ if (extHit) {
6788
+ node.l1Hit = true;
6789
+ node.l1RiskLevel = extHit.riskLevel;
6790
+ node.l1RiskType = extHit.riskType;
6791
+ node.l1Reason = extHit.reason;
6792
+ if (extHit.riskLevel === "critical") {
6793
+ chain.hasCritical = true;
6794
+ chain.firstHitNode = node;
6795
+ break;
6796
+ }
6797
+ continue;
6798
+ }
6428
6799
  if (l1 && !l1.allowed) {
6429
6800
  node.l1Hit = true;
6430
6801
  node.l1RiskLevel = l1.riskLevel;
@@ -6488,7 +6859,7 @@ function scanScriptChainKeywords(chain) {
6488
6859
  }
6489
6860
  return chain;
6490
6861
  }
6491
- function analyzeScriptContent2(input) {
6862
+ function analyzeScriptContent2(input, ext) {
6492
6863
  const actionType = input.aNext.actionType.toLowerCase();
6493
6864
  if (actionType !== "bash") return emptyChain();
6494
6865
  const { path: scriptPath, args: scriptArgs } = extractScriptPathWithArgs(input.aNext.actionDetail);
@@ -6520,7 +6891,7 @@ function analyzeScriptContent2(input) {
6520
6891
  const tracker = new RecursiveTracker(l1Adapter);
6521
6892
  const chain = tracker.track(resolvedPath, isInline ? null : scriptArgs, input.cwd || process.cwd());
6522
6893
  if (chain.nodes.length === 0) return chain;
6523
- scanScriptChainWithL1(chain, input);
6894
+ scanScriptChainWithL1(chain, input, ext);
6524
6895
  scanScriptChainWithL2(chain, input);
6525
6896
  scanScriptChainKeywords(chain);
6526
6897
  return chain;
@@ -8343,9 +8714,13 @@ async function runAnalysis(input) {
8343
8714
  }
8344
8715
  }
8345
8716
  }
8717
+ const agentExt = mergeAgentExtensions(
8718
+ isLayerEnabled("agent_rules_enabled") ? getAgentExtensions(input.agentId) : null,
8719
+ isLayerEnabled("scenario_rules_enabled") ? getScenarioExtensionsMulti(input.scenarioId || process.env.AGENT_MOSS_SCENARIO || "") : null
8720
+ );
8346
8721
  let scriptChain = null;
8347
8722
  if (input.aNext.actionType.toLowerCase() === "bash") {
8348
- scriptChain = analyzeScriptContent2(input);
8723
+ scriptChain = analyzeScriptContent2(input, agentExt);
8349
8724
  if (scriptChain.nodes.length > 0) {
8350
8725
  for (const node of scriptChain.nodes) {
8351
8726
  if (node.unanalyzable) {
@@ -8397,10 +8772,6 @@ async function runAnalysis(input) {
8397
8772
  const actionType = input.aNext.actionType.toLowerCase();
8398
8773
  const rawDetail = extractRawCommand(input.aNext.actionDetail);
8399
8774
  const lowerDetail = rawDetail.toLowerCase();
8400
- const agentExt = mergeAgentExtensions(
8401
- isLayerEnabled("agent_rules_enabled") ? getAgentExtensions(input.agentId) : null,
8402
- isLayerEnabled("scenario_rules_enabled") ? getScenarioExtensionsMulti(input.scenarioId || process.env.AGENT_MOSS_SCENARIO || "") : null
8403
- );
8404
8775
  if (FULLY_SAFE_TOOLS.has(actionType)) {
8405
8776
  return {
8406
8777
  allowed: true,