@kenhkl/agent-moss 0.16.5 → 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/server.js CHANGED
@@ -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, path6, data) {
385
413
  if (warnedOverride.has(path6)) return;
386
414
  warnedOverride.add(path6);
387
415
  console.warn(
388
- `[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`
416
+ `[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`
389
417
  );
390
418
  const missing = (EXPECTED_KEYS[name] || []).filter((k) => !(k in data));
391
419
  if (missing.length > 0) {
@@ -397,24 +425,34 @@ function warnOverride(name, path6, data) {
397
425
  function overriddenRuleFiles() {
398
426
  const out = [];
399
427
  for (const name of Object.keys(EXPECTED_KEYS)) {
400
- const p = resolve(overrideRulesDir(), name);
401
- if (existsSync(p)) out.push(p);
428
+ for (const dir of overrideRulesDirs()) {
429
+ const p = resolve(dir, name);
430
+ if (existsSync(p)) {
431
+ out.push(p);
432
+ break;
433
+ }
434
+ }
402
435
  }
403
436
  return out;
404
437
  }
405
438
  function findRulesFile(name) {
406
- const override = resolve(overrideRulesDir(), name);
407
- if (existsSync(override)) return override;
439
+ for (const dir of overrideRulesDirs()) {
440
+ const p = resolve(dir, name);
441
+ if (existsSync(p)) return p;
442
+ }
408
443
  for (const dir of RULES_DIR_CANDS) {
409
444
  const p = resolve(dir, name);
410
445
  if (existsSync(p)) return p;
411
446
  }
412
447
  return null;
413
448
  }
449
+ function isOverridePath(name, p) {
450
+ return overrideRulesDirs().some((dir) => resolve(dir, name) === p);
451
+ }
414
452
  function loadJson(name) {
415
453
  const p = findRulesFile(name);
416
454
  if (!p) return {};
417
- const isOverride = p === resolve(overrideRulesDir(), name);
455
+ const isOverride = isOverridePath(name, p);
418
456
  let data;
419
457
  try {
420
458
  data = JSON.parse(readFileSync2(p, "utf-8")) || {};
@@ -425,6 +463,14 @@ function loadJson(name) {
425
463
  if (isOverride) warnOverride(name, p, data);
426
464
  return data;
427
465
  }
466
+ function resolveScenarioRulesFile(scenarioId) {
467
+ const rel = join2("scenarios", scenarioId, "rules.json");
468
+ for (const dir of overrideRulesDirs()) {
469
+ const p = resolve(dir, rel);
470
+ if (existsSync(p)) return p;
471
+ }
472
+ return null;
473
+ }
428
474
  function loadL1() {
429
475
  return loadJson("l1_rules.json");
430
476
  }
@@ -457,7 +503,142 @@ function loadMC() {
457
503
  function loadToolRegistry() {
458
504
  return loadJson("tool_registry.json");
459
505
  }
460
- var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS;
506
+ function extraRulesDirs() {
507
+ return parseDirEnv("AGENT_MOSS_EXTRA_RULES_DIR");
508
+ }
509
+ function listJsonFilesDeep(dir) {
510
+ const out = [];
511
+ let entries;
512
+ try {
513
+ entries = readdirSync(dir, { withFileTypes: true }).map((d) => d.name);
514
+ } catch {
515
+ return out;
516
+ }
517
+ for (const name of entries.sort()) {
518
+ const p = resolve(dir, name);
519
+ let isDir = false;
520
+ try {
521
+ isDir = statSync(p).isDirectory();
522
+ } catch {
523
+ continue;
524
+ }
525
+ if (isDir) out.push(...listJsonFilesDeep(p));
526
+ else if (name.toLowerCase().endsWith(".json")) out.push(p);
527
+ }
528
+ return out;
529
+ }
530
+ function scanExtraFiles(files) {
531
+ const data = {};
532
+ const loaded = [];
533
+ const seenIds = /* @__PURE__ */ new Map();
534
+ const classifyFlat = (r) => {
535
+ for (const [keys, cat] of EXTRA_FLAT_FIELD) {
536
+ if (r && typeof r === "object" && keys.every((k) => r[k])) return cat;
537
+ }
538
+ return null;
539
+ };
540
+ for (const p of files) {
541
+ let parsed;
542
+ try {
543
+ parsed = JSON.parse(readFileSync2(p, "utf-8"));
544
+ } catch (e) {
545
+ console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6\u635F\u574F/\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u8DF3\u8FC7: ${p}\uFF08${e.message}\uFF09`);
546
+ continue;
547
+ }
548
+ let pairs = [];
549
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
550
+ pairs = Object.entries(parsed);
551
+ } else if (Array.isArray(parsed)) {
552
+ let flatBad = 0;
553
+ for (const r of parsed) {
554
+ const cat = classifyFlat(r);
555
+ if (cat === null) {
556
+ flatBad += 1;
557
+ continue;
558
+ }
559
+ pairs.push([cat, [r]]);
560
+ }
561
+ if (flatBad > 0) {
562
+ console.warn(
563
+ `[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`
564
+ );
565
+ }
566
+ } else {
567
+ console.warn(
568
+ `[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`
569
+ );
570
+ continue;
571
+ }
572
+ let nLoaded = 0;
573
+ for (const [cat, rules] of pairs) {
574
+ const required = EXTRA_CATEGORIES[cat];
575
+ if (!required) {
576
+ console.warn(
577
+ `[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`
578
+ );
579
+ continue;
580
+ }
581
+ if (!Array.isArray(rules)) {
582
+ 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`);
583
+ continue;
584
+ }
585
+ for (const r of rules) {
586
+ if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
587
+ console.warn(
588
+ `[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`
589
+ );
590
+ continue;
591
+ }
592
+ const rid = r && typeof r === "object" ? String(r.id || "") : "";
593
+ if (rid) {
594
+ if (seenIds.has(rid)) {
595
+ console.warn(
596
+ `[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`
597
+ );
598
+ continue;
599
+ }
600
+ seenIds.set(rid, p);
601
+ }
602
+ (data[cat] = data[cat] || []).push(r);
603
+ nLoaded += 1;
604
+ }
605
+ }
606
+ if (nLoaded > 0) {
607
+ loaded.push(p);
608
+ console.info(`[AgentMoss] \u5DF2\u52A0\u8F7D\u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p}\uFF1A${nLoaded} \u6761`);
609
+ }
610
+ }
611
+ return { data, loaded };
612
+ }
613
+ function loadExtraRules() {
614
+ const raw = (process.env.AGENT_MOSS_EXTRA_RULES_DIR || "").trim();
615
+ const dirs = extraRulesDirs();
616
+ let files = [];
617
+ for (const d of dirs) {
618
+ if (existsSync(d)) files.push(...listJsonFilesDeep(d));
619
+ else console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${d}\uFF08AGENT_MOSS_EXTRA_RULES_DIR\uFF09`);
620
+ }
621
+ files = files.sort();
622
+ const fp = files.map((p) => {
623
+ try {
624
+ const st = statSync(p);
625
+ return [p, st.mtimeMs, st.size];
626
+ } catch {
627
+ return [p, -1, -1];
628
+ }
629
+ });
630
+ if (extraCache && extraCache.raw === raw && JSON.stringify(extraCache.fp) === JSON.stringify(fp)) {
631
+ return extraCache.data;
632
+ }
633
+ const { data, loaded } = scanExtraFiles(files);
634
+ extraCache = { raw, fp, data, loaded };
635
+ return data;
636
+ }
637
+ function extraRuleFiles() {
638
+ loadExtraRules();
639
+ return extraCache ? [...extraCache.loaded] : [];
640
+ }
641
+ var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS, EXTRA_CATEGORIES, EXTRA_FLAT_FIELD, extraCache;
461
642
  var init_rules_loader = __esm({
462
643
  "src/infra/rules-loader.ts"() {
463
644
  "use strict";
@@ -492,6 +673,22 @@ var init_rules_loader = __esm({
492
673
  ];
493
674
  return cands.filter((c) => existsSync(c));
494
675
  })();
676
+ EXTRA_CATEGORIES = {
677
+ dangerous_commands: ["pattern"],
678
+ injection_detection: ["keyword", "lang"],
679
+ sensitive_path_access: ["path"],
680
+ intent_consistency: ["intent_keywords"],
681
+ password_modify_consent: ["pattern"],
682
+ user_deletion_consent: ["pattern"]
683
+ };
684
+ EXTRA_FLAT_FIELD = [
685
+ [["path"], "sensitive_path_access"],
686
+ [["keyword", "lang"], "injection_detection"],
687
+ [["intent_keywords"], "intent_consistency"],
688
+ [["pattern", "risk_level"], "dangerous_commands"],
689
+ [["pattern", "risk_type"], "dangerous_commands"]
690
+ ];
691
+ extraCache = null;
495
692
  }
496
693
  });
497
694
 
@@ -708,7 +905,9 @@ var init_platform_traits = __esm({
708
905
  /^ag(?:\s|$)/i,
709
906
  /^rg(?:\s|$)/i,
710
907
  /^awk\s+/i,
711
- /^sed\s+/i,
908
+ // NEW-FN-P(M03):sed -i/--in-place 是原地写,挡出白名单走 L2 read_before_write。
909
+ // 前瞻放最前(外层 ^sed\s+ 会消费 -i 前的空格)。与 Python profiles/linux.py 对齐。
910
+ /^(?!.*(?:\s|['"])-[a-zA-Z]*i[a-zA-Z]*\b)(?!.*(?:\s|['"])--in-place)sed\s+/i,
712
911
  /^cut\s+/i,
713
912
  /^sort(?:\s|$)/i,
714
913
  /^uniq(?:\s|$)/i,
@@ -944,8 +1143,8 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
944
1143
  if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
945
1144
  const { writeCommandRes } = getPlatformTraits(osType);
946
1145
  if (writeCommandRes.some((rx) => rx.test(actionDetail))) return true;
947
- if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
948
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1146
+ if (SED_INPLACE_RE.test(actionDetail)) return true;
1147
+ for (const seg of splitCommandSegments(actionDetail)) {
949
1148
  const trimmed = seg.trim();
950
1149
  if (!trimmed) continue;
951
1150
  const parts = trimmed.split(/\s+/);
@@ -1173,11 +1372,11 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1173
1372
  const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
1174
1373
  let hasWriteSeg = hasRedirectWrite;
1175
1374
  if (!hasWriteSeg) {
1176
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1375
+ for (const seg of splitCommandSegments(actionDetail)) {
1177
1376
  const trimmed = seg.trim();
1178
1377
  if (!trimmed) continue;
1179
1378
  const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1180
- if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
1379
+ if (WRITE_CMDS.has(fw) || SED_INPLACE_RE.test(trimmed)) {
1181
1380
  hasWriteSeg = true;
1182
1381
  break;
1183
1382
  }
@@ -1196,14 +1395,15 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
1196
1395
  const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
1197
1396
  const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
1198
1397
  const targetPaths = [];
1199
- for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
1398
+ for (const seg of splitCommandSegments(actionDetail)) {
1200
1399
  const trimmed = seg.trim();
1201
1400
  if (!trimmed) continue;
1202
1401
  const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
1203
1402
  if (firstWord === "cd") {
1204
1403
  continue;
1205
1404
  }
1206
- if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1405
+ if (firstWord === "sed" && SED_INPLACE_RE.test(trimmed)) {
1406
+ } else if (getPlatformTraits(osType).readOnlyCommands.has(firstWord)) {
1207
1407
  const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
1208
1408
  const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
1209
1409
  const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
@@ -1318,7 +1518,7 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory, osTyp
1318
1518
  }
1319
1519
  function containsPrivilegeEscalation(actionDetail) {
1320
1520
  const ad = actionDetail.toLowerCase();
1321
- for (const seg of ad.split(/&&|\|\||;|\n/)) {
1521
+ for (const seg of splitCommandSegments(ad)) {
1322
1522
  const trimmed = seg.trim();
1323
1523
  if (!trimmed) continue;
1324
1524
  const parts = trimmed.split(/\s+/);
@@ -1407,7 +1607,7 @@ function checkIndirectFileAccess(actionType, actionDetail, cwd, osType) {
1407
1607
  violatedLayers: ["L2.1"]
1408
1608
  };
1409
1609
  }
1410
- 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;
1610
+ 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;
1411
1611
  var init_logic_rules = __esm({
1412
1612
  "src/engine/logic-rules.ts"() {
1413
1613
  "use strict";
@@ -1416,6 +1616,7 @@ var init_logic_rules = __esm({
1416
1616
  init_runtime_config_bridge();
1417
1617
  init_rules_loader();
1418
1618
  init_platform_traits();
1619
+ init_shell_split();
1419
1620
  ROOT_SENSITIVE_PATHS = _loadSensitivePathScope("root");
1420
1621
  HOME_SENSITIVE_PATHS = _loadSensitivePathScope("home");
1421
1622
  CREDENTIAL_PATHS = _loadSensitivePathScope("credential").map(({ path: path6, riskLevel, desc, dirDelOnly }) => ({ path: path6, riskLevel, desc, credential: true, dirDelOnly }));
@@ -1450,6 +1651,7 @@ var init_logic_rules = __esm({
1450
1651
  ]);
1451
1652
  NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
1452
1653
  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"];
1654
+ SED_INPLACE_RE = /\bsed\s+(?:-\S+\s+)*(?:-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/i;
1453
1655
  DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
1454
1656
  CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
1455
1657
  PASSWORD_MODIFY_PATTERNS = (() => {
@@ -1526,7 +1728,7 @@ var init_asset_dir = __esm({
1526
1728
  });
1527
1729
 
1528
1730
  // src/engine/skill-loader.ts
1529
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
1731
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
1530
1732
  import { resolve as resolve3, dirname as dirname2 } from "path";
1531
1733
  import { fileURLToPath as fileURLToPath3 } from "url";
1532
1734
  function loadAllSkills() {
@@ -1538,7 +1740,7 @@ function loadAllSkills() {
1538
1740
  console.warn("[AgentMoss] Skills directory not found:", primary, "and fallback:", fallback);
1539
1741
  return cachedSkills;
1540
1742
  }
1541
- const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
1743
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".md"));
1542
1744
  for (const f of files) {
1543
1745
  const name = f.replace(".md", "");
1544
1746
  cachedSkills[name] = readFileSync3(resolve3(dir, f), "utf-8");
@@ -1717,7 +1919,7 @@ __export(runtime_config_exports, {
1717
1919
  updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
1718
1920
  updateSkillEnabled: () => updateSkillEnabled
1719
1921
  });
1720
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync3, statSync } from "fs";
1922
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync, unlinkSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1721
1923
  import { join as join3 } from "path";
1722
1924
  import { fileURLToPath as fileURLToPath4 } from "url";
1723
1925
  import { dirname as dirname3 } from "path";
@@ -1763,7 +1965,7 @@ function loadRuntimeConfig() {
1763
1965
  return seeded;
1764
1966
  }
1765
1967
  try {
1766
- const st = statSync(p);
1968
+ const st = statSync2(p);
1767
1969
  const cacheKey = `${p}|${st.mtimeMs}|${st.size}`;
1768
1970
  if (runtimeConfigCache && runtimeConfigCache.key === cacheKey) {
1769
1971
  return runtimeConfigCache.value;
@@ -1844,9 +2046,18 @@ function _mergeUserCustom(primary, runtime, layers, key) {
1844
2046
  const extra = _userCustomRules(runtime, layers, key);
1845
2047
  return extra.length > 0 ? [...primary, ...extra] : [...primary];
1846
2048
  }
2049
+ function _mergeExtraDirRules(primary, category) {
2050
+ try {
2051
+ const extra = loadExtraRules()[category] || [];
2052
+ return extra.length > 0 ? [...primary, ...extra] : [...primary];
2053
+ } catch {
2054
+ return [...primary];
2055
+ }
2056
+ }
1847
2057
  function getEnabledL1CommandPatterns(runtime, osType) {
1848
2058
  const cat = runtime.L1_rules?.dangerous_commands || { category_enabled: true, rules: [] };
1849
2059
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2060
+ rules = _mergeExtraDirRules(rules, "dangerous_commands");
1850
2061
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "pattern");
1851
2062
  if (osType) rules = _filterRulesByOs(rules, osType);
1852
2063
  return rules.map((r) => ({
@@ -1862,6 +2073,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
1862
2073
  cat = runtime.L2_rules?.sensitive_path_access || { category_enabled: true, rules: [] };
1863
2074
  }
1864
2075
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2076
+ rules = _mergeExtraDirRules(rules, "sensitive_path_access");
1865
2077
  rules = _mergeUserCustom(rules, runtime, ["L1_rules", "L2_rules"], "path");
1866
2078
  if (osType) rules = _filterRulesByOs(rules, osType);
1867
2079
  return rules.map((r) => {
@@ -1876,7 +2088,7 @@ function hasRuntimeConfig() {
1876
2088
  const p = getRuntimeConfigPath();
1877
2089
  let key;
1878
2090
  try {
1879
- const st = statSync(p);
2091
+ const st = statSync2(p);
1880
2092
  key = `${p}|${st.mtimeMs}|${st.size}`;
1881
2093
  } catch {
1882
2094
  return false;
@@ -2106,12 +2318,12 @@ function _scenarioRulesToDefaults() {
2106
2318
  if (!existsSync5(scenDir)) return out;
2107
2319
  let entries;
2108
2320
  try {
2109
- entries = readdirSync3(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2321
+ entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2110
2322
  } catch {
2111
2323
  return out;
2112
2324
  }
2113
2325
  for (const scenarioId of entries) {
2114
- const rulesFile = join3(scenDir, scenarioId, "rules.json");
2326
+ const rulesFile = resolveScenarioRulesFile(scenarioId) ?? join3(scenDir, scenarioId, "rules.json");
2115
2327
  if (!existsSync5(rulesFile)) continue;
2116
2328
  let data;
2117
2329
  try {
@@ -2155,7 +2367,7 @@ function _agentRulesToDefaults() {
2155
2367
  if (!existsSync5(agentsDir)) return out;
2156
2368
  let entries;
2157
2369
  try {
2158
- entries = readdirSync3(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2370
+ entries = readdirSync4(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2159
2371
  } catch {
2160
2372
  return out;
2161
2373
  }
@@ -2493,6 +2705,7 @@ function _migrateCategoriesAcrossLayers(cfg, srcLayer, dstLayer, catNames) {
2493
2705
  function getEnabledL1InjectionKeywords(runtime, osType) {
2494
2706
  const cat = runtime.L1_rules?.injection_detection || { category_enabled: true, rules: [] };
2495
2707
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2708
+ rules = _mergeExtraDirRules(rules, "injection_detection");
2496
2709
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "keyword");
2497
2710
  if (osType) rules = _filterRulesByOs(rules, osType);
2498
2711
  return rules.map((r) => ({
@@ -2504,6 +2717,7 @@ function getEnabledL1InjectionKeywords(runtime, osType) {
2504
2717
  function getEnabledL2IntentPatterns(runtime, osType) {
2505
2718
  const cat = runtime.L2_rules?.intent_consistency || { category_enabled: true, rules: [] };
2506
2719
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2720
+ rules = _mergeExtraDirRules(rules, "intent_consistency");
2507
2721
  rules = _mergeUserCustom(rules, runtime, "L2_rules", "intent_keywords");
2508
2722
  if (osType) rules = _filterRulesByOs(rules, osType);
2509
2723
  return rules.map((r) => ({
@@ -2514,12 +2728,22 @@ function getEnabledL2IntentPatterns(runtime, osType) {
2514
2728
  }
2515
2729
  function getEnabledL2PasswordPatterns(runtime) {
2516
2730
  const cat = runtime.L2_rules?.password_modify_consent || { category_enabled: true, rules: [] };
2517
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2731
+ const rules = _mergeUserCustom(
2732
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2733
+ runtime,
2734
+ "L2_rules",
2735
+ "pattern"
2736
+ );
2518
2737
  return rules.map((r) => r.pattern || "");
2519
2738
  }
2520
2739
  function getEnabledL2UserDeletionPatterns(runtime) {
2521
2740
  const cat = runtime.L2_rules?.user_deletion_consent || { category_enabled: true, rules: [] };
2522
- const rules = _mergeUserCustom(filterEnabledRules(cat.rules || [], cat.category_enabled), runtime, "L2_rules", "pattern");
2741
+ const rules = _mergeUserCustom(
2742
+ _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2743
+ runtime,
2744
+ "L2_rules",
2745
+ "pattern"
2746
+ );
2523
2747
  return rules.map((r) => r.pattern || "");
2524
2748
  }
2525
2749
  function isL2CategoryEnabled(runtime, categoryKey) {
@@ -2551,28 +2775,48 @@ function getEnabledL1CommandPatternsOrDefault(runtime, osType) {
2551
2775
  if (_hasRulesForOs(cat?.rules, osType)) {
2552
2776
  return getEnabledL1CommandPatterns(runtime, osType);
2553
2777
  }
2554
- return _mergeUserCustom(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), runtime, "L1_rules", "pattern");
2778
+ return _mergeUserCustom(
2779
+ _mergeExtraDirRules(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2780
+ runtime,
2781
+ "L1_rules",
2782
+ "pattern"
2783
+ );
2555
2784
  }
2556
2785
  function getEnabledL1InjectionKeywordsOrDefault(runtime, osType) {
2557
2786
  const cat = runtime.L1_rules?.injection_detection;
2558
2787
  if (_hasRulesForOs(cat?.rules, osType)) {
2559
2788
  return getEnabledL1InjectionKeywords(runtime, osType);
2560
2789
  }
2561
- return _mergeUserCustom(_l1InjectionKeywordsToRules(), runtime, "L1_rules", "keyword");
2790
+ return _mergeUserCustom(
2791
+ _mergeExtraDirRules(_l1InjectionKeywordsToRules(), "injection_detection"),
2792
+ runtime,
2793
+ "L1_rules",
2794
+ "keyword"
2795
+ );
2562
2796
  }
2563
2797
  function getEnabledL2SensitivePathsOrDefault(runtime, osType) {
2564
2798
  const spCat = _sensitivePathCategoryOf(runtime);
2565
2799
  if (_hasRulesForOs(spCat?.rules, osType)) {
2566
2800
  return getEnabledL2SensitivePaths(runtime, osType);
2567
2801
  }
2568
- return _mergeUserCustom(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), runtime, ["L1_rules", "L2_rules"], "path");
2802
+ return _mergeUserCustom(
2803
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2804
+ runtime,
2805
+ ["L1_rules", "L2_rules"],
2806
+ "path"
2807
+ );
2569
2808
  }
2570
2809
  function getEnabledL2IntentPatternsOrDefault(runtime, osType) {
2571
2810
  const cat = runtime.L2_rules?.intent_consistency;
2572
2811
  if (_hasRulesForOs(cat?.rules, osType)) {
2573
2812
  return getEnabledL2IntentPatterns(runtime, osType);
2574
2813
  }
2575
- return _mergeUserCustom(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), runtime, "L2_rules", "intent_keywords");
2814
+ return _mergeUserCustom(
2815
+ _mergeExtraDirRules(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2816
+ runtime,
2817
+ "L2_rules",
2818
+ "intent_keywords"
2819
+ );
2576
2820
  }
2577
2821
  function getEnabledL2PasswordPatternsOrDefault(runtime, osType) {
2578
2822
  const cat = runtime.L2_rules?.password_modify_consent;
@@ -2968,6 +3212,7 @@ var init_runtime_config = __esm({
2968
3212
  init_heuristic();
2969
3213
  init_logic_rules();
2970
3214
  init_skill_loader();
3215
+ init_rules_loader();
2971
3216
  init_runtime_config_bridge();
2972
3217
  __dirname4 = dirname3(fileURLToPath4(import.meta.url));
2973
3218
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
@@ -3028,7 +3273,9 @@ __export(config_exports, {
3028
3273
  isLLMAvailable: () => isLLMAvailable,
3029
3274
  isLogicRulesEnabled: () => isLogicRulesEnabled,
3030
3275
  probeLLMConnection: () => probeLLMConnection,
3031
- updateLLMConfig: () => updateLLMConfig
3276
+ updateLLMConfig: () => updateLLMConfig,
3277
+ userRuleVariantPattern: () => userRuleVariantPattern,
3278
+ wildcardToRegExp: () => wildcardToRegExp
3032
3279
  });
3033
3280
  import fs2 from "fs";
3034
3281
  import path2 from "path";
@@ -3125,6 +3372,8 @@ function loadCustomRules() {
3125
3372
  const p = r?.pattern;
3126
3373
  if (!p) continue;
3127
3374
  rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
3375
+ const variant = userRuleVariantPattern(p);
3376
+ if (variant) rules.push({ pattern: wildcardToRegExp(variant), action: "Deny", severity: "high" });
3128
3377
  }
3129
3378
  break;
3130
3379
  } catch {
@@ -3132,6 +3381,15 @@ function loadCustomRules() {
3132
3381
  }
3133
3382
  return rules;
3134
3383
  }
3384
+ function userRuleVariantPattern(pattern) {
3385
+ const tokens = pattern.split(/\s+/);
3386
+ if (tokens.length >= 3 && tokens[1] === "*" && tokens[2] !== "*") {
3387
+ let variant = [tokens[0], ...tokens.slice(2)].join(" ");
3388
+ if (!variant.endsWith("*")) variant += " *";
3389
+ return variant;
3390
+ }
3391
+ return null;
3392
+ }
3135
3393
  function getLLMConfig() {
3136
3394
  return llmConfig;
3137
3395
  }
@@ -3368,15 +3626,27 @@ function getScenarioExtensions(scenarioId) {
3368
3626
  if (!scenarioId) return null;
3369
3627
  if (cache2.has(scenarioId)) return cache2.get(scenarioId) ?? null;
3370
3628
  let ext = null;
3629
+ let overrideHit = null;
3371
3630
  try {
3372
- if (hasRuntimeConfig()) {
3373
- ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3374
- }
3631
+ overrideHit = resolveScenarioRulesFile(scenarioId);
3375
3632
  } catch {
3376
3633
  }
3634
+ if (!overrideHit) {
3635
+ try {
3636
+ if (hasRuntimeConfig()) {
3637
+ ext = getEnabledScenarioExtensions(loadRuntimeConfig(), scenarioId);
3638
+ }
3639
+ } catch {
3640
+ }
3641
+ }
3377
3642
  if (ext === null) {
3378
- const scenariosDir = resolveScenariosDir();
3379
- const rulesFile = resolve5(scenariosDir, scenarioId, "rules.json");
3643
+ const builtinFile = resolve5(resolveScenariosDir(), scenarioId, "rules.json");
3644
+ const rulesFile = overrideHit ?? builtinFile;
3645
+ if (overrideHit) {
3646
+ console.warn(
3647
+ `[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`
3648
+ );
3649
+ }
3380
3650
  if (!existsSync7(rulesFile)) {
3381
3651
  cache2.set(scenarioId, null);
3382
3652
  return null;
@@ -3428,6 +3698,7 @@ var init_scenarios = __esm({
3428
3698
  init_esm_shims();
3429
3699
  init_agents();
3430
3700
  init_runtime_config();
3701
+ init_rules_loader();
3431
3702
  __filename5 = fileURLToPath7(import.meta.url);
3432
3703
  __dirname6 = dirname5(__filename5);
3433
3704
  SCENARIOS_DIR_PROD = resolve5(__dirname6, "scenarios");
@@ -3493,16 +3764,41 @@ function scanDangerousCommandPatterns(actionType, actionDetail, osType) {
3493
3764
  violatedLayers: ["L1.1"]
3494
3765
  };
3495
3766
  }
3767
+ function allUrlsLoopback(actionDetail) {
3768
+ const hosts = actionDetail.match(new RegExp(LOOPBACK_URL_RE.source, "gi"));
3769
+ if (!hosts || hosts.length === 0) return false;
3770
+ return hosts.every((u) => {
3771
+ const raw = u.replace(/^https?:\/\//i, "");
3772
+ const host = raw.split("@").pop().split(":")[0].replace(/^\[|\]$/g, "").toLowerCase();
3773
+ return LOOPBACK_HOSTS.has(host) || host.startsWith("127.");
3774
+ });
3775
+ }
3776
+ function stripEchoParamsForUserRules(actionDetail) {
3777
+ return splitCommandSegments2(actionDetail).map((seg) => {
3778
+ const m = seg.match(/^(echo|printf)(?![a-zA-Z0-9_])\s*/i);
3779
+ if (!m) return seg;
3780
+ const head = m[1].toLowerCase();
3781
+ const rest = seg.slice(m[0].length);
3782
+ const cut = rest.search(/>+|\|/);
3783
+ return `${head} ` + (cut >= 0 ? rest.slice(cut) : "");
3784
+ }).join(" ");
3785
+ }
3496
3786
  function runHeuristicCheck(input) {
3497
3787
  const command = `${input.aNext.actionType}: ${input.aNext.actionDetail}`;
3498
3788
  const matched = [];
3499
3789
  let highestSeverity = "low";
3500
3790
  let highestRiskType = "";
3501
3791
  const userRules = getCustomRules();
3792
+ const atLower = input.aNext.actionType.toLowerCase();
3793
+ const isBashLike = ["bash", "executebash", "terminal-executebash"].includes(atLower);
3794
+ const urlsAllLoopback = allUrlsLoopback(input.aNext.actionDetail);
3795
+ const userRuleScanDetail = isBashLike ? stripEchoParamsForUserRules(input.aNext.actionDetail) : input.aNext.actionDetail;
3796
+ const scanCommand = `${input.aNext.actionType}: ${userRuleScanDetail}`;
3502
3797
  for (const rule of userRules) {
3503
3798
  try {
3504
3799
  const re = new RegExp(rule.pattern);
3505
- if (re.test(command)) {
3800
+ if (re.test(command) || re.test(scanCommand)) {
3801
+ if (urlsAllLoopback) continue;
3506
3802
  if (rule.action === "Deny") {
3507
3803
  return {
3508
3804
  allowed: false,
@@ -3685,6 +3981,23 @@ function buildPathContext(actionDetail) {
3685
3981
  if (!lines.length) return "";
3686
3982
  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";
3687
3983
  }
3984
+ function isPurePrintCode(code) {
3985
+ if (code.includes("\\")) return false;
3986
+ if (PY_EXEC_IO_RE.test(code) || PY_BIGNUM_RE.test(code)) return false;
3987
+ return PY_PRINT_RE.test(code) || !/\w+\s*\(/.test(code);
3988
+ }
3989
+ function stripInlinePythonLiterals(text) {
3990
+ const subst = (_m, g1, g2, g3) => isPurePrintCode(g2 ?? g3 ?? "") ? "$(<<pylit>>)" : _m;
3991
+ const plain = (_m, g1, g2, g3) => {
3992
+ if (!isPurePrintCode(g2 ?? g3 ?? "")) return _m;
3993
+ const q = _m.trimEnd().endsWith('"') ? '"' : "'";
3994
+ return `${g1} -c ${q}<<pylit>>${q}`;
3995
+ };
3996
+ let out = text;
3997
+ out = out.replace(new RegExp(PY_CMD_SUBST_C_RE.source, PY_CMD_SUBST_C_RE.flags), subst);
3998
+ out = out.replace(new RegExp(PY_INLINE_C_RE.source, PY_INLINE_C_RE.flags), plain);
3999
+ return out;
4000
+ }
3688
4001
  function stripLiteralTextSegments(actionDetail) {
3689
4002
  const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
3690
4003
  const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node|crontab|at)\b/.test(actionDetail);
@@ -3695,13 +4008,14 @@ function stripLiteralTextSegments(actionDetail) {
3695
4008
  for (const seg of splitCommandSegments2(actionDetail)) {
3696
4009
  const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
3697
4010
  if (fw === "echo" || fw === "printf") {
3698
- const segUnquotedRedirect = seg.replace(
4011
+ const segPy = stripInlinePythonLiterals(seg);
4012
+ const segUnquotedRedirect = segPy.replace(
3699
4013
  />{1,2}\s*(['"])([~./][^'"]+)\1/g,
3700
4014
  (_m, _q, p) => ">" + p
3701
4015
  );
3702
4016
  resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
3703
4017
  } else {
3704
- resultParts.push(seg);
4018
+ resultParts.push(stripInlinePythonLiterals(seg));
3705
4019
  }
3706
4020
  }
3707
4021
  return resultParts.join("");
@@ -4225,7 +4539,7 @@ function checkLateralMovement(actionDetail) {
4225
4539
  }
4226
4540
  return null;
4227
4541
  }
4228
- 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;
4542
+ 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;
4229
4543
  var init_heuristic = __esm({
4230
4544
  "src/engine/heuristic.ts"() {
4231
4545
  "use strict";
@@ -4249,8 +4563,15 @@ var init_heuristic = __esm({
4249
4563
  DANGEROUS_PATTERNS_WINDOWS = _loadDangerousPatterns("windows");
4250
4564
  DANGEROUS_PATTERNS_MACOS = _loadDangerousPatterns("macos");
4251
4565
  DANGEROUS_PATTERNS_HARMONY = _loadDangerousPatterns("harmony");
4566
+ LOOPBACK_URL_RE = /https?:\/\/([^\/\s;'"|&)]+)/i;
4567
+ LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "::1", "0.0.0.0", "[::1]"]);
4252
4568
  PATH_CONTEXT_TRIGGER_RE = /(?:^|[;&|]\s*)(?:rm|rmdir|unlink|ln|mv|cp|dd|truncate|shred|chmod|chown)\b/i;
4253
4569
  HOME_PREFIX_RE = /(?:~|~(?=\/)|\$HOME\b|\$\{HOME\}|\/home\/[a-z0-9_.-]+)/gi;
4570
+ 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;
4571
+ PY_PRINT_RE = /\b(?:print|sys\.stdout\.write)\s*\(/;
4572
+ PY_BIGNUM_RE = /\d{6,}/;
4573
+ PY_INLINE_C_RE = /(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"|'([^']*)')/i;
4574
+ PY_CMD_SUBST_C_RE = /\$\(\s*(python(?:3(?:\.\d+)?)?|perl5?|ruby|node|php|lua)\s+(?:-[a-zA-Z]+\s+)*-c\s+(?:"([^"]*)"'|'([^']*)')\s*\)/i;
4254
4575
  SENSITIVE_EXCLUDED_PREFIXES = ["memory-", "skill-", "skill-manager-", "mailbox-", "app-"];
4255
4576
  SEARCH_ACTION_PREFIXES = ["search-", "web-", "browser-", "query-", "websearch", "web_search"];
4256
4577
  HOME_TARGET_RE = /(?:^|[\s="'($])(?:~|(?:\$home|\$\{home\}))(?:\/(?:\.\*|\*|\.)?)?(?=[\s"';}|&)]|$)/i;
@@ -6026,7 +6347,7 @@ var init_tracker = __esm({
6026
6347
  });
6027
6348
 
6028
6349
  // src/engine/script-content-analyzer.ts
6029
- import { statSync as statSync3, existsSync as existsSync10 } from "fs";
6350
+ import { statSync as statSync4, existsSync as existsSync10 } from "fs";
6030
6351
  import { resolve as resolve9, join as join6, isAbsolute as isAbsolute2 } from "path";
6031
6352
  import { homedir as homedir3 } from "os";
6032
6353
  function emptyChain() {
@@ -6140,7 +6461,7 @@ function extractInlineScriptPath(actionDetail) {
6140
6461
  try {
6141
6462
  const resolved = resolvePath2(tok.replace(/^['"]|['"]$/g, ""));
6142
6463
  if (existsSync10(resolved)) {
6143
- const stat = statSync3(resolved);
6464
+ const stat = statSync4(resolved);
6144
6465
  if (stat.isFile()) return resolved;
6145
6466
  }
6146
6467
  } catch {
@@ -6154,7 +6475,7 @@ function extractInlineScriptPath(actionDetail) {
6154
6475
  try {
6155
6476
  const resolved = resolvePath2(p);
6156
6477
  if (existsSync10(resolved)) {
6157
- const stat = statSync3(resolved);
6478
+ const stat = statSync4(resolved);
6158
6479
  if (stat.isFile()) return resolved;
6159
6480
  }
6160
6481
  } catch {
@@ -6170,7 +6491,7 @@ function extractInlineScriptPath(actionDetail) {
6170
6491
  try {
6171
6492
  resolved = resolvePath2(p);
6172
6493
  if (existsSync10(resolved)) {
6173
- const stat = statSync3(resolved);
6494
+ const stat = statSync4(resolved);
6174
6495
  if (stat.isFile()) return resolved;
6175
6496
  }
6176
6497
  } catch {
@@ -10345,10 +10666,22 @@ function logDeploymentSelfCheck() {
10345
10666
  try {
10346
10667
  const overrides = overriddenRuleFiles();
10347
10668
  if (overrides.length > 0) {
10348
- 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`);
10669
+ 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`);
10349
10670
  for (const p of overrides) console.error(` ${p}`);
10350
10671
  console.error(
10351
- " \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"
10672
+ " \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"
10673
+ );
10674
+ }
10675
+ } catch {
10676
+ }
10677
+ try {
10678
+ const extras = extraRuleFiles();
10679
+ if (extras.length > 0) {
10680
+ const cats = loadExtraRules();
10681
+ const total = Object.values(cats).reduce((n, v) => n + v.length, 0);
10682
+ const catSummary = Object.entries(cats).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}\xD7${v.length}`).join(", ");
10683
+ console.log(
10684
+ `\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`
10352
10685
  );
10353
10686
  }
10354
10687
  } catch {