@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.
package/dist/cli.js CHANGED
@@ -111,18 +111,41 @@ function hasCommandSubstitution(text) {
111
111
  return CMD_SUBSTITUTION.test(text);
112
112
  }
113
113
  function splitCommandSegments(command) {
114
- return command.split(CMD_SEPARATORS).map((s) => s.trim()).filter((s) => s.length > 0);
114
+ const segments = [];
115
+ let cur = "";
116
+ let quote = null;
117
+ for (let i = 0; i < command.length; i++) {
118
+ const ch = command[i];
119
+ if (quote) {
120
+ cur += ch;
121
+ if (ch === quote) quote = null;
122
+ continue;
123
+ }
124
+ if (ch === "'" || ch === '"') {
125
+ quote = ch;
126
+ cur += ch;
127
+ continue;
128
+ }
129
+ if (/\||&|;|\n/.test(ch)) {
130
+ segments.push(cur);
131
+ segments.push(ch);
132
+ cur = "";
133
+ continue;
134
+ }
135
+ cur += ch;
136
+ }
137
+ if (cur) segments.push(cur);
138
+ return segments.map((s) => s.trim()).filter((s) => s.length > 0 && !/^(?:\|\||&&|[|;&])$/.test(s));
115
139
  }
116
140
  function extractFirstCommand(command) {
117
141
  const segments = splitCommandSegments(command);
118
142
  return segments.length ? segments[0] : "";
119
143
  }
120
- var CMD_SEPARATORS, CMD_SUBSTITUTION;
144
+ var CMD_SUBSTITUTION;
121
145
  var init_shell_split = __esm({
122
146
  "src/engine/shell-split.ts"() {
123
147
  "use strict";
124
148
  init_esm_shims();
125
- CMD_SEPARATORS = /\|\||&&|[|;&\n]/;
126
149
  CMD_SUBSTITUTION = /\$\(|`|</;
127
150
  }
128
151
  });
@@ -373,20 +396,25 @@ var init_runtime_config_bridge = __esm({
373
396
  });
374
397
 
375
398
  // src/infra/rules-loader.ts
376
- import { readFileSync as readFileSync2, existsSync } from "fs";
377
- import { resolve, dirname } from "path";
399
+ import { readFileSync as readFileSync2, existsSync, readdirSync, statSync } from "fs";
400
+ import { resolve, dirname, join as join2 } from "path";
378
401
  import { fileURLToPath as fileURLToPath2 } from "url";
379
402
  function dataRootDir() {
380
403
  return getDataRoot();
381
404
  }
382
- function overrideRulesDir() {
383
- return resolve(dataRootDir(), "rules");
405
+ function parseDirEnv(envName) {
406
+ const raw = (process.env[envName] || "").trim();
407
+ if (!raw) return [];
408
+ return raw.split(/[,;]/).map((s) => s.trim()).filter(Boolean).map((s) => resolve(s));
409
+ }
410
+ function overrideRulesDirs() {
411
+ return [...parseDirEnv("AGENT_MOSS_OVERRIDE_RULES_DIR"), resolve(dataRootDir(), "rules")];
384
412
  }
385
413
  function warnOverride(name, path6, data) {
386
414
  if (warnedOverride.has(path6)) return;
387
415
  warnedOverride.add(path6);
388
416
  console.warn(
389
- `[AgentMoss] \u89C4\u5219\u6587\u4EF6\u88AB\u7528\u6237\u6570\u636E\u76EE\u5F55\u8986\u76D6: ${path6}\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`
417
+ `[AgentMoss] \u89C4\u5219\u6587\u4EF6\u88AB\u8986\u76D6\u76EE\u5F55\u8986\u76D6: ${path6}\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`
390
418
  );
391
419
  const missing = (EXPECTED_KEYS[name] || []).filter((k) => !(k in data));
392
420
  if (missing.length > 0) {
@@ -398,24 +426,34 @@ function warnOverride(name, path6, data) {
398
426
  function overriddenRuleFiles() {
399
427
  const out = [];
400
428
  for (const name of Object.keys(EXPECTED_KEYS)) {
401
- const p = resolve(overrideRulesDir(), name);
402
- if (existsSync(p)) out.push(p);
429
+ for (const dir of overrideRulesDirs()) {
430
+ const p = resolve(dir, name);
431
+ if (existsSync(p)) {
432
+ out.push(p);
433
+ break;
434
+ }
435
+ }
403
436
  }
404
437
  return out;
405
438
  }
406
439
  function findRulesFile(name) {
407
- const override = resolve(overrideRulesDir(), name);
408
- if (existsSync(override)) return override;
440
+ for (const dir of overrideRulesDirs()) {
441
+ const p = resolve(dir, name);
442
+ if (existsSync(p)) return p;
443
+ }
409
444
  for (const dir of RULES_DIR_CANDS) {
410
445
  const p = resolve(dir, name);
411
446
  if (existsSync(p)) return p;
412
447
  }
413
448
  return null;
414
449
  }
450
+ function isOverridePath(name, p) {
451
+ return overrideRulesDirs().some((dir) => resolve(dir, name) === p);
452
+ }
415
453
  function loadJson(name) {
416
454
  const p = findRulesFile(name);
417
455
  if (!p) return {};
418
- const isOverride = p === resolve(overrideRulesDir(), name);
456
+ const isOverride = isOverridePath(name, p);
419
457
  let data;
420
458
  try {
421
459
  data = JSON.parse(readFileSync2(p, "utf-8")) || {};
@@ -426,6 +464,14 @@ function loadJson(name) {
426
464
  if (isOverride) warnOverride(name, p, data);
427
465
  return data;
428
466
  }
467
+ function resolveScenarioRulesFile(scenarioId) {
468
+ const rel = join2("scenarios", scenarioId, "rules.json");
469
+ for (const dir of overrideRulesDirs()) {
470
+ const p = resolve(dir, rel);
471
+ if (existsSync(p)) return p;
472
+ }
473
+ return null;
474
+ }
429
475
  function loadL1() {
430
476
  return loadJson("l1_rules.json");
431
477
  }
@@ -458,7 +504,142 @@ function loadMC() {
458
504
  function loadToolRegistry() {
459
505
  return loadJson("tool_registry.json");
460
506
  }
461
- var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS;
507
+ function extraRulesDirs() {
508
+ return parseDirEnv("AGENT_MOSS_EXTRA_RULES_DIR");
509
+ }
510
+ function listJsonFilesDeep(dir) {
511
+ const out = [];
512
+ let entries;
513
+ try {
514
+ entries = readdirSync(dir, { withFileTypes: true }).map((d) => d.name);
515
+ } catch {
516
+ return out;
517
+ }
518
+ for (const name of entries.sort()) {
519
+ const p = resolve(dir, name);
520
+ let isDir = false;
521
+ try {
522
+ isDir = statSync(p).isDirectory();
523
+ } catch {
524
+ continue;
525
+ }
526
+ if (isDir) out.push(...listJsonFilesDeep(p));
527
+ else if (name.toLowerCase().endsWith(".json")) out.push(p);
528
+ }
529
+ return out;
530
+ }
531
+ function scanExtraFiles(files) {
532
+ const data = {};
533
+ const loaded = [];
534
+ const seenIds = /* @__PURE__ */ new Map();
535
+ const classifyFlat = (r) => {
536
+ for (const [keys, cat] of EXTRA_FLAT_FIELD) {
537
+ if (r && typeof r === "object" && keys.every((k) => r[k])) return cat;
538
+ }
539
+ return null;
540
+ };
541
+ for (const p of files) {
542
+ let parsed;
543
+ try {
544
+ parsed = JSON.parse(readFileSync2(p, "utf-8"));
545
+ } catch (e) {
546
+ console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6\u635F\u574F/\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u8DF3\u8FC7: ${p}\uFF08${e.message}\uFF09`);
547
+ continue;
548
+ }
549
+ let pairs = [];
550
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
551
+ pairs = Object.entries(parsed);
552
+ } else if (Array.isArray(parsed)) {
553
+ let flatBad = 0;
554
+ for (const r of parsed) {
555
+ const cat = classifyFlat(r);
556
+ if (cat === null) {
557
+ flatBad += 1;
558
+ continue;
559
+ }
560
+ pairs.push([cat, [r]]);
561
+ }
562
+ if (flatBad > 0) {
563
+ console.warn(
564
+ `[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`
565
+ );
566
+ }
567
+ } else {
568
+ console.warn(
569
+ `[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`
570
+ );
571
+ continue;
572
+ }
573
+ let nLoaded = 0;
574
+ for (const [cat, rules] of pairs) {
575
+ const required = EXTRA_CATEGORIES[cat];
576
+ if (!required) {
577
+ console.warn(
578
+ `[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`
579
+ );
580
+ continue;
581
+ }
582
+ if (!Array.isArray(rules)) {
583
+ 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`);
584
+ continue;
585
+ }
586
+ for (const r of rules) {
587
+ if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
588
+ console.warn(
589
+ `[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`
590
+ );
591
+ continue;
592
+ }
593
+ const rid = r && typeof r === "object" ? String(r.id || "") : "";
594
+ if (rid) {
595
+ if (seenIds.has(rid)) {
596
+ console.warn(
597
+ `[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`
598
+ );
599
+ continue;
600
+ }
601
+ seenIds.set(rid, p);
602
+ }
603
+ (data[cat] = data[cat] || []).push(r);
604
+ nLoaded += 1;
605
+ }
606
+ }
607
+ if (nLoaded > 0) {
608
+ loaded.push(p);
609
+ console.info(`[AgentMoss] \u5DF2\u52A0\u8F7D\u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p}\uFF1A${nLoaded} \u6761`);
610
+ }
611
+ }
612
+ return { data, loaded };
613
+ }
614
+ function loadExtraRules() {
615
+ const raw = (process.env.AGENT_MOSS_EXTRA_RULES_DIR || "").trim();
616
+ const dirs = extraRulesDirs();
617
+ let files = [];
618
+ for (const d of dirs) {
619
+ if (existsSync(d)) files.push(...listJsonFilesDeep(d));
620
+ else console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${d}\uFF08AGENT_MOSS_EXTRA_RULES_DIR\uFF09`);
621
+ }
622
+ files = files.sort();
623
+ const fp = files.map((p) => {
624
+ try {
625
+ const st = statSync(p);
626
+ return [p, st.mtimeMs, st.size];
627
+ } catch {
628
+ return [p, -1, -1];
629
+ }
630
+ });
631
+ if (extraCache && extraCache.raw === raw && JSON.stringify(extraCache.fp) === JSON.stringify(fp)) {
632
+ return extraCache.data;
633
+ }
634
+ const { data, loaded } = scanExtraFiles(files);
635
+ extraCache = { raw, fp, data, loaded };
636
+ return data;
637
+ }
638
+ function extraRuleFiles() {
639
+ loadExtraRules();
640
+ return extraCache ? [...extraCache.loaded] : [];
641
+ }
642
+ var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS, EXTRA_CATEGORIES, EXTRA_FLAT_FIELD, extraCache;
462
643
  var init_rules_loader = __esm({
463
644
  "src/infra/rules-loader.ts"() {
464
645
  "use strict";
@@ -493,6 +674,22 @@ var init_rules_loader = __esm({
493
674
  ];
494
675
  return cands.filter((c) => existsSync(c));
495
676
  })();
677
+ EXTRA_CATEGORIES = {
678
+ dangerous_commands: ["pattern"],
679
+ injection_detection: ["keyword", "lang"],
680
+ sensitive_path_access: ["path"],
681
+ intent_consistency: ["intent_keywords"],
682
+ password_modify_consent: ["pattern"],
683
+ user_deletion_consent: ["pattern"]
684
+ };
685
+ EXTRA_FLAT_FIELD = [
686
+ [["path"], "sensitive_path_access"],
687
+ [["keyword", "lang"], "injection_detection"],
688
+ [["intent_keywords"], "intent_consistency"],
689
+ [["pattern", "risk_level"], "dangerous_commands"],
690
+ [["pattern", "risk_type"], "dangerous_commands"]
691
+ ];
692
+ extraCache = null;
496
693
  }
497
694
  });
498
695
 
@@ -709,7 +906,9 @@ var init_platform_traits = __esm({
709
906
  /^ag(?:\s|$)/i,
710
907
  /^rg(?:\s|$)/i,
711
908
  /^awk\s+/i,
712
- /^sed\s+/i,
909
+ // NEW-FN-P(M03):sed -i/--in-place 是原地写,挡出白名单走 L2 read_before_write。
910
+ // 前瞻放最前(外层 ^sed\s+ 会消费 -i 前的空格)。与 Python profiles/linux.py 对齐。
911
+ /^(?!.*(?:\s|['"])-[a-zA-Z]*i[a-zA-Z]*\b)(?!.*(?:\s|['"])--in-place)sed\s+/i,
713
912
  /^cut\s+/i,
714
913
  /^sort(?:\s|$)/i,
715
914
  /^uniq(?:\s|$)/i,
@@ -945,8 +1144,8 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
945
1144
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
946
1145
  const { writeCommandRes } = getPlatformTraits(osType);
947
1146
  if (writeCommandRes.some((rx) => rx.test(actionDetail))) return true;
948
- if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
949
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1147
+ if (SED_INPLACE_RE.test(actionDetail)) return true;
1148
+ for (const seg of splitCommandSegments(actionDetail)) {
950
1149
  const trimmed = seg.trim();
951
1150
  if (!trimmed) continue;
952
1151
  const parts = trimmed.split(/\s+/);
@@ -1174,11 +1373,11 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1174
1373
  const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
1175
1374
  let hasWriteSeg = hasRedirectWrite;
1176
1375
  if (!hasWriteSeg) {
1177
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1376
+ for (const seg of splitCommandSegments(actionDetail)) {
1178
1377
  const trimmed = seg.trim();
1179
1378
  if (!trimmed) continue;
1180
1379
  const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1181
- if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
1380
+ if (WRITE_CMDS.has(fw) || SED_INPLACE_RE.test(trimmed)) {
1182
1381
  hasWriteSeg = true;
1183
1382
  break;
1184
1383
  }
@@ -1197,14 +1396,15 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1197
1396
  const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
1198
1397
  const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
1199
1398
  const targetPaths = [];
1200
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1399
+ for (const seg of splitCommandSegments(actionDetail)) {
1201
1400
  const trimmed = seg.trim();
1202
1401
  if (!trimmed) continue;
1203
1402
  const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1204
1403
  if (firstWord === "cd") {
1205
1404
  continue;
1206
1405
  }
1207
- if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1406
+ if (firstWord === "sed" && SED_INPLACE_RE.test(trimmed)) {
1407
+ } else if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1208
1408
  const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
1209
1409
  const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
1210
1410
  const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
@@ -1319,7 +1519,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory, osTyp
1319
1519
  }
1320
1520
  function containsPrivilegeEscalation(actionDetail) {
1321
1521
  const ad = actionDetail.toLowerCase();
1322
- for (const seg of ad.split(/&&|\|\||;|\n/)) {
1522
+ for (const seg of splitCommandSegments(ad)) {
1323
1523
  const trimmed = seg.trim();
1324
1524
  if (!trimmed) continue;
1325
1525
  const parts = trimmed.split(/\s+/);
@@ -1408,7 +1608,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd, osType) {
1408
1608
  violatedLayers: ["L2.1"]
1409
1609
  };
1410
1610
  }
1411
- 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;
1611
+ 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;
1412
1612
  var init_logic_rules = __esm({
1413
1613
  "src/engine/logic-rules.ts"() {
1414
1614
  "use strict";
@@ -1417,6 +1617,7 @@ var init_logic_rules = __esm({
1417
1617
  init_runtime_config_bridge();
1418
1618
  init_rules_loader();
1419
1619
  init_platform_traits();
1620
+ init_shell_split();
1420
1621
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1421
1622
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1422
1623
  CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc, dirDelOnly }) => ({ path: path6, riskLevel, desc, credential: true, dirDelOnly }));
@@ -1451,6 +1652,7 @@ var init_logic_rules = __esm({
1451
1652
  ]);
1452
1653
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
1453
1654
  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"];
1655
+ SED_INPLACE_RE = /\bsed\s+(?:-\S+\s+)*(?:-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/i;
1454
1656
  DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
1455
1657
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
1456
1658
  PASSWORD_MODIFY_PATTERNS = (() => {
@@ -1527,7 +1729,7 @@ var init_asset_dir = __esm({
1527
1729
  });
1528
1730
 
1529
1731
  // src/engine/skill-loader.ts
1530
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
1732
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
1531
1733
  import { resolve as resolve3, dirname as dirname2 } from "path";
1532
1734
  import { fileURLToPath as fileURLToPath3 } from "url";
1533
1735
  function loadAllSkills() {
@@ -1539,7 +1741,7 @@ function loadAllSkills() {
1539
1741
  console.warn("[AgentMoss] Skills directory not found:", primary, "and fallback:", fallback);
1540
1742
  return cachedSkills;
1541
1743
  }
1542
- const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
1744
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".md"));
1543
1745
  for (const f of files) {
1544
1746
  const name = f.replace(".md", "");
1545
1747
  cachedSkills[name] = readFileSync3(resolve3(dir, f), "utf-8");
@@ -1718,7 +1920,7 @@ __export(runtime_config_exports, {
1718
1920
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1719
1921
  updateSkillEnabled: () => updateSkillEnabled
1720
1922
  });
1721
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync3, statSync } from "fs";
1923
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1722
1924
  import { join as join3 } from "path";
1723
1925
  import { fileURLToPath as fileURLToPath4 } from "url";
1724
1926
  import { dirname as dirname3 } from "path";
@@ -1764,7 +1966,7 @@ function loadRuntimeConfig() {
1764
1966
  return seeded;
1765
1967
  }
1766
1968
  try {
1767
- const st = statSync(p);
1969
+ const st = statSync2(p);
1768
1970
  const cacheKey = `${p}|${st.mtimeMs}|${st.size}`;
1769
1971
  if (runtimeConfigCache && runtimeConfigCache.key === cacheKey) {
1770
1972
  return runtimeConfigCache.value;
@@ -1845,9 +2047,18 @@ function _mergeUserCustom(primary, runtime, layers, key) {
1845
2047
  const extra = _userCustomRules(runtime, layers, key);
1846
2048
  return extra.length > 0 ? [...primary, ...extra] : [...primary];
1847
2049
  }
2050
+ function _mergeExtraDirRules(primary, category) {
2051
+ try {
2052
+ const extra = loadExtraRules()[category] || [];
2053
+ return extra.length > 0 ? [...primary, ...extra] : [...primary];
2054
+ } catch {
2055
+ return [...primary];
2056
+ }
2057
+ }
1848
2058
  function getEnabledL1CommandPatterns(runtime, osType) {
1849
2059
  const cat = runtime.L1_rules?.dangerous_commands || { category_enabled: true, rules: [] };
1850
2060
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2061
+ rules = _mergeExtraDirRules(rules, "dangerous_commands");
1851
2062
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "pattern");
1852
2063
  if (osType) rules = _filterRulesByOs(rules, osType);
1853
2064
  return rules.map((r) => ({
@@ -1863,6 +2074,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
1863
2074
  cat = runtime.L2_rules?.sensitive_path_access || { category_enabled: true, rules: [] };
1864
2075
  }
1865
2076
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2077
+ rules = _mergeExtraDirRules(rules, "sensitive_path_access");
1866
2078
  rules = _mergeUserCustom(rules, runtime, ["L1_rules", "L2_rules"], "path");
1867
2079
  if (osType) rules = _filterRulesByOs(rules, osType);
1868
2080
  return rules.map((r) => {
@@ -1877,7 +2089,7 @@ function hasRuntimeConfig() {
1877
2089
  const p = getRuntimeConfigPath();
1878
2090
  let key;
1879
2091
  try {
1880
- const st = statSync(p);
2092
+ const st = statSync2(p);
1881
2093
  key = `${p}|${st.mtimeMs}|${st.size}`;
1882
2094
  } catch {
1883
2095
  return false;
@@ -2107,12 +2319,12 @@ function _scenarioRulesToDefaults() {
2107
2319
  if (!existsSync5(scenDir)) return out;
2108
2320
  let entries;
2109
2321
  try {
2110
- entries = readdirSync3(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2322
+ entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2111
2323
  } catch {
2112
2324
  return out;
2113
2325
  }
2114
2326
  for (const scenarioId of entries) {
2115
- const rulesFile = join3(scenDir, scenarioId, "rules.json");
2327
+ const rulesFile = resolveScenarioRulesFile(scenarioId) ?? join3(scenDir, scenarioId, "rules.json");
2116
2328
  if (!existsSync5(rulesFile)) continue;
2117
2329
  let data;
2118
2330
  try {
@@ -2156,7 +2368,7 @@ function _agentRulesToDefaults() {
2156
2368
  if (!existsSync5(agentsDir)) return out;
2157
2369
  let entries;
2158
2370
  try {
2159
- entries = readdirSync3(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2371
+ entries = readdirSync4(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2160
2372
  } catch {
2161
2373
  return out;
2162
2374
  }
@@ -2494,6 +2706,7 @@ function _migrateCategoriesAcrossLayers(cfg, srcLayer, dstLayer, catNames) {
2494
2706
  function getEnabledL1InjectionKeywords(runtime, osType) {
2495
2707
  const cat = runtime.L1_rules?.injection_detection || { category_enabled: true, rules: [] };
2496
2708
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2709
+ rules = _mergeExtraDirRules(rules, "injection_detection");
2497
2710
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "keyword");
2498
2711
  if (osType) rules = _filterRulesByOs(rules, osType);
2499
2712
  return rules.map((r) => ({
@@ -2505,6 +2718,7 @@ function getEnabledL1InjectionKeywords(runtime, osType) {
2505
2718
  function getEnabledL2IntentPatterns(runtime, osType) {
2506
2719
  const cat = runtime.L2_rules?.intent_consistency || { category_enabled: true, rules: [] };
2507
2720
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2721
+ rules = _mergeExtraDirRules(rules, "intent_consistency");
2508
2722
  rules = _mergeUserCustom(rules, runtime, "L2_rules", "intent_keywords");
2509
2723
  if (osType) rules = _filterRulesByOs(rules, osType);
2510
2724
  return rules.map((r) => ({
@@ -2515,12 +2729,22 @@ function getEnabledL2IntentPatterns(runtime, osType) {
2515
2729
  }
2516
2730
  function getEnabledL2PasswordPatterns(runtime) {
2517
2731
  const cat = runtime.L2_rules?.password_modify_consent || { category_enabled: true, rules: [] };
2518
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2732
+ const rules = _mergeUserCustom(
2733
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2734
+ runtime,
2735
+ "L2_rules",
2736
+ "pattern"
2737
+ );
2519
2738
  return rules.map((r) => r.pattern || "");
2520
2739
  }
2521
2740
  function getEnabledL2UserDeletionPatterns(runtime) {
2522
2741
  const cat = runtime.L2_rules?.user_deletion_consent || { category_enabled: true, rules: [] };
2523
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2742
+ const rules = _mergeUserCustom(
2743
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2744
+ runtime,
2745
+ "L2_rules",
2746
+ "pattern"
2747
+ );
2524
2748
  return rules.map((r) => r.pattern || "");
2525
2749
  }
2526
2750
  function isL2CategoryEnabled(runtime, categoryKey) {
@@ -2552,28 +2776,48 @@ function getEnabledL1CommandPatternsOrDefault(runtime, osType) {
2552
2776
  if (_hasRulesForOs(cat?.rules, osType)) {
2553
2777
  return getEnabledL1CommandPatterns(runtime, osType);
2554
2778
  }
2555
- return _mergeUserCustom(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), runtime, "L1_rules", "pattern");
2779
+ return _mergeUserCustom(
2780
+ _mergeExtraDirRules(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2781
+ runtime,
2782
+ "L1_rules",
2783
+ "pattern"
2784
+ );
2556
2785
  }
2557
2786
  function getEnabledL1InjectionKeywordsOrDefault(runtime, osType) {
2558
2787
  const cat = runtime.L1_rules?.injection_detection;
2559
2788
  if (_hasRulesForOs(cat?.rules, osType)) {
2560
2789
  return getEnabledL1InjectionKeywords(runtime, osType);
2561
2790
  }
2562
- return _mergeUserCustom(_l1InjectionKeywordsToRules(), runtime, "L1_rules", "keyword");
2791
+ return _mergeUserCustom(
2792
+ _mergeExtraDirRules(_l1InjectionKeywordsToRules(), "injection_detection"),
2793
+ runtime,
2794
+ "L1_rules",
2795
+ "keyword"
2796
+ );
2563
2797
  }
2564
2798
  function getEnabledL2SensitivePathsOrDefault(runtime, osType) {
2565
2799
  const spCat = _sensitivePathCategoryOf(runtime);
2566
2800
  if (_hasRulesForOs(spCat?.rules, osType)) {
2567
2801
  return getEnabledL2SensitivePaths(runtime, osType);
2568
2802
  }
2569
- return _mergeUserCustom(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), runtime, ["L1_rules", "L2_rules"], "path");
2803
+ return _mergeUserCustom(
2804
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2805
+ runtime,
2806
+ ["L1_rules", "L2_rules"],
2807
+ "path"
2808
+ );
2570
2809
  }
2571
2810
  function getEnabledL2IntentPatternsOrDefault(runtime, osType) {
2572
2811
  const cat = runtime.L2_rules?.intent_consistency;
2573
2812
  if (_hasRulesForOs(cat?.rules, osType)) {
2574
2813
  return getEnabledL2IntentPatterns(runtime, osType);
2575
2814
  }
2576
- return _mergeUserCustom(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), runtime, "L2_rules", "intent_keywords");
2815
+ return _mergeUserCustom(
2816
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2817
+ runtime,
2818
+ "L2_rules",
2819
+ "intent_keywords"
2820
+ );
2577
2821
  }
2578
2822
  function getEnabledL2PasswordPatternsOrDefault(runtime, osType) {
2579
2823
  const cat = runtime.L2_rules?.password_modify_consent;
@@ -2969,6 +3213,7 @@ var init_runtime_config = __esm({
2969
3213
  init_heuristic();
2970
3214
  init_logic_rules();
2971
3215
  init_skill_loader();
3216
+ init_rules_loader();
2972
3217
  init_runtime_config_bridge();
2973
3218
  __dirname4 = dirname3(fileURLToPath4(import.meta.url));
2974
3219
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
@@ -3029,7 +3274,9 @@ __export(config_exports, {
3029
3274
  isLLMAvailable: () => isLLMAvailable,
3030
3275
  isLogicRulesEnabled: () => isLogicRulesEnabled,
3031
3276
  probeLLMConnection: () => probeLLMConnection,
3032
- updateLLMConfig: () => updateLLMConfig
3277
+ updateLLMConfig: () => updateLLMConfig,
3278
+ userRuleVariantPattern: () => userRuleVariantPattern,
3279
+ wildcardToRegExp: () => wildcardToRegExp
3033
3280
  });
3034
3281
  import fs2 from "fs";
3035
3282
  import path2 from "path";
@@ -3126,6 +3373,8 @@ function loadCustomRules() {
3126
3373
  const p = r?.pattern;
3127
3374
  if (!p) continue;
3128
3375
  rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
3376
+ const variant = userRuleVariantPattern(p);
3377
+ if (variant) rules.push({ pattern: wildcardToRegExp(variant), action: "Deny", severity: "high" });
3129
3378
  }
3130
3379
  break;
3131
3380
  } catch {
@@ -3133,6 +3382,15 @@ function loadCustomRules() {
3133
3382
  }
3134
3383
  return rules;
3135
3384
  }
3385
+ function userRuleVariantPattern(pattern) {
3386
+ const tokens = pattern.split(/\s+/);
3387
+ if (tokens.length >= 3 && tokens[1] === "*" && tokens[2] !== "*") {
3388
+ let variant = [tokens[0], ...tokens.slice(2)].join(" ");
3389
+ if (!variant.endsWith("*")) variant += " *";
3390
+ return variant;
3391
+ }
3392
+ return null;
3393
+ }
3136
3394
  function getLLMConfig() {
3137
3395
  return llmConfig;
3138
3396
  }
@@ -3369,15 +3627,27 @@ function getScenarioExtensions(scenarioId) {
3369
3627
  if (!scenarioId) return null;
3370
3628
  if (cache2.has(scenarioId)) return cache2.get(scenarioId) ?? null;
3371
3629
  let ext = null;
3630
+ let overrideHit = null;
3372
3631
  try {
3373
- if (hasRuntimeConfig()) {
3374
- ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3375
- }
3632
+ overrideHit = resolveScenarioRulesFile(scenarioId);
3376
3633
  } catch {
3377
3634
  }
3635
+ if (!overrideHit) {
3636
+ try {
3637
+ if (hasRuntimeConfig()) {
3638
+ ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3639
+ }
3640
+ } catch {
3641
+ }
3642
+ }
3378
3643
  if (ext === null) {
3379
- const scenariosDir = resolveScenariosDir();
3380
- const rulesFile = resolve5(scenariosDir, scenarioId, "rules.json");
3644
+ const builtinFile = resolve5(resolveScenariosDir(), scenarioId, "rules.json");
3645
+ const rulesFile = overrideHit ?? builtinFile;
3646
+ if (overrideHit) {
3647
+ console.warn(
3648
+ `[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`
3649
+ );
3650
+ }
3381
3651
  if (!existsSync7(rulesFile)) {
3382
3652
  cache2.set(scenarioId, null);
3383
3653
  return null;
@@ -3429,6 +3699,7 @@ var init_scenarios = __esm({
3429
3699
  init_esm_shims();
3430
3700
  init_agents();
3431
3701
  init_runtime_config();
3702
+ init_rules_loader();
3432
3703
  __filename5 = fileURLToPath7(import.meta.url);
3433
3704
  __dirname6 = dirname5(__filename5);
3434
3705
  SCENARIOS_DIR_PROD = resolve5(__dirname6, "scenarios");
@@ -3494,16 +3765,41 @@ function scanDangerousCommandPatterns(actionType, actionDetail, osType) {
3494
3765
  violatedLayers: ["L1.1"]
3495
3766
  };
3496
3767
  }
3768
+ function allUrlsLoopback(actionDetail) {
3769
+ const hosts = actionDetail.match(new RegExp(LOOPBACK_URL_RE.source, "gi"));
3770
+ if (!hosts || hosts.length === 0) return false;
3771
+ return hosts.every((u) => {
3772
+ const raw = u.replace(/^https?:\/\//i, "");
3773
+ const host = raw.split("@").pop().split(":")[0].replace(/^\[|\]$/g, "").toLowerCase();
3774
+ return LOOPBACK_HOSTS.has(host) || host.startsWith("127.");
3775
+ });
3776
+ }
3777
+ function stripEchoParamsForUserRules(actionDetail) {
3778
+ return splitCommandSegments2(actionDetail).map((seg) => {
3779
+ const m = seg.match(/^(echo|printf)(?![a-zA-Z0-9_])\s*/i);
3780
+ if (!m) return seg;
3781
+ const head = m[1].toLowerCase();
3782
+ const rest = seg.slice(m[0].length);
3783
+ const cut = rest.search(/>+|\|/);
3784
+ return `${head} ` + (cut >= 0 ? rest.slice(cut) : "");
3785
+ }).join(" ");
3786
+ }
3497
3787
  function runHeuristicCheck(input) {
3498
3788
  const command = `${input.aNext.actionType}: ${input.aNext.actionDetail}`;
3499
3789
  const matched = [];
3500
3790
  let highestSeverity = "low";
3501
3791
  let highestRiskType = "";
3502
3792
  const userRules = getCustomRules();
3793
+ const atLower = input.aNext.actionType.toLowerCase();
3794
+ const isBashLike = ["bash", "executebash", "terminal-executebash"].includes(atLower);
3795
+ const urlsAllLoopback = allUrlsLoopback(input.aNext.actionDetail);
3796
+ const userRuleScanDetail = isBashLike ? stripEchoParamsForUserRules(input.aNext.actionDetail) : input.aNext.actionDetail;
3797
+ const scanCommand = `${input.aNext.actionType}: ${userRuleScanDetail}`;
3503
3798
  for (const rule of userRules) {
3504
3799
  try {
3505
3800
  const re = new RegExp(rule.pattern);
3506
- if (re.test(command)) {
3801
+ if (re.test(command) || re.test(scanCommand)) {
3802
+ if (urlsAllLoopback) continue;
3507
3803
  if (rule.action === "Deny") {
3508
3804
  return {
3509
3805
  allowed: false,
@@ -3686,6 +3982,23 @@ function buildPathContext(actionDetail) {
3686
3982
  if (!lines.length) return "";
3687
3983
  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";
3688
3984
  }
3985
+ function isPurePrintCode(code) {
3986
+ if (code.includes("\\")) return false;
3987
+ if (PY_EXEC_IO_RE.test(code) || PY_BIGNUM_RE.test(code)) return false;
3988
+ return PY_PRINT_RE.test(code) || !/\w+\s*\(/.test(code);
3989
+ }
3990
+ function stripInlinePythonLiterals(text) {
3991
+ const subst = (_m, g1, g2, g3) => isPurePrintCode(g2 ?? g3 ?? "") ? "$(<<pylit>>)" : _m;
3992
+ const plain = (_m, g1, g2, g3) => {
3993
+ if (!isPurePrintCode(g2 ?? g3 ?? "")) return _m;
3994
+ const q = _m.trimEnd().endsWith('"') ? '"' : "'";
3995
+ return `${g1} -c ${q}<<pylit>>${q}`;
3996
+ };
3997
+ let out = text;
3998
+ out = out.replace(new RegExp(PY_CMD_SUBST_C_RE.source, PY_CMD_SUBST_C_RE.flags), subst);
3999
+ out = out.replace(new RegExp(PY_INLINE_C_RE.source, PY_INLINE_C_RE.flags), plain);
4000
+ return out;
4001
+ }
3689
4002
  function stripLiteralTextSegments(actionDetail) {
3690
4003
  const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
3691
4004
  const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node|crontab|at)\b/.test(actionDetail);
@@ -3696,13 +4009,14 @@ function stripLiteralTextSegments(actionDetail) {
3696
4009
  for (const seg of splitCommandSegments2(actionDetail)) {
3697
4010
  const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3698
4011
  if (fw === "echo" || fw === "printf") {
3699
- const segUnquotedRedirect = seg.replace(
4012
+ const segPy = stripInlinePythonLiterals(seg);
4013
+ const segUnquotedRedirect = segPy.replace(
3700
4014
  />{1,2}\s*(['"])([~./][^'"]+)\1/g,
3701
4015
  (_m, _q, p) => ">" + p
3702
4016
  );
3703
4017
  resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
3704
4018
  } else {
3705
- resultParts.push(seg);
4019
+ resultParts.push(stripInlinePythonLiterals(seg));
3706
4020
  }
3707
4021
  }
3708
4022
  return resultParts.join("");
@@ -4226,7 +4540,7 @@ function checkLateralMovement(actionDetail) {
4226
4540
  }
4227
4541
  return null;
4228
4542
  }
4229
- 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;
4543
+ 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;
4230
4544
  var init_heuristic = __esm({
4231
4545
  "src/engine/heuristic.ts"() {
4232
4546
  "use strict";
@@ -4250,8 +4564,15 @@ var init_heuristic = __esm({
4250
4564
  DANGEROUS_PATTERNS_WINDOWS = _loadDangerousPatterns("windows");
4251
4565
  DANGEROUS_PATTERNS_MACOS = _loadDangerousPatterns("macos");
4252
4566
  DANGEROUS_PATTERNS_HARMONY = _loadDangerousPatterns("harmony");
4567
+ LOOPBACK_URL_RE = /https?:\/\/([^\/\s;'"|&)]+)/i;
4568
+ LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "::1", "0.0.0.0", "[::1]"]);
4253
4569
  PATH_CONTEXT_TRIGGER_RE = /(?:^|[;&|]\s*)(?:rm|rmdir|unlink|ln|mv|cp|dd|truncate|shred|chmod|chown)\b/i;
4254
4570
  HOME_PREFIX_RE = /(?:~|~(?=\/)|\$HOME\b|\$\{HOME\}|\/home\/[a-z0-9_.-]+)/gi;
4571
+ 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;
4572
+ PY_PRINT_RE = /\b(?:print|sys\.stdout\.write)\s*\(/;
4573
+ PY_BIGNUM_RE = /\d{6,}/;
4574
+ PY_INLINE_C_RE = /(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"|'([^']*)')/i;
4575
+ PY_CMD_SUBST_C_RE = /\$\(\s*(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"'|'([^']*)')\s*\)/i;
4255
4576
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
4256
4577
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
4257
4578
  HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
@@ -5724,6 +6045,27 @@ function parseIf(content, args) {
5724
6045
  ${chosen}
5725
6046
  ${content.slice(fiPos + 2)}`;
5726
6047
  }
6048
+ function stripNonExecutableLines(content) {
6049
+ if (!content) return content;
6050
+ const out = [];
6051
+ let heredocEnd = null;
6052
+ const lines = content.split("\n");
6053
+ for (let i = 0; i < lines.length; i++) {
6054
+ const line = lines[i];
6055
+ if (heredocEnd !== null) {
6056
+ if (line.trim() === heredocEnd) heredocEnd = null;
6057
+ out.push(line);
6058
+ continue;
6059
+ }
6060
+ const m = HEREDOC_RE.exec(line);
6061
+ if (m) heredocEnd = m[1];
6062
+ const stripped = line.replace(/^\s+/, "");
6063
+ if (i === 0 && stripped.startsWith("#!")) continue;
6064
+ if (stripped.startsWith("#")) continue;
6065
+ out.push(line);
6066
+ }
6067
+ return out.join("\n");
6068
+ }
5727
6069
  function markLoopsReachable(content) {
5728
6070
  return content;
5729
6071
  }
@@ -5745,12 +6087,13 @@ function extractCallsFromContent(content) {
5745
6087
  }
5746
6088
  return calls;
5747
6089
  }
5748
- var CHILD_CALL_RE2, ShellBackend;
6090
+ var HEREDOC_RE, CHILD_CALL_RE2, ShellBackend;
5749
6091
  var init_shell_backend = __esm({
5750
6092
  "src/engine/script-engine/backends/shell-backend.ts"() {
5751
6093
  "use strict";
5752
6094
  init_esm_shims();
5753
6095
  init_symbolic();
6096
+ HEREDOC_RE = /<<-?\s*['"]?(\w+)['"]?/;
5754
6097
  CHILD_CALL_RE2 = /(?:^|\s|&&|\|\|)\s*((?:\/bin\/)?bash|sh|zsh|(?:python|python3|node|ruby|perl|php|lua))\s+(\S+(?:\.(?:sh|shrc|py|js|rb|pl|php|lua))?)([^\n;|&]*?)$/gmi;
5755
6098
  ShellBackend = class {
5756
6099
  language = "shell";
@@ -5780,6 +6123,7 @@ var init_shell_backend = __esm({
5780
6123
  bound = symOut;
5781
6124
  }
5782
6125
  }
6126
+ bound = stripNonExecutableLines(bound);
5783
6127
  const loopMarked = markLoopsReachable(bound);
5784
6128
  const calls = extractCallsFromContent(loopMarked);
5785
6129
  return {
@@ -6004,7 +6348,7 @@ var init_tracker = __esm({
6004
6348
  });
6005
6349
 
6006
6350
  // src/engine/script-content-analyzer.ts
6007
- import { statSync as statSync3, existsSync as existsSync10 } from "fs";
6351
+ import { statSync as statSync4, existsSync as existsSync10 } from "fs";
6008
6352
  import { resolve as resolve9, join as join6, isAbsolute as isAbsolute2 } from "path";
6009
6353
  import { homedir as homedir3 } from "os";
6010
6354
  function emptyChain() {
@@ -6118,7 +6462,7 @@ function extractInlineScriptPath(actionDetail) {
6118
6462
  try {
6119
6463
  const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
6120
6464
  if (existsSync10(resolved)) {
6121
- const stat = statSync3(resolved);
6465
+ const stat = statSync4(resolved);
6122
6466
  if (stat.isFile()) return resolved;
6123
6467
  }
6124
6468
  } catch {
@@ -6132,7 +6476,7 @@ function extractInlineScriptPath(actionDetail) {
6132
6476
  try {
6133
6477
  const resolved = resolvePath2(p);
6134
6478
  if (existsSync10(resolved)) {
6135
- const stat = statSync3(resolved);
6479
+ const stat = statSync4(resolved);
6136
6480
  if (stat.isFile()) return resolved;
6137
6481
  }
6138
6482
  } catch {
@@ -6148,7 +6492,7 @@ function extractInlineScriptPath(actionDetail) {
6148
6492
  try {
6149
6493
  resolved = resolvePath2(p);
6150
6494
  if (existsSync10(resolved)) {
6151
- const stat = statSync3(resolved);
6495
+ const stat = statSync4(resolved);
6152
6496
  if (stat.isFile()) return resolved;
6153
6497
  }
6154
6498
  } catch {
@@ -6192,7 +6536,30 @@ function resolvePath2(p) {
6192
6536
  }
6193
6537
  return resolve9(expanded);
6194
6538
  }
6195
- function scanScriptChainWithL1(chain, input) {
6539
+ function scanExtensionPatterns(ext, content) {
6540
+ const pats = ext?.extra_command_patterns;
6541
+ if (!pats || pats.length === 0) return null;
6542
+ for (const p of pats) {
6543
+ const src = typeof p === "string" ? p : p?.pattern;
6544
+ if (!src) continue;
6545
+ let re;
6546
+ try {
6547
+ re = new RegExp(src, "i");
6548
+ } catch {
6549
+ continue;
6550
+ }
6551
+ if (re.test(content)) {
6552
+ const risk = typeof p === "string" ? "high" : p.risk_level || "high";
6553
+ return {
6554
+ riskLevel: risk,
6555
+ riskType: typeof p === "string" ? "sensitive_action" : p.risk_type || "sensitive_action",
6556
+ reason: typeof p === "string" ? `\u547D\u4E2D\u6269\u5C55\u89C4\u5219: ${src}` : p.reason || `\u547D\u4E2D\u6269\u5C55\u89C4\u5219: ${src}`
6557
+ };
6558
+ }
6559
+ }
6560
+ return null;
6561
+ }
6562
+ function scanScriptChainWithL1(chain, input, ext) {
6196
6563
  for (const node of chain.nodes) {
6197
6564
  if (node.error || !node.preprocessedContent || node.l1Hit) continue;
6198
6565
  const virtualInput = {
@@ -6200,6 +6567,19 @@ function scanScriptChainWithL1(chain, input) {
6200
6567
  aNext: { actionType: "bash", actionDetail: node.preprocessedContent }
6201
6568
  };
6202
6569
  const l1 = scanDangerousCommandPatterns("bash", node.preprocessedContent, input.osType);
6570
+ const extHit = scanExtensionPatterns(ext, node.preprocessedContent);
6571
+ if (extHit) {
6572
+ node.l1Hit = true;
6573
+ node.l1RiskLevel = extHit.riskLevel;
6574
+ node.l1RiskType = extHit.riskType;
6575
+ node.l1Reason = extHit.reason;
6576
+ if (extHit.riskLevel === "critical") {
6577
+ chain.hasCritical = true;
6578
+ chain.firstHitNode = node;
6579
+ break;
6580
+ }
6581
+ continue;
6582
+ }
6203
6583
  if (l1 && !l1.allowed) {
6204
6584
  node.l1Hit = true;
6205
6585
  node.l1RiskLevel = l1.riskLevel;
@@ -6263,7 +6643,7 @@ function scanScriptChainKeywords(chain) {
6263
6643
  }
6264
6644
  return chain;
6265
6645
  }
6266
- function analyzeScriptContent2(input) {
6646
+ function analyzeScriptContent2(input, ext) {
6267
6647
  const actionType = input.aNext.actionType.toLowerCase();
6268
6648
  if (actionType !== "bash") return emptyChain();
6269
6649
  const { path: scriptPath, args: scriptArgs } = extractScriptPathWithArgs(input.aNext.actionDetail);
@@ -6295,7 +6675,7 @@ function analyzeScriptContent2(input) {
6295
6675
  const tracker = new RecursiveTracker(l1Adapter);
6296
6676
  const chain = tracker.track(resolvedPath, isInline ? null : scriptArgs, input.cwd || process.cwd());
6297
6677
  if (chain.nodes.length === 0) return chain;
6298
- scanScriptChainWithL1(chain, input);
6678
+ scanScriptChainWithL1(chain, input, ext);
6299
6679
  scanScriptChainWithL2(chain, input);
6300
6680
  scanScriptChainKeywords(chain);
6301
6681
  return chain;
@@ -8973,9 +9353,13 @@ async function runAnalysis(input) {
8973
9353
  }
8974
9354
  }
8975
9355
  }
9356
+ const agentExt = mergeAgentExtensions(
9357
+ isLayerEnabled("agent_rules_enabled") ? getAgentExtensions(input.agentId) : null,
9358
+ isLayerEnabled("scenario_rules_enabled") ? getScenarioExtensionsMulti(input.scenarioId || process.env.AGENT_MOSS_SCENARIO || "") : null
9359
+ );
8976
9360
  let scriptChain = null;
8977
9361
  if (input.aNext.actionType.toLowerCase() === "bash") {
8978
- scriptChain = analyzeScriptContent2(input);
9362
+ scriptChain = analyzeScriptContent2(input, agentExt);
8979
9363
  if (scriptChain.nodes.length > 0) {
8980
9364
  for (const node of scriptChain.nodes) {
8981
9365
  if (node.unanalyzable) {
@@ -9027,10 +9411,6 @@ async function runAnalysis(input) {
9027
9411
  const actionType = input.aNext.actionType.toLowerCase();
9028
9412
  const rawDetail = extractRawCommand(input.aNext.actionDetail);
9029
9413
  const lowerDetail = rawDetail.toLowerCase();
9030
- const agentExt = mergeAgentExtensions(
9031
- isLayerEnabled("agent_rules_enabled") ? getAgentExtensions(input.agentId) : null,
9032
- isLayerEnabled("scenario_rules_enabled") ? getScenarioExtensionsMulti(input.scenarioId || process.env.AGENT_MOSS_SCENARIO || "") : null
9033
- );
9034
9414
  if (FULLY_SAFE_TOOLS.has(actionType)) {
9035
9415
  return {
9036
9416
  allowed: true,
@@ -10290,10 +10670,22 @@ function logDeploymentSelfCheck() {
10290
10670
  try {
10291
10671
  const overrides = overriddenRuleFiles();
10292
10672
  if (overrides.length > 0) {
10293
- console.error(`\u26A0\uFE0F \u68C0\u6D4B\u5230\u7528\u6237\u89C4\u5219\u8986\u76D6\u76EE\u5F55\u4E0B\u6709 ${overrides.length} \u4E2A\u6587\u4EF6\uFF0C**\u6574\u4EFD\u66FF\u6362**\u4E86\u5185\u7F6E\u89C4\u5219\uFF1A`);
10673
+ console.error(`\u26A0\uFE0F \u68C0\u6D4B\u5230 ${overrides.length} \u4E2A\u89C4\u5219\u8986\u76D6\u6587\u4EF6\uFF0C**\u6574\u4EFD\u66FF\u6362**\u4E86\u5185\u7F6E\u89C4\u5219\uFF1A`);
10294
10674
  for (const p of overrides) console.error(` ${p}`);
10295
10675
  console.error(
10296
- " \u8FD9\u4E9B\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4\uFF0C**\u5185\u7F6E\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4EEC\u4E0A\u9762**\uFF1B\u5220\u9664\u5BF9\u5E94\u6587\u4EF6\u5373\u6062\u590D\u5185\u7F6E\u3002\u82E5\u53EA\u60F3\u9010\u6761\u542F\u505C/\u589E\u5220\uFF0C\u8BF7\u6539\u7528 runtime config\uFF08Console\uFF09\u3002"
10676
+ " \u8FD9\u4E9B\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4\uFF0C**\u5185\u7F6E\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4EEC\u4E0A\u9762**\uFF1B\u5220\u9664\u5BF9\u5E94\u6587\u4EF6\uFF08\u6216\u6E05\u7A7A AGENT_MOSS_OVERRIDE_RULES_DIR\uFF09\u5373\u6062\u590D\u5185\u7F6E\u3002\u82E5\u53EA\u60F3\u9010\u6761\u542F\u505C/\u589E\u5220\uFF0C\u8BF7\u6539\u7528 runtime config\uFF08Console\uFF09\u3002"
10677
+ );
10678
+ }
10679
+ } catch {
10680
+ }
10681
+ try {
10682
+ const extras = extraRuleFiles();
10683
+ if (extras.length > 0) {
10684
+ const cats = loadExtraRules();
10685
+ const total = Object.values(cats).reduce((n, v) => n + v.length, 0);
10686
+ const catSummary = Object.entries(cats).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}\xD7${v.length}`).join(", ");
10687
+ console.log(
10688
+ `\u5DF2\u4ECE\u8FFD\u52A0\u76EE\u5F55 ${extraRulesDirs().join(", ")} \u52A0\u8F7D ${extras.length} \u4E2A\u89C4\u5219\u6587\u4EF6\uFF08\u5171 ${total} \u6761${catSummary ? `\uFF0C\u7C7B\u522B\uFF1A${catSummary}` : ""}\uFF09\u3002\u8FFD\u52A0\u8BED\u4E49\uFF1A\u5185\u7F6E\u89C4\u5219\u5168\u90E8\u4FDD\u7559\uFF0C\u8FD9\u4E9B\u89C4\u5219\u6302\u5728\u5176\u540E\u4E00\u8D77\u5224\u5B9A\u3002`
10297
10689
  );
10298
10690
  }
10299
10691
  } catch {