@kenhkl/agent-moss 0.16.9 → 0.16.13

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.
@@ -394,6 +394,52 @@ var init_runtime_config_bridge = __esm({
394
394
  }
395
395
  });
396
396
 
397
+ // src/infra/rules-mode.ts
398
+ function truthy(name) {
399
+ return TRUTHY.has((process.env[name] || "").trim().toLowerCase());
400
+ }
401
+ function getRulesMode() {
402
+ if (cached !== null) return cached;
403
+ const raw = (process.env[ENV_RULES_MODE] || "").trim().toLowerCase();
404
+ if (raw) {
405
+ if (VALID_MODES.has(raw)) {
406
+ cached = raw;
407
+ } else {
408
+ console.error(
409
+ `[AgentMoss] ${ENV_RULES_MODE}=${JSON.stringify(raw)} \u975E\u6CD5\uFF08\u53EA\u652F\u6301 hot / cold / builtin\uFF09\uFF1B\u4E3A\u5B89\u5168\u8D77\u89C1\u6309 builtin\uFF08\u4EC5\u5185\u7F6E\u89C4\u5219\uFF09\u5904\u7406`
410
+ );
411
+ cached = "builtin";
412
+ }
413
+ return cached;
414
+ }
415
+ if (truthy(ENV_RULES_BUILTIN_ONLY)) cached = "builtin";
416
+ else if (truthy(ENV_DISABLE_RULE_HOT_RELOAD)) cached = "cold";
417
+ else cached = "hot";
418
+ return cached;
419
+ }
420
+ function isHotReloadEnabled() {
421
+ return getRulesMode() === "hot";
422
+ }
423
+ function isRuntimeConfigDisabled() {
424
+ return getRulesMode() !== "hot";
425
+ }
426
+ function isBuiltinOnly() {
427
+ return getRulesMode() === "builtin";
428
+ }
429
+ var ENV_RULES_MODE, ENV_RULES_BUILTIN_ONLY, ENV_DISABLE_RULE_HOT_RELOAD, TRUTHY, VALID_MODES, cached;
430
+ var init_rules_mode = __esm({
431
+ "src/infra/rules-mode.ts"() {
432
+ "use strict";
433
+ init_esm_shims();
434
+ ENV_RULES_MODE = "AGENT_MOSS_RULES_MODE";
435
+ ENV_RULES_BUILTIN_ONLY = "AGENT_MOSS_RULES_BUILTIN_ONLY";
436
+ ENV_DISABLE_RULE_HOT_RELOAD = "AGENT_MOSS_DISABLE_RULE_HOT_RELOAD";
437
+ TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
438
+ VALID_MODES = /* @__PURE__ */ new Set(["hot", "cold", "builtin"]);
439
+ cached = null;
440
+ }
441
+ });
442
+
397
443
  // src/infra/rules-loader.ts
398
444
  import { readFileSync as readFileSync2, existsSync, readdirSync, statSync } from "fs";
399
445
  import { resolve, dirname, join as join2 } from "path";
@@ -406,58 +452,64 @@ function parseDirEnv(envName) {
406
452
  if (!raw) return [];
407
453
  return raw.split(/[,;]/).map((s) => s.trim()).filter(Boolean).map((s) => resolve(s));
408
454
  }
409
- function overrideRulesDirs() {
410
- return [...parseDirEnv("AGENT_MOSS_OVERRIDE_RULES_DIR"), resolve(dataRootDir(), "rules")];
411
- }
412
- function warnOverride(name, path5, data) {
413
- if (warnedOverride.has(path5)) return;
414
- warnedOverride.add(path5);
415
- console.warn(
416
- `[AgentMoss] \u89C4\u5219\u6587\u4EF6\u88AB\u8986\u76D6\u76EE\u5F55\u8986\u76D6: ${path5}\uFF08\u5185\u7F6E\u9ED8\u8BA4\u88AB\u5FFD\u7565\uFF09\u3002\u8BE5\u6587\u4EF6\u7531\u90E8\u7F72\u4FA7\u81EA\u884C\u7EF4\u62A4 \u2014\u2014 **\u5185\u7F6E\u89C4\u5219\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4E0A\u9762**\uFF1B\u5220\u9664\u8BE5\u6587\u4EF6\uFF08\u6216\u6E05\u7A7A AGENT_MOSS_OVERRIDE_RULES_DIR\uFF09\u5373\u6062\u590D\u5185\u7F6E\u89C4\u5219\u3002\u82E5\u53EA\u60F3\u9010\u6761\u542F\u505C/\u589E\u5220\uFF0C\u8BF7\u6539\u7528 runtime config\uFF08Console\uFF09\u3002`
417
- );
418
- const missing = (EXPECTED_KEYS[name] || []).filter((k) => !(k in data));
419
- if (missing.length > 0) {
420
- console.warn(
421
- `\u26A0\uFE0F [AgentMoss] \u8986\u76D6\u6587\u4EF6 ${path5} \u7F3A\u5C11\u9876\u5C42\u952E [${missing.join(", ")}] \u2014\u2014 \u5BF9\u5E94\u7C7B\u522B\u4F1A**\u4E00\u6761\u89C4\u5219\u90FD\u4E0D\u8DD1**\uFF08\u9759\u9ED8\u5931\u6548\uFF09\u3002\u82E5\u53EA\u60F3\u6539\u5176\u4E2D\u4E00\u7C7B\uFF0C\u8BF7\u5148\u62F7\u8D1D\u5B8C\u6574\u7684\u5185\u7F6E\u6587\u4EF6\u518D\u6539\u3002`
422
- );
423
- }
455
+ function rulesDirs() {
456
+ if (isBuiltinOnly()) return [];
457
+ const dirs = [];
458
+ for (const env of RULES_DIR_ENVS) dirs.push(...parseDirEnv(env));
459
+ dirs.push(resolve(dataRootDir(), "rules"));
460
+ return [...new Set(dirs)];
424
461
  }
425
462
  function findRulesFile(name) {
426
- for (const dir of overrideRulesDirs()) {
427
- const p = resolve(dir, name);
428
- if (existsSync(p)) return p;
429
- }
430
463
  for (const dir of RULES_DIR_CANDS) {
431
464
  const p = resolve(dir, name);
432
465
  if (existsSync(p)) return p;
433
466
  }
434
467
  return null;
435
468
  }
436
- function isOverridePath(name, p) {
437
- return overrideRulesDirs().some((dir) => resolve(dir, name) === p);
438
- }
439
469
  function loadJson(name) {
470
+ if (!isHotReloadEnabled()) {
471
+ const cached2 = frozenJsonCache.get(name);
472
+ if (cached2 !== void 0) return cached2;
473
+ }
440
474
  const p = findRulesFile(name);
441
475
  if (!p) return {};
442
- const isOverride = isOverridePath(name, p);
443
- let data;
444
476
  try {
445
- data = JSON.parse(readFileSync2(p, "utf-8")) || {};
477
+ const parsed = JSON.parse(readFileSync2(p, "utf-8")) || {};
478
+ if (!isHotReloadEnabled()) frozenJsonCache.set(name, parsed);
479
+ return parsed;
446
480
  } catch {
447
- if (isOverride) warnOverride(name, p, {});
448
481
  return {};
449
482
  }
450
- if (isOverride) warnOverride(name, p, data);
451
- return data;
452
483
  }
453
484
  function resolveScenarioRulesFile(scenarioId) {
454
485
  const rel = join2("scenarios", scenarioId, "rules.json");
455
- for (const dir of overrideRulesDirs()) {
486
+ for (const dir of rulesDirs()) {
456
487
  const p = resolve(dir, rel);
457
- if (existsSync(p)) return p;
488
+ if (!existsSync(p)) continue;
489
+ let reason = "";
490
+ try {
491
+ reason = invalidScenarioRulesReason(JSON.parse(readFileSync2(p, "utf-8")) || {});
492
+ } catch (err) {
493
+ reason = `\u8BFB\u53D6/\u89E3\u6790\u5931\u8D25: ${String(err)}`;
494
+ }
495
+ if (reason) {
496
+ console.error(
497
+ `[AgentMoss] \u274C \u573A\u666F\u89C4\u5219\u8986\u76D6\u6587\u4EF6\u7ED3\u6784\u4E0D\u53EF\u7528\uFF0C**\u5DF2\u5FFD\u7565\u5E76\u56DE\u9000\u5185\u7F6E**: ${p}
498
+ \u539F\u56E0: ${reason}`
499
+ );
500
+ return null;
501
+ }
502
+ console.warn(`[AgentMoss] \u26A0\uFE0F \u573A\u666F ${scenarioId} \u89C4\u5219\u88AB\u8986\u76D6: ${p}\uFF08\u5185\u7F6E\u66F4\u65B0\u4E0D\u4F1A\u53CD\u6620\u5230\u5B83\u4E0A\u9762\uFF09`);
503
+ return p;
458
504
  }
459
505
  return null;
460
506
  }
507
+ function invalidScenarioRulesReason(data) {
508
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
509
+ return `\u9876\u5C42\u5FC5\u987B\u662F JSON \u5BF9\u8C61\uFF0C\u5B9E\u9645\u662F ${Array.isArray(data) ? "array" : typeof data}`;
510
+ }
511
+ return SCENARIO_LIST_KEYS.filter((k) => k in data && !Array.isArray(data[k])).map((k) => `"${k}" \u5E94\u4E3A\u6570\u7EC4\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF09\uFF0C\u5B9E\u9645\u662F ${typeof data[k]}`).join("\uFF1B");
512
+ }
461
513
  function loadL1() {
462
514
  return loadJson("l1_rules.json");
463
515
  }
@@ -465,10 +517,40 @@ function loadL2() {
465
517
  return loadJson("l2_rules.json");
466
518
  }
467
519
  function loadL3() {
468
- return loadJson("l3_rules.json");
520
+ const data = { ...loadJson("l3_rules.json") };
521
+ try {
522
+ const ov = loadOverrideRules().skills;
523
+ const ex = loadExtraRules().skills || {};
524
+ if (ov && typeof ov === "object") {
525
+ for (const [cat, skills] of Object.entries(ov)) data[cat] = { ...skills };
526
+ }
527
+ if (ex && typeof ex === "object") {
528
+ for (const [cat, skills] of Object.entries(ex)) data[cat] = { ...data[cat] || {}, ...skills };
529
+ }
530
+ } catch {
531
+ }
532
+ return data;
533
+ }
534
+ function loadMCRaw() {
535
+ const mc = { ...loadJson("mc_rules.json") };
536
+ let rules = [...mc.rules || []];
537
+ try {
538
+ const ov = loadOverrideRules().ltl_rules;
539
+ const ex = loadExtraRules().ltl_rules || [];
540
+ if (Array.isArray(ov)) rules = [...ov];
541
+ if (Array.isArray(ex) && ex.length > 0) {
542
+ const byId = /* @__PURE__ */ new Map();
543
+ for (const r of rules) if (r && r.id) byId.set(r.id, r);
544
+ for (const r of ex) if (r && r.id) byId.set(r.id, r);
545
+ rules = [...byId.values()];
546
+ }
547
+ } catch {
548
+ }
549
+ mc.rules = rules;
550
+ return mc;
469
551
  }
470
552
  function loadMC() {
471
- const mc = loadJson("mc_rules.json");
553
+ const mc = loadMCRaw();
472
554
  const rules = mc.rules || [];
473
555
  try {
474
556
  const rt = runtimeConfigBridge.current;
@@ -488,162 +570,297 @@ function loadMC() {
488
570
  return mc;
489
571
  }
490
572
  function loadToolRegistry() {
491
- return loadJson("tool_registry.json");
492
- }
493
- function extraRulesDirs() {
494
- return parseDirEnv("AGENT_MOSS_EXTRA_RULES_DIR");
495
- }
496
- function listJsonFilesDeep(dir) {
497
- const out = [];
498
- let entries;
573
+ const data = { ...loadJson("tool_registry.json") };
574
+ let tools = { ...data.tools || {} };
499
575
  try {
500
- entries = readdirSync(dir, { withFileTypes: true }).map((d) => d.name);
576
+ const ov = loadOverrideRules().tools;
577
+ const ex = loadExtraRules().tools || {};
578
+ if (ov && typeof ov === "object") tools = { ...ov };
579
+ if (ex && typeof ex === "object") tools = { ...tools, ...ex };
501
580
  } catch {
502
- return out;
503
581
  }
504
- for (const name of entries.sort()) {
505
- const p = resolve(dir, name);
506
- let isDir = false;
507
- try {
508
- isDir = statSync(p).isDirectory();
509
- } catch {
510
- continue;
582
+ data.tools = tools;
583
+ return data;
584
+ }
585
+ function scanStructEntry(cat, value, path5) {
586
+ if (cat === "skills") {
587
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
588
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 skills \u5E94\u4E3A\u5BF9\u8C61\uFF08{\u7C7B\u522B: {\u6280\u80FD\u540D: [\u5173\u952E\u8BCD]}}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
589
+ return null;
590
+ }
591
+ const out = {};
592
+ for (const [skCat, skills] of Object.entries(value)) {
593
+ if (!skills || typeof skills !== "object" || Array.isArray(skills)) {
594
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 skills.${skCat} \u5E94\u4E3A\u5BF9\u8C61\uFF08\u6280\u80FD\u540D \u2192 \u5173\u952E\u8BCD\u6570\u7EC4\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u7C7B\u522B\u3002`);
595
+ continue;
596
+ }
597
+ const keep = {};
598
+ for (const [name, kws] of Object.entries(skills)) {
599
+ if (Array.isArray(kws)) keep[name] = kws;
600
+ else console.warn(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 skills.${skCat}.${name} \u5173\u952E\u8BCD\u4E0D\u662F\u6570\u7EC4 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`);
601
+ }
602
+ if (Object.keys(keep).length > 0) out[skCat] = keep;
511
603
  }
512
- if (isDir) out.push(...listJsonFilesDeep(p));
513
- else if (name.toLowerCase().endsWith(".json")) out.push(p);
604
+ return Object.keys(out).length > 0 ? out : null;
514
605
  }
515
- return out;
606
+ if (cat === "ltl_rules") {
607
+ if (!Array.isArray(value)) {
608
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 ltl_rules \u5E94\u4E3A\u5BF9\u8C61\u6570\u7EC4 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
609
+ return null;
610
+ }
611
+ const keep = value.filter((r) => r && typeof r === "object" && r.id);
612
+ if (keep.length !== value.length) {
613
+ console.warn(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 ltl_rules \u6709\u6761\u76EE\u7F3A id \u6216\u4E0D\u662F\u5BF9\u8C61 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`);
614
+ }
615
+ return keep.length > 0 ? keep : null;
616
+ }
617
+ if (cat === "tools") {
618
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
619
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 tools \u5E94\u4E3A\u5BF9\u8C61\uFF08{\u5DE5\u5177\u540D: {actionClass, ...}}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
620
+ return null;
621
+ }
622
+ const keep = {};
623
+ for (const [k, v] of Object.entries(value)) {
624
+ if (v && typeof v === "object" && !Array.isArray(v)) keep[k] = v;
625
+ else console.warn(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 tools.${k} \u4E0D\u662F\u5BF9\u8C61 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`);
626
+ }
627
+ return Object.keys(keep).length > 0 ? keep : null;
628
+ }
629
+ return null;
516
630
  }
517
- function scanExtraFiles(files) {
518
- const data = {};
519
- const loaded = [];
520
- const seenIds = /* @__PURE__ */ new Map();
631
+ function scanRulesFile(path5, kind) {
632
+ const out = {};
633
+ const label = kind === "override" ? "\u8986\u76D6" : "\u8FFD\u52A0";
634
+ let parsed;
635
+ try {
636
+ parsed = JSON.parse(readFileSync2(path5, "utf-8"));
637
+ } catch (e) {
638
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u635F\u574F/\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u8DF3\u8FC7\uFF08${kind} \u89C4\u5219\u4E0D\u751F\u6548\uFF09: ${e.message}`);
639
+ return out;
640
+ }
521
641
  const classifyFlat = (r) => {
522
642
  for (const [keys, cat] of EXTRA_FLAT_FIELD) {
523
643
  if (r && typeof r === "object" && keys.every((k) => r[k])) return cat;
524
644
  }
525
645
  return null;
526
646
  };
527
- for (const p of files) {
528
- let parsed;
529
- try {
530
- parsed = JSON.parse(readFileSync2(p, "utf-8"));
531
- } catch (e) {
532
- console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6\u635F\u574F/\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u8DF3\u8FC7: ${p}\uFF08${e.message}\uFF09`);
647
+ let pairs = [];
648
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
649
+ pairs = Object.entries(parsed);
650
+ } else if (Array.isArray(parsed)) {
651
+ let flatBad = 0;
652
+ for (const r of parsed) {
653
+ const cat = classifyFlat(r);
654
+ if (cat === null) {
655
+ flatBad += 1;
656
+ continue;
657
+ }
658
+ pairs.push([cat, [r]]);
659
+ }
660
+ if (flatBad > 0) {
661
+ console.warn(
662
+ `[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \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`
663
+ );
664
+ }
665
+ } else {
666
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u9876\u5C42\u5E94\u662F\u5BF9\u8C61\uFF08\u7C7B\u522B\u952E\u2192\u6761\u76EE\u6570\u7EC4\uFF09\u6216\u6570\u7EC4\uFF0C\u5DF2\u8DF3\u8FC7\u3002`);
667
+ return out;
668
+ }
669
+ for (const [cat, rules] of pairs) {
670
+ if (String(cat).startsWith("_")) continue;
671
+ if (STRUCT_KEYS.includes(cat)) {
672
+ const got = scanStructEntry(cat, rules, path5);
673
+ if (got !== null) out[cat] = got;
533
674
  continue;
534
675
  }
535
- let pairs = [];
536
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
537
- pairs = Object.entries(parsed);
538
- } else if (Array.isArray(parsed)) {
539
- let flatBad = 0;
540
- for (const r of parsed) {
541
- const cat = classifyFlat(r);
542
- if (cat === null) {
543
- flatBad += 1;
544
- continue;
545
- }
546
- pairs.push([cat, [r]]);
547
- }
548
- if (flatBad > 0) {
549
- console.warn(
550
- `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u6709 ${flatBad} \u6761\u5E73\u94FA\u6761\u76EE\u65E0\u6CD5\u5F52\u7C7B\uFF08\u88F8 pattern \u4E0D\u5E26 risk_level/risk_type \u65F6\u533A\u5206\u4E0D\u4E86 L1 \u5371\u9669\u547D\u4EE4\u4E0E L2 \u6388\u6743\u7C7B\uFF09\u2014\u2014 \u8BF7\u6539\u7528\u5BF9\u8C61\u5F62\u5F0F\uFF1A{"\u7C7B\u522B\u952E": [\u6761\u76EE...]}\u3002`
551
- );
552
- }
553
- } else {
676
+ const required = EXTRA_CATEGORIES[cat];
677
+ if (!required) {
554
678
  console.warn(
555
- `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u9876\u5C42\u5E94\u662F\u5BF9\u8C61\uFF08\u7C7B\u522B\u952E\u2192\u6761\u76EE\u6570\u7EC4\uFF09\u6216\u6570\u7EC4\uFF0C\u5DF2\u8DF3\u8FC7\u3002`
679
+ `[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \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`
680
+ );
681
+ continue;
682
+ }
683
+ if (rules && typeof rules === "object" && !Array.isArray(rules)) {
684
+ console.error(
685
+ `[AgentMoss] \u274C \u89C4\u5219\u6587\u4EF6 ${path5} \u7684\u7C7B\u522B "${cat}" \u662F**\u5BF9\u8C61**\uFF0C\u5E94\u4E3A**\u5BF9\u8C61\u6570\u7EC4** \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u7C7B\u522B\u3002
686
+ \u6B63\u786E\u5199\u6CD5: {"${cat}": [{"pattern": "...", "risk_level": "high", ...}]}
687
+ \u4F60\u5199\u7684\u50CF\u662F runtime config \u7684\u683C\u5F0F\uFF08{"category_enabled": ..., "rules": [...]}\uFF09\u2014\u2014\u90A3\u662F ~/.config/agentmoss/agent_moss_runtime.json \u7684\u683C\u5F0F\uFF0C\u4E0D\u80FD\u76F4\u63A5\u5F53\u89C4\u5219\u6587\u4EF6\u7528\u3002`
556
688
  );
557
689
  continue;
558
690
  }
559
- let nLoaded = 0;
560
- for (const [cat, rules] of pairs) {
561
- const required = EXTRA_CATEGORIES[cat];
562
- if (!required) {
691
+ if (!Array.isArray(rules)) {
692
+ console.warn(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684\u7C7B\u522B "${cat}" \u5E94\u4E3A\u6570\u7EC4\uFF0C\u5B9E\u9645\u662F ${typeof rules} \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u7C7B\u522B\u3002`);
693
+ continue;
694
+ }
695
+ const items = [];
696
+ for (const r of rules) {
697
+ if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
563
698
  console.warn(
564
- `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u542B\u672A\u77E5\u7C7B\u522B\u952E "${cat}"\uFF08\u53EF\u7528\uFF1A${Object.keys(EXTRA_CATEGORIES).join(", ")}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u7C7B\u522B\u3002`
699
+ `[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7C7B\u522B ${cat} \u6709\u6761\u76EE\u7F3A\u5FC5\u9700\u5B57\u6BB5 [${required.join(", ")}] \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`
565
700
  );
566
701
  continue;
567
702
  }
568
- if (!Array.isArray(rules)) {
569
- console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u7C7B\u522B ${cat} \u7684\u503C\u5E94\u662F\u6570\u7EC4 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
703
+ items.push(r);
704
+ }
705
+ if (items.length > 0) out[cat] = (out[cat] || []).concat(items);
706
+ }
707
+ if (Object.keys(out).length > 0) {
708
+ console.info(
709
+ `[AgentMoss] \u5DF2\u52A0\u8F7D${label}\u89C4\u5219\u6587\u4EF6 ${path5}\uFF1A` + Object.entries(out).map(([k, v]) => `${k}\xD7${v.length}`).join(", ")
710
+ );
711
+ }
712
+ return out;
713
+ }
714
+ function scanRuleDirs() {
715
+ const override = {};
716
+ const extra = {};
717
+ const loaded = [];
718
+ const ignored = [];
719
+ for (const d of rulesDirs()) {
720
+ if (!existsSync(d)) continue;
721
+ let entries = [];
722
+ try {
723
+ entries = readdirSync(d).sort();
724
+ } catch {
725
+ continue;
726
+ }
727
+ for (const name of entries) {
728
+ const f = resolve(d, name);
729
+ try {
730
+ if (statSync(f).isDirectory()) continue;
731
+ } catch {
570
732
  continue;
571
733
  }
572
- for (const r of rules) {
573
- if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
574
- console.warn(
575
- `[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p} \u7C7B\u522B ${cat} \u6709\u6761\u76EE\u7F3A\u5FC5\u9700\u5B57\u6BB5 [${required.join(", ")}] \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`
576
- );
577
- continue;
734
+ if (name === OVERRIDE_FILENAME) {
735
+ const got = scanRulesFile(f, "override");
736
+ if (Object.keys(got).length > 0) loaded.push(f);
737
+ for (const [cat, items] of Object.entries(got)) {
738
+ if (Array.isArray(items)) {
739
+ override[cat] = (override[cat] || []).concat(items);
740
+ } else {
741
+ override[cat] = { ...override[cat] || {}, ...items };
742
+ }
578
743
  }
579
- const rid = r && typeof r === "object" ? String(r.id || "") : "";
580
- if (rid) {
581
- if (seenIds.has(rid)) {
582
- console.warn(
583
- `[AgentMoss] \u8FFD\u52A0\u89C4\u5219 id "${rid}" \u91CD\u590D\uFF08\u5148\u89C1\u4E8E ${seenIds.get(rid)}\uFF0C\u672C\u6761\u6765\u81EA ${p}\uFF09\u2014\u2014 \u5DF2\u8DF3\u8FC7\u672C\u6761\u3002`
584
- );
585
- continue;
744
+ } else if (name === EXTRA_FILENAME) {
745
+ const got = scanRulesFile(f, "extra");
746
+ if (Object.values(got).some((v) => v.length > 0)) loaded.push(f);
747
+ for (const [cat, items] of Object.entries(got)) {
748
+ if (Array.isArray(items)) {
749
+ extra[cat] = (extra[cat] || []).concat(items);
750
+ } else {
751
+ extra[cat] = { ...extra[cat] || {}, ...items };
586
752
  }
587
- seenIds.set(rid, p);
588
753
  }
589
- (data[cat] = data[cat] || []).push(r);
590
- nLoaded += 1;
754
+ } else if (name.toLowerCase().endsWith(".json")) {
755
+ ignored.push(f);
756
+ }
757
+ }
758
+ }
759
+ if (Object.keys(override).length > 0 && identicalToBuiltin(override)) {
760
+ const key = "override-identical::" + Object.entries(override).map(([c, v]) => `${c}=${v.length}`).sort().join("|");
761
+ if (!warnedIgnored.has(key)) {
762
+ warnedIgnored.add(key);
763
+ console.warn(
764
+ `[AgentMoss] \u2139\uFE0F ${OVERRIDE_FILENAME} \u4E0E**\u5185\u7F6E\u89C4\u5219\u5B8C\u5168\u4E00\u81F4**\uFF08` + Object.entries(override).sort().map(([c, v]) => `${c}\xD7${v.length}`).join(", ") + `\uFF09\u2014\u2014 \u770B\u8D77\u6765\u662F\u6A21\u677F\u539F\u6837\u62F7\u8FDB\u6765\u6CA1\u6539\u3002
765
+ \u5B83\u73B0\u5728\u7684\u6548\u679C = \u5185\u7F6E\u89C4\u5219\uFF1B\u4F46**\u4EE5\u540E\u5347\u7EA7\u5185\u7F6E\u89C4\u5219\u4E0D\u4F1A\u53CD\u6620\u5230\u8FD9\u4EFD\u4E0A**\uFF08\u88AB\u9489\u4F4F\u4E86\uFF09\u3002
766
+ \u53EA\u60F3\u8FFD\u52A0\u89C4\u5219\uFF1A\u5220\u6389 ${OVERRIDE_FILENAME}\uFF0C\u6539\u7528 ${EXTRA_FILENAME}\u3002`
767
+ );
768
+ }
769
+ }
770
+ if (Object.keys(override).length > 0) {
771
+ const key = "override-summary::" + Object.entries(override).map(([c, v]) => `${c}=${v.length}`).sort().join("|");
772
+ if (!warnedIgnored.has(key)) {
773
+ warnedIgnored.add(key);
774
+ const l1 = loadL1();
775
+ const l2 = loadL2();
776
+ const builtinCounts = {
777
+ dangerous_commands: (l1.dangerous_commands || []).length,
778
+ injection_detection: (l1.injection_detection || []).length,
779
+ sensitive_path_access: (l1.sensitive_path_access || []).length,
780
+ intent_consistency: (l2.intent_consistency || []).length,
781
+ password_modify_consent: (l2.password_modify_consent || []).length,
782
+ user_deletion_consent: (l2.user_deletion_consent || []).length
783
+ };
784
+ console.warn(`[AgentMoss] \u26A0\uFE0F ${OVERRIDE_FILENAME} \u751F\u6548\uFF08**\u6574\u4EFD\u66FF\u6362**\uFF0C\u672A\u5217\u51FA\u7684\u5185\u7F6E\u89C4\u5219\u4E0D\u518D\u751F\u6548\uFF09\uFF1A`);
785
+ for (const [cat, items] of Object.entries(override).sort()) {
786
+ console.warn(` \u7C7B\u522B ${cat}\uFF1A\u5185\u7F6E ${builtinCounts[cat] ?? 0} \u6761 \u2192 \u4F60\u7684 ${items.length} \u6761`);
591
787
  }
788
+ console.warn(` \u53EA\u60F3**\u8FFD\u52A0**\u89C4\u5219\u8BF7\u6539\u7528 ${EXTRA_FILENAME}\uFF1B\u8981\u6539\u67D0\u4E00\u6761\u5185\u7F6E\u89C4\u5219\uFF0C\u8BF7\u5148\u62F7\u8D1D\u5185\u7F6E\u6587\u4EF6\u518D\u6539\u3002`);
592
789
  }
593
- if (nLoaded > 0) {
594
- loaded.push(p);
595
- console.info(`[AgentMoss] \u5DF2\u52A0\u8F7D\u8FFD\u52A0\u89C4\u5219\u6587\u4EF6 ${p}\uFF1A${nLoaded} \u6761`);
790
+ }
791
+ if (ignored.length > 0) {
792
+ const key = "ignored::" + ignored.join("|");
793
+ if (!warnedIgnored.has(key)) {
794
+ warnedIgnored.add(key);
795
+ console.warn(
796
+ `[AgentMoss] \u26A0\uFE0F \u89C4\u5219\u76EE\u5F55\u91CC\u6709 ${ignored.length} \u4E2A json **\u4E0D\u4F1A\u88AB\u8BFB\u53D6**\uFF08\u53EA\u6709 ${OVERRIDE_FILENAME} \u4E0E ${EXTRA_FILENAME} \u4F1A\u88AB\u8BFB\uFF09: ${ignored.join(", ")}
797
+ \u8981\u8986\u76D6\u5185\u7F6E\u89C4\u5219 \u2192 \u5185\u5BB9\u653E\u8FDB ${OVERRIDE_FILENAME}\uFF1B\u8981\u8FFD\u52A0 \u2192 \u653E\u8FDB ${EXTRA_FILENAME}\u3002`
798
+ );
596
799
  }
597
800
  }
598
- return { data, loaded };
801
+ return { override, extra, loaded, ignored };
599
802
  }
600
- function loadExtraRules() {
601
- const raw = (process.env.AGENT_MOSS_EXTRA_RULES_DIR || "").trim();
602
- const dirs = extraRulesDirs();
603
- let files = [];
803
+ function rulesCached() {
804
+ if (!isHotReloadEnabled() && _RULES_CACHE) return _RULES_CACHE;
805
+ const dirs = rulesDirs();
806
+ const sig = RULES_DIR_ENVS.map((e) => `${e}=${process.env[e] || ""}`).join("|") + "|" + dirs.join("|");
807
+ const files = [];
604
808
  for (const d of dirs) {
605
- if (existsSync(d)) files.push(...listJsonFilesDeep(d));
606
- else console.warn(`[AgentMoss] \u8FFD\u52A0\u89C4\u5219\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${d}\uFF08AGENT_MOSS_EXTRA_RULES_DIR\uFF09`);
809
+ if (!existsSync(d)) continue;
810
+ try {
811
+ for (const name of readdirSync(d).sort()) {
812
+ if (name.toLowerCase().endsWith(".json")) files.push(resolve(d, name));
813
+ }
814
+ } catch {
815
+ }
607
816
  }
608
- files = files.sort();
609
- const fp = files.map((p) => {
817
+ const fp = JSON.stringify(files.map((p) => {
610
818
  try {
611
819
  const st = statSync(p);
612
820
  return [p, st.mtimeMs, st.size];
613
821
  } catch {
614
822
  return [p, -1, -1];
615
823
  }
616
- });
617
- if (extraCache && extraCache.raw === raw && JSON.stringify(extraCache.fp) === JSON.stringify(fp)) {
618
- return extraCache.data;
824
+ }));
825
+ if (_RULES_CACHE && _RULES_CACHE.sig === sig && _RULES_CACHE.fp === fp) return _RULES_CACHE;
826
+ _RULES_CACHE = { sig, fp, ...scanRuleDirs() };
827
+ return _RULES_CACHE;
828
+ }
829
+ function identicalToBuiltin(override) {
830
+ const l1 = loadL1();
831
+ const l2 = loadL2();
832
+ for (const [cat, items] of Object.entries(override)) {
833
+ const builtin = l1[cat] ?? l2[cat];
834
+ if (!Array.isArray(builtin) || builtin.length !== items.length) return false;
835
+ if (JSON.stringify(builtin) !== JSON.stringify(items)) return false;
619
836
  }
620
- const { data, loaded } = scanExtraFiles(files);
621
- extraCache = { raw, fp, data, loaded };
622
- return data;
837
+ return Object.keys(override).length > 0;
838
+ }
839
+ function loadOverrideRules() {
840
+ if (isBuiltinOnly()) return {};
841
+ return rulesCached().override;
842
+ }
843
+ function loadExtraRules() {
844
+ if (isBuiltinOnly()) return {};
845
+ return rulesCached().extra;
623
846
  }
624
- var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS, EXTRA_CATEGORIES, EXTRA_FLAT_FIELD, extraCache;
847
+ var __filename2, __dirname2, OVERRIDE_FILENAME, EXTRA_FILENAME, RULES_DIR_ENVS, RULES_DIR_CANDS, frozenJsonCache, SCENARIO_LIST_KEYS, EXTRA_CATEGORIES, STRUCT_KEYS, EXTRA_FLAT_FIELD, _RULES_CACHE, warnedIgnored;
625
848
  var init_rules_loader = __esm({
626
849
  "src/infra/rules-loader.ts"() {
627
850
  "use strict";
628
851
  init_esm_shims();
629
852
  init_paths();
853
+ init_rules_mode();
630
854
  init_runtime_config_bridge();
631
855
  __filename2 = fileURLToPath2(import.meta.url);
632
856
  __dirname2 = dirname(__filename2);
633
- EXPECTED_KEYS = {
634
- "l1_rules.json": ["dangerous_commands", "injection_detection", "sensitive_path_access"],
635
- "l2_rules.json": ["intent_consistency", "password_modify_consent", "user_deletion_consent"],
636
- "l3_rules.json": [],
637
- "mc_rules.json": ["rules"],
638
- "tool_registry.json": ["tools"]
639
- };
640
- warnedOverride = /* @__PURE__ */ new Set();
857
+ OVERRIDE_FILENAME = "override.json";
858
+ EXTRA_FILENAME = "extra.json";
859
+ RULES_DIR_ENVS = ["AGENT_MOSS_EXTRA_RULES_DIR", "AGENT_MOSS_OVERRIDE_RULES_DIR"];
641
860
  RULES_DIR_CANDS = (() => {
642
861
  const cands = [
643
- // 用户数据目录覆盖优先(与 Python rules_loader.resolve_rules_file 的
644
- // <data_root>/rules/<name> 优先逻辑对齐)。用户安装 pip/npm 包后把自定义
645
- // mc_rules.json 放到 <data_root>/rules/ 即可覆盖内置默认,无需改源码。
646
- resolve(dataRootDir(), "rules"),
862
+ // 注:`<data_root>/rules/` 不再是"按内置文件名覆盖"的查找位 —— 2026-10 起
863
+ // 规则目录只认 override.json / extra.json(见 rulesDirs())。
647
864
  // dist: __dirname=dist/ → dist/rules;__dirname=dist/engine/ → dist/engine/../rules=dist/rules
648
865
  resolve(__dirname2, "../rules"),
649
866
  resolve(__dirname2, "rules"),
@@ -656,6 +873,8 @@ var init_rules_loader = __esm({
656
873
  ];
657
874
  return cands.filter((c) => existsSync(c));
658
875
  })();
876
+ frozenJsonCache = /* @__PURE__ */ new Map();
877
+ SCENARIO_LIST_KEYS = ["extra_command_patterns", "extra_injection_keywords", "extra_sensitive_paths"];
659
878
  EXTRA_CATEGORIES = {
660
879
  dangerous_commands: ["pattern"],
661
880
  injection_detection: ["keyword", "lang"],
@@ -664,6 +883,7 @@ var init_rules_loader = __esm({
664
883
  password_modify_consent: ["pattern"],
665
884
  user_deletion_consent: ["pattern"]
666
885
  };
886
+ STRUCT_KEYS = ["skills", "ltl_rules", "tools"];
667
887
  EXTRA_FLAT_FIELD = [
668
888
  [["path"], "sensitive_path_access"],
669
889
  [["keyword", "lang"], "injection_detection"],
@@ -671,7 +891,8 @@ var init_rules_loader = __esm({
671
891
  [["pattern", "risk_level"], "dangerous_commands"],
672
892
  [["pattern", "risk_type"], "dangerous_commands"]
673
893
  ];
674
- extraCache = null;
894
+ _RULES_CACHE = null;
895
+ warnedIgnored = /* @__PURE__ */ new Set();
675
896
  }
676
897
  });
677
898
 
@@ -1735,6 +1956,7 @@ function userSkillsDir() {
1735
1956
  return resolve3(getDataRoot(), "agent_moss_skills");
1736
1957
  }
1737
1958
  function runtimeCustomSkillKeywords() {
1959
+ if (isRuntimeConfigDisabled()) return {};
1738
1960
  try {
1739
1961
  const rt = runtimeConfigBridge.current?.loadRuntimeConfig?.();
1740
1962
  if (!rt) return {};
@@ -1752,6 +1974,7 @@ function runtimeCustomSkillKeywords() {
1752
1974
  }
1753
1975
  }
1754
1976
  function loadUserSkill(name) {
1977
+ if (isRuntimeConfigDisabled()) return "";
1755
1978
  if (userSkillCache[name] !== void 0) return userSkillCache[name];
1756
1979
  try {
1757
1980
  const p = resolve3(userSkillsDir(), `${name}.md`);
@@ -1820,6 +2043,7 @@ var init_skill_loader = __esm({
1820
2043
  init_esm_shims();
1821
2044
  init_rules_loader();
1822
2045
  init_paths();
2046
+ init_rules_mode();
1823
2047
  init_asset_dir();
1824
2048
  init_runtime_config_bridge();
1825
2049
  __filename3 = fileURLToPath3(import.meta.url);
@@ -1880,6 +2104,7 @@ __export(runtime_config_exports, {
1880
2104
  getEnabledScenarioExtensions: () => getEnabledScenarioExtensions,
1881
2105
  getEnvOverrides: () => getEnvOverrides,
1882
2106
  getProfileOrDetect: () => getProfileOrDetect,
2107
+ getRuleSourceFingerprint: () => getRuleSourceFingerprint,
1883
2108
  getRuntimeConfigPath: () => getRuntimeConfigPath,
1884
2109
  getScenarioRules: () => getScenarioRules,
1885
2110
  hasRuntimeConfig: () => hasRuntimeConfig,
@@ -1914,7 +2139,22 @@ function getRuntimeConfigPath() {
1914
2139
  if (envPath) return envPath;
1915
2140
  return RUNTIME_CONFIG_PATH;
1916
2141
  }
2142
+ function getRuleSourceFingerprint() {
2143
+ if (isRuntimeConfigDisabled()) return `mode:${getRulesMode()}`;
2144
+ const p = getRuntimeConfigPath();
2145
+ try {
2146
+ const st = statSync2(p);
2147
+ return `${p}|${st.mtimeMs}|${st.size}`;
2148
+ } catch {
2149
+ return `${p}|absent`;
2150
+ }
2151
+ }
1917
2152
  function saveRuntimeConfig(config) {
2153
+ if (isRuntimeConfigDisabled()) {
2154
+ throw new Error(
2155
+ `\u89C4\u5219\u6765\u6E90\u6A21\u5F0F\u4E3A ${getRulesMode()}\uFF08runtime \u914D\u7F6E\u5DF2\u7981\u7528\uFF09\uFF1A\u4FEE\u6539\u89C4\u5219\u4E0D\u4F1A\u751F\u6548\uFF0C\u8BF7\u5207\u6362\u89C4\u5219\u6765\u6E90\u6A21\u5F0F\u5E76\u91CD\u542F\u670D\u52A1\u3002`
2156
+ );
2157
+ }
1918
2158
  const p = getRuntimeConfigPath();
1919
2159
  const dir = p.substring(0, p.lastIndexOf("/"));
1920
2160
  if (dir && !existsSync5(dir)) mkdirSync(dir, { recursive: true });
@@ -1938,6 +2178,18 @@ function invalidateRuntimeConfigCache() {
1938
2178
  hasRuntimeConfigCache.clear();
1939
2179
  }
1940
2180
  function loadRuntimeConfig() {
2181
+ if (isRuntimeConfigDisabled()) {
2182
+ return {
2183
+ version: 1,
2184
+ layers: { ...DEFAULT_LAYERS },
2185
+ L1_rules: {},
2186
+ L2_rules: {},
2187
+ L3_skills: {},
2188
+ L2_2: {},
2189
+ agent_rules: {},
2190
+ scenario_rules: {}
2191
+ };
2192
+ }
1941
2193
  const p = getRuntimeConfigPath();
1942
2194
  if (!existsSync5(p)) {
1943
2195
  const seeded = mergeConfig({}, generateSourceDefaults());
@@ -1984,6 +2236,7 @@ function loadRuntimeConfig() {
1984
2236
  function isLayerEnabled(layerKey) {
1985
2237
  const disableKey = LAYER_DISABLE_ENV[layerKey];
1986
2238
  if (disableKey && process.env[disableKey] === "1") return false;
2239
+ if (isRuntimeConfigDisabled()) return true;
1987
2240
  const runtime = loadRuntimeConfig();
1988
2241
  const layers = runtime.layers || {};
1989
2242
  if (layerKey in layers) return !!layers[layerKey];
@@ -2029,18 +2282,34 @@ function _mergeUserCustom(primary, runtime, layers, key) {
2029
2282
  const extra = _userCustomRules(runtime, layers, key);
2030
2283
  return extra.length > 0 ? [...primary, ...extra] : [...primary];
2031
2284
  }
2032
- function _mergeExtraDirRules(primary, category) {
2285
+ function _dedup(rules, category) {
2286
+ const keys = _DEDUP_KEYS[category];
2287
+ if (!keys) return rules;
2288
+ const seen = /* @__PURE__ */ new Set();
2289
+ const out = [];
2290
+ for (const r of rules) {
2291
+ const k = keys.map((x) => String(r?.[x] ?? "")).join("\0");
2292
+ if (seen.has(k)) continue;
2293
+ seen.add(k);
2294
+ out.push(r);
2295
+ }
2296
+ return out;
2297
+ }
2298
+ function _mergeRulesFromDirs(primary, category) {
2299
+ let base = [...primary];
2033
2300
  try {
2301
+ const override = loadOverrideRules()[category];
2302
+ if (override !== void 0 && override !== null) base = [...override];
2034
2303
  const extra = loadExtraRules()[category] || [];
2035
- return extra.length > 0 ? [...primary, ...extra] : [...primary];
2304
+ return _dedup([...base, ...extra], category);
2036
2305
  } catch {
2037
- return [...primary];
2306
+ return base;
2038
2307
  }
2039
2308
  }
2040
2309
  function getEnabledL1CommandPatterns(runtime, osType) {
2041
2310
  const cat = runtime.L1_rules?.dangerous_commands || { category_enabled: true, rules: [] };
2042
2311
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2043
- rules = _mergeExtraDirRules(rules, "dangerous_commands");
2312
+ rules = _mergeRulesFromDirs(rules, "dangerous_commands");
2044
2313
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "pattern");
2045
2314
  if (osType) rules = _filterRulesByOs(rules, osType);
2046
2315
  return rules.map((r) => ({
@@ -2056,7 +2325,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
2056
2325
  cat = runtime.L2_rules?.sensitive_path_access || { category_enabled: true, rules: [] };
2057
2326
  }
2058
2327
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2059
- rules = _mergeExtraDirRules(rules, "sensitive_path_access");
2328
+ rules = _mergeRulesFromDirs(rules, "sensitive_path_access");
2060
2329
  rules = _mergeUserCustom(rules, runtime, ["L1_rules", "L2_rules"], "path");
2061
2330
  if (osType) rules = _filterRulesByOs(rules, osType);
2062
2331
  return rules.map((r) => {
@@ -2068,6 +2337,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
2068
2337
  });
2069
2338
  }
2070
2339
  function hasRuntimeConfig() {
2340
+ if (isRuntimeConfigDisabled()) return false;
2071
2341
  const p = getRuntimeConfigPath();
2072
2342
  let key;
2073
2343
  try {
@@ -2076,8 +2346,8 @@ function hasRuntimeConfig() {
2076
2346
  } catch {
2077
2347
  return false;
2078
2348
  }
2079
- const cached = hasRuntimeConfigCache.get(key);
2080
- if (cached !== void 0) return cached;
2349
+ const cached2 = hasRuntimeConfigCache.get(key);
2350
+ if (cached2 !== void 0) return cached2;
2081
2351
  let ok = false;
2082
2352
  try {
2083
2353
  JSON.parse(readFileSync4(p, "utf-8"));
@@ -2441,25 +2711,11 @@ function generateSourceDefaults() {
2441
2711
  };
2442
2712
  }
2443
2713
  function _ltlRulesToRuntime() {
2444
- const cands = [
2445
- getRuntimeConfigDir() && join3(getRuntimeConfigDir(), "rules"),
2446
- join3(__dirname4, "../rules"),
2447
- join3(__dirname4, "rules"),
2448
- join3(__dirname4, "../../../agent_moss/rules"),
2449
- join3(__dirname4, "../../../../agent_moss/rules"),
2450
- join3(process.cwd(), "agent_moss/rules")
2451
- ].filter(Boolean);
2452
2714
  let mc = {};
2453
- for (const dir of cands) {
2454
- const p = join3(dir, "mc_rules.json");
2455
- if (existsSync5(p)) {
2456
- try {
2457
- mc = JSON.parse(readFileSync4(p, "utf-8")) || {};
2458
- } catch {
2459
- mc = {};
2460
- }
2461
- break;
2462
- }
2715
+ try {
2716
+ mc = loadMCRaw();
2717
+ } catch {
2718
+ mc = {};
2463
2719
  }
2464
2720
  const rules = mc.rules || [];
2465
2721
  return {
@@ -2688,7 +2944,7 @@ function _migrateCategoriesAcrossLayers(cfg, srcLayer, dstLayer, catNames) {
2688
2944
  function getEnabledL1InjectionKeywords(runtime, osType) {
2689
2945
  const cat = runtime.L1_rules?.injection_detection || { category_enabled: true, rules: [] };
2690
2946
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2691
- rules = _mergeExtraDirRules(rules, "injection_detection");
2947
+ rules = _mergeRulesFromDirs(rules, "injection_detection");
2692
2948
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "keyword");
2693
2949
  if (osType) rules = _filterRulesByOs(rules, osType);
2694
2950
  return rules.map((r) => ({
@@ -2700,7 +2956,7 @@ function getEnabledL1InjectionKeywords(runtime, osType) {
2700
2956
  function getEnabledL2IntentPatterns(runtime, osType) {
2701
2957
  const cat = runtime.L2_rules?.intent_consistency || { category_enabled: true, rules: [] };
2702
2958
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2703
- rules = _mergeExtraDirRules(rules, "intent_consistency");
2959
+ rules = _mergeRulesFromDirs(rules, "intent_consistency");
2704
2960
  rules = _mergeUserCustom(rules, runtime, "L2_rules", "intent_keywords");
2705
2961
  if (osType) rules = _filterRulesByOs(rules, osType);
2706
2962
  return rules.map((r) => ({
@@ -2712,7 +2968,7 @@ function getEnabledL2IntentPatterns(runtime, osType) {
2712
2968
  function getEnabledL2PasswordPatterns(runtime) {
2713
2969
  const cat = runtime.L2_rules?.password_modify_consent || { category_enabled: true, rules: [] };
2714
2970
  const rules = _mergeUserCustom(
2715
- _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2971
+ _mergeRulesFromDirs(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2716
2972
  runtime,
2717
2973
  "L2_rules",
2718
2974
  "pattern"
@@ -2722,7 +2978,7 @@ function getEnabledL2PasswordPatterns(runtime) {
2722
2978
  function getEnabledL2UserDeletionPatterns(runtime) {
2723
2979
  const cat = runtime.L2_rules?.user_deletion_consent || { category_enabled: true, rules: [] };
2724
2980
  const rules = _mergeUserCustom(
2725
- _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2981
+ _mergeRulesFromDirs(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2726
2982
  runtime,
2727
2983
  "L2_rules",
2728
2984
  "pattern"
@@ -2759,7 +3015,7 @@ function getEnabledL1CommandPatternsOrDefault(runtime, osType) {
2759
3015
  return getEnabledL1CommandPatterns(runtime, osType);
2760
3016
  }
2761
3017
  return _mergeUserCustom(
2762
- _mergeExtraDirRules(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
3018
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2763
3019
  runtime,
2764
3020
  "L1_rules",
2765
3021
  "pattern"
@@ -2771,7 +3027,7 @@ function getEnabledL1InjectionKeywordsOrDefault(runtime, osType) {
2771
3027
  return getEnabledL1InjectionKeywords(runtime, osType);
2772
3028
  }
2773
3029
  return _mergeUserCustom(
2774
- _mergeExtraDirRules(_l1InjectionKeywordsToRules(), "injection_detection"),
3030
+ _mergeRulesFromDirs(_l1InjectionKeywordsToRules(), "injection_detection"),
2775
3031
  runtime,
2776
3032
  "L1_rules",
2777
3033
  "keyword"
@@ -2783,7 +3039,7 @@ function getEnabledL2SensitivePathsOrDefault(runtime, osType) {
2783
3039
  return getEnabledL2SensitivePaths(runtime, osType);
2784
3040
  }
2785
3041
  return _mergeUserCustom(
2786
- _mergeExtraDirRules(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
3042
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2787
3043
  runtime,
2788
3044
  ["L1_rules", "L2_rules"],
2789
3045
  "path"
@@ -2795,7 +3051,7 @@ function getEnabledL2IntentPatternsOrDefault(runtime, osType) {
2795
3051
  return getEnabledL2IntentPatterns(runtime, osType);
2796
3052
  }
2797
3053
  return _mergeUserCustom(
2798
- _mergeExtraDirRules(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
3054
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2799
3055
  runtime,
2800
3056
  "L2_rules",
2801
3057
  "intent_keywords"
@@ -2971,6 +3227,11 @@ function deleteCustomRule(layer, category, ruleId) {
2971
3227
  }
2972
3228
  function addCustomSkill(skillId, category, keywords, content) {
2973
3229
  _validateSkillId(skillId);
3230
+ if (isRuntimeConfigDisabled()) {
3231
+ throw new Error(
3232
+ `\u89C4\u5219\u6765\u6E90\u6A21\u5F0F\u4E3A ${getRulesMode()}\uFF08runtime \u914D\u7F6E\u5DF2\u7981\u7528\uFF09\uFF0C\u4E0D\u5141\u8BB8\u65B0\u589E\u81EA\u5B9A\u4E49 skill\u3002`
3233
+ );
3234
+ }
2974
3235
  const runtime = loadRuntimeConfig();
2975
3236
  const skillsDir = join3(getRuntimeConfigDir(), "agent_moss_skills");
2976
3237
  try {
@@ -3186,7 +3447,7 @@ function updateScenarioCategoryEnabled(scenarioId, category, enabled) {
3186
3447
  saveRuntimeConfig(runtime);
3187
3448
  return runtime;
3188
3449
  }
3189
- var __dirname4, ENV_RUNTIME_CONFIG_PATH, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH, LAYER_DISABLE_ENV, DEFAULT_LAYERS, DENY_MODE_LABELS, runtimeConfigCache, hasRuntimeConfigCache, _AGENTS_DIR_PROD, _AGENTS_DIR_DEV, _SCENARIOS_DIR_PROD, _SCENARIOS_DIR_DEV, _effectiveCache, SKILL_ID_RE;
3450
+ var __dirname4, ENV_RUNTIME_CONFIG_PATH, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH, LAYER_DISABLE_ENV, DEFAULT_LAYERS, DENY_MODE_LABELS, runtimeConfigCache, _DEDUP_KEYS, hasRuntimeConfigCache, _AGENTS_DIR_PROD, _AGENTS_DIR_DEV, _SCENARIOS_DIR_PROD, _SCENARIOS_DIR_DEV, _effectiveCache, SKILL_ID_RE;
3190
3451
  var init_runtime_config = __esm({
3191
3452
  "src/infra/runtime-config.ts"() {
3192
3453
  "use strict";
@@ -3197,6 +3458,7 @@ var init_runtime_config = __esm({
3197
3458
  init_skill_loader();
3198
3459
  init_rules_loader();
3199
3460
  init_runtime_config_bridge();
3461
+ init_rules_mode();
3200
3462
  __dirname4 = dirname3(fileURLToPath4(import.meta.url));
3201
3463
  ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
3202
3464
  RUNTIME_CONFIG_DIR = runtimeDir();
@@ -3219,6 +3481,14 @@ var init_runtime_config = __esm({
3219
3481
  deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
3220
3482
  };
3221
3483
  runtimeConfigCache = null;
3484
+ _DEDUP_KEYS = {
3485
+ dangerous_commands: ["pattern"],
3486
+ password_modify_consent: ["pattern"],
3487
+ user_deletion_consent: ["pattern"],
3488
+ sensitive_path_access: ["path"],
3489
+ injection_detection: ["keyword", "lang"],
3490
+ intent_consistency: ["intent_keywords", "reason"]
3491
+ };
3222
3492
  hasRuntimeConfigCache = /* @__PURE__ */ new Map();
3223
3493
  _AGENTS_DIR_PROD = join3(__dirname4, "agents");
3224
3494
  _AGENTS_DIR_DEV = join3(__dirname4, "..", "..", "..", "agent_moss", "agents");
@@ -3244,6 +3514,8 @@ var init_runtime_config = __esm({
3244
3514
  var config_exports = {};
3245
3515
  __export(config_exports, {
3246
3516
  detectProvider: () => detectProvider,
3517
+ getAgentMossSetting: () => getAgentMossSetting,
3518
+ getCacheOptions: () => getCacheOptions,
3247
3519
  getCustomRules: () => getCustomRules,
3248
3520
  getLLMConfig: () => getLLMConfig,
3249
3521
  getLLMFailMode: () => getLLMFailMode,
@@ -3255,7 +3527,10 @@ __export(config_exports, {
3255
3527
  isHeuristicEnabled: () => isHeuristicEnabled,
3256
3528
  isLLMAvailable: () => isLLMAvailable,
3257
3529
  isLogicRulesEnabled: () => isLogicRulesEnabled,
3530
+ isPolicyGenEnabled: () => isPolicyGenEnabled,
3531
+ isPolicySaveEnabled: () => isPolicySaveEnabled,
3258
3532
  probeLLMConnection: () => probeLLMConnection,
3533
+ reloadCustomRules: () => reloadCustomRules,
3259
3534
  updateLLMConfig: () => updateLLMConfig,
3260
3535
  userRuleVariantPattern: () => userRuleVariantPattern,
3261
3536
  wildcardToRegExp: () => wildcardToRegExp
@@ -3277,8 +3552,10 @@ function resolveConfigPath() {
3277
3552
  const userConfig = path2.join(getConfigDir(), "config.json");
3278
3553
  if (fs2.existsSync(userConfig)) return userConfig;
3279
3554
  const defaultCandidates = [
3280
- path2.join(__dirname, "config.json.example"),
3281
- path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example")
3555
+ path2.join(__configModuleDir, "config.json.example"),
3556
+ path2.join(__configModuleDir, "..", "config.json.example"),
3557
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "infra", "config.json.example"),
3558
+ path2.join(__configModuleDir, "..", "..", "..", "agent_moss", "infra", "config.json.example")
3282
3559
  ];
3283
3560
  const defaultConfig = defaultCandidates.find((p) => fs2.existsSync(p));
3284
3561
  if (defaultConfig) {
@@ -3287,8 +3564,8 @@ function resolveConfigPath() {
3287
3564
  const pkgConfig = loadConfigFile(defaultConfig);
3288
3565
  const safeConfig = pkgConfig ? { ...pkgConfig, llm: { ...pkgConfig.llm, api_key: "" } } : {};
3289
3566
  fs2.writeFileSync(userConfig, JSON.stringify(safeConfig, null, 4), "utf-8");
3290
- console.log(`[AgentMoss] \u5DF2\u521B\u5EFA\u7528\u6237\u914D\u7F6E\u6587\u4EF6: ${userConfig}`);
3291
- console.log(`[AgentMoss] \u8BF7\u7F16\u8F91\u8BE5\u6587\u4EF6\u914D\u7F6E\u4F60\u7684 LLM API Key`);
3567
+ console.error(`[AgentMoss] \u5DF2\u521B\u5EFA\u7528\u6237\u914D\u7F6E\u6587\u4EF6: ${userConfig}`);
3568
+ console.error(`[AgentMoss] \u8BF7\u7F16\u8F91\u8BE5\u6587\u4EF6\u914D\u7F6E\u4F60\u7684 LLM API Key`);
3292
3569
  }
3293
3570
  return userConfig;
3294
3571
  }
@@ -3334,17 +3611,19 @@ function wildcardToRegExp(pattern) {
3334
3611
  }
3335
3612
  function loadCustomRules() {
3336
3613
  const rules = [];
3337
- try {
3338
- const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
3339
- if (raw) {
3340
- const parsed = JSON.parse(raw);
3341
- if (Array.isArray(parsed)) {
3342
- for (const r of parsed) {
3343
- if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
3614
+ if (!isBuiltinOnly()) {
3615
+ try {
3616
+ const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
3617
+ if (raw) {
3618
+ const parsed = JSON.parse(raw);
3619
+ if (Array.isArray(parsed)) {
3620
+ for (const r of parsed) {
3621
+ if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
3622
+ }
3344
3623
  }
3345
3624
  }
3625
+ } catch {
3346
3626
  }
3347
- } catch {
3348
3627
  }
3349
3628
  for (const rulesPath of USER_RULES_PATHS) {
3350
3629
  try {
@@ -3373,6 +3652,9 @@ function userRuleVariantPattern(pattern) {
3373
3652
  }
3374
3653
  return null;
3375
3654
  }
3655
+ function reloadCustomRules() {
3656
+ customRules = loadCustomRules();
3657
+ }
3376
3658
  function getLLMConfig() {
3377
3659
  return llmConfig;
3378
3660
  }
@@ -3460,7 +3742,33 @@ function layerEnabledOrDefault(key) {
3460
3742
  }
3461
3743
  }
3462
3744
  function isBrainEnabled() {
3463
- return process.env.AGENT_MOSS_BRAIN_ENABLED === "1";
3745
+ if (isRuntimeConfigDisabled()) return false;
3746
+ return getAgentMossSetting("AGENT_MOSS_BRAIN_ENABLED", "") === "1";
3747
+ }
3748
+ function getAgentMossSetting(key, def = "") {
3749
+ const envValue = process.env[key];
3750
+ if (envValue !== void 0) return envValue;
3751
+ const cfg = loadConfigFile(resolveConfigPath());
3752
+ if (cfg && key in cfg) return String(cfg[key]);
3753
+ return def;
3754
+ }
3755
+ function isPolicyGenEnabled() {
3756
+ return getAgentMossSetting("AGENT_MOSS_ENABLE_POLICY_GEN", "") === "1";
3757
+ }
3758
+ function getCacheOptions() {
3759
+ let enabled = true;
3760
+ let maxSize = 1e3;
3761
+ const cfg = loadConfigFile(resolveConfigPath());
3762
+ const cache3 = cfg?.cache;
3763
+ if (cache3 && typeof cache3 === "object") {
3764
+ if (typeof cache3.enabled === "boolean") enabled = cache3.enabled;
3765
+ if (typeof cache3.max_size === "number" && cache3.max_size > 0) maxSize = cache3.max_size;
3766
+ }
3767
+ if (process.env.AGENT_MOSS_DISABLE_CACHE === "1") enabled = false;
3768
+ return { enabled, maxSize };
3769
+ }
3770
+ function isPolicySaveEnabled() {
3771
+ return getAgentMossSetting("AGENT_MOSS_SAVE_POLICY", "1") !== "0";
3464
3772
  }
3465
3773
  function isLogicRulesEnabled() {
3466
3774
  if (process.env.AGENT_MOSS_DISABLE_LOGIC_RULES === "1") return false;
@@ -3469,13 +3777,21 @@ function isLogicRulesEnabled() {
3469
3777
  function isFormalObserveMode() {
3470
3778
  return process.env.AGENT_MOSS_FORMAL_OBSERVE === "1";
3471
3779
  }
3472
- var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
3780
+ var __configModuleDir, timeoutConfig, retryConfig, llmConfig, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
3473
3781
  var init_config = __esm({
3474
3782
  "src/config.ts"() {
3475
3783
  "use strict";
3476
3784
  init_esm_shims();
3477
3785
  init_paths();
3478
3786
  init_runtime_config();
3787
+ init_rules_mode();
3788
+ __configModuleDir = (() => {
3789
+ try {
3790
+ return path2.dirname(fileURLToPath5(import.meta.url));
3791
+ } catch {
3792
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
3793
+ }
3794
+ })();
3479
3795
  llmConfig = (() => {
3480
3796
  const configPath = resolveConfigPath();
3481
3797
  const fileConfig = fs2.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
@@ -3493,13 +3809,6 @@ var init_config = __esm({
3493
3809
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
3494
3810
  };
3495
3811
  })();
3496
- __configModuleDir = (() => {
3497
- try {
3498
- return path2.dirname(fileURLToPath5(import.meta.url));
3499
- } catch {
3500
- return typeof __dirname !== "undefined" ? __dirname : process.cwd();
3501
- }
3502
- })();
3503
3812
  USER_RULES_PATHS = [
3504
3813
  path2.join(__configModuleDir, "rules", "user_rules.json"),
3505
3814
  // 源在 ts/src/rules(一般不存在)
@@ -3532,7 +3841,9 @@ function resolveAgentsDir() {
3532
3841
  }
3533
3842
  function getAgentExtensions(agentId) {
3534
3843
  if (!agentId) return null;
3535
- if (cache.has(agentId)) return cache.get(agentId) ?? null;
3844
+ const fp = getRuleSourceFingerprint();
3845
+ const cached2 = cache.get(agentId);
3846
+ if (cached2 && cached2.fp === fp) return cached2.ext;
3536
3847
  let ext = null;
3537
3848
  try {
3538
3849
  if (hasRuntimeConfig()) {
@@ -3544,7 +3855,7 @@ function getAgentExtensions(agentId) {
3544
3855
  const agentsDir = resolveAgentsDir();
3545
3856
  const rulesFile = resolve4(agentsDir, agentId, "rules.json");
3546
3857
  if (!existsSync6(rulesFile)) {
3547
- cache.set(agentId, null);
3858
+ cache.set(agentId, { fp, ext: null });
3548
3859
  return null;
3549
3860
  }
3550
3861
  try {
@@ -3552,16 +3863,16 @@ function getAgentExtensions(agentId) {
3552
3863
  ext = JSON.parse(raw);
3553
3864
  if (!ext.agent_id) {
3554
3865
  console.warn(`[AgentMoss agents] rules.json \u7F3A\u5C11 agent_id \u5B57\u6BB5: ${rulesFile}`);
3555
- cache.set(agentId, null);
3866
+ cache.set(agentId, { fp, ext: null });
3556
3867
  return null;
3557
3868
  }
3558
3869
  } catch (e) {
3559
3870
  console.error(`[AgentMoss agents] \u52A0\u8F7D agent ${agentId} rules.json \u5931\u8D25:`, e.message);
3560
- cache.set(agentId, null);
3871
+ cache.set(agentId, { fp, ext: null });
3561
3872
  return null;
3562
3873
  }
3563
3874
  }
3564
- cache.set(agentId, ext);
3875
+ cache.set(agentId, { fp, ext });
3565
3876
  console.error(
3566
3877
  `[AgentMoss agents] \u52A0\u8F7D agent ${agentId} \u5B9A\u5236\u89C4\u5219: ${ext.extra_command_patterns?.length ?? 0} cmd patterns, ${ext.extra_injection_keywords?.length ?? 0} injection keywords, ${ext.extra_sensitive_paths?.length ?? 0} sensitive paths, ${ext.builtin_safe_skills?.length ?? 0} builtin safe skills, ${ext.builtin_safe_action_types?.length ?? 0} builtin safe action types`
3567
3878
  );
@@ -3607,7 +3918,9 @@ function resolveScenariosDir() {
3607
3918
  }
3608
3919
  function getScenarioExtensions(scenarioId) {
3609
3920
  if (!scenarioId) return null;
3610
- if (cache2.has(scenarioId)) return cache2.get(scenarioId) ?? null;
3921
+ const fp = getRuleSourceFingerprint();
3922
+ const cached2 = cache2.get(scenarioId);
3923
+ if (cached2 && cached2.fp === fp) return cached2.ext;
3611
3924
  let ext = null;
3612
3925
  let overrideHit = null;
3613
3926
  try {
@@ -3631,7 +3944,7 @@ function getScenarioExtensions(scenarioId) {
3631
3944
  );
3632
3945
  }
3633
3946
  if (!existsSync7(rulesFile)) {
3634
- cache2.set(scenarioId, null);
3947
+ cache2.set(scenarioId, { fp, ext: null });
3635
3948
  return null;
3636
3949
  }
3637
3950
  try {
@@ -3640,7 +3953,7 @@ function getScenarioExtensions(scenarioId) {
3640
3953
  const id = data.scenario_id || data.agent_id || "";
3641
3954
  if (!id) {
3642
3955
  console.warn(`[AgentMoss scenarios] rules.json \u7F3A\u5C11 scenario_id \u5B57\u6BB5: ${rulesFile}`);
3643
- cache2.set(scenarioId, null);
3956
+ cache2.set(scenarioId, { fp, ext: null });
3644
3957
  return null;
3645
3958
  }
3646
3959
  ext = {
@@ -3655,11 +3968,11 @@ function getScenarioExtensions(scenarioId) {
3655
3968
  };
3656
3969
  } catch (e) {
3657
3970
  console.error(`[AgentMoss scenarios] \u52A0\u8F7D\u573A\u666F ${scenarioId} rules.json \u5931\u8D25:`, e.message);
3658
- cache2.set(scenarioId, null);
3971
+ cache2.set(scenarioId, { fp, ext: null });
3659
3972
  return null;
3660
3973
  }
3661
3974
  }
3662
- cache2.set(scenarioId, ext);
3975
+ cache2.set(scenarioId, { fp, ext });
3663
3976
  console.error(
3664
3977
  `[AgentMoss scenarios] \u52A0\u8F7D\u573A\u666F ${scenarioId} \u5B9A\u5236\u89C4\u5219: ${ext.extra_command_patterns?.length ?? 0} cmd patterns, ${ext.extra_injection_keywords?.length ?? 0} injection keywords, ${ext.extra_sensitive_paths?.length ?? 0} sensitive paths`
3665
3978
  );
@@ -6980,10 +7293,10 @@ async function runLLMAnalysis(input, previous) {
6980
7293
  const config = input.llmConfig?.apiKey ? { ...globalConfig, ...input.llmConfig } : globalConfig;
6981
7294
  const rawDetail = input.aNext.actionDetail.replace(/^terminal-\w+:\s*command=([^,]+).*/, "$1").trim();
6982
7295
  const cacheKey = `${input.aNext.actionType}:${rawDetail || input.aNext.actionDetail}`;
6983
- const cached = llmCache.get(cacheKey);
6984
- if (cached) {
7296
+ const cached2 = llmCache.get(cacheKey);
7297
+ if (cached2) {
6985
7298
  console.log("[AgentMoss] LLM cache hit");
6986
- return cached;
7299
+ return cached2;
6987
7300
  }
6988
7301
  const matchedSkills = matchSkills(input.aNext.actionType, input.aNext.actionDetail, input.reason);
6989
7302
  const templates = loadTemplates();
@@ -7286,6 +7599,56 @@ var BrainRuleStore = class {
7286
7599
  getRule(ruleId) {
7287
7600
  return this.data.rules.find((r) => r.id === ruleId);
7288
7601
  }
7602
+ /** 质疑:active → disputed,并把 disputed_count +1(镜像 Python dispute_rule)。 */
7603
+ disputeRule(ruleId, _reason = "") {
7604
+ const rule = this.getRule(ruleId);
7605
+ if (!rule) throw new Error(`\u89C4\u5219\u4E0D\u5B58\u5728: ${ruleId}`);
7606
+ if (rule.status !== STATUS_ACTIVE) throw new Error(`\u53EA\u6709 active \u72B6\u6001\u53EF\u8D28\u7591\uFF0C\u5F53\u524D: ${rule.status}`);
7607
+ rule.status = STATUS_DISPUTED;
7608
+ rule.disputedCount += 1;
7609
+ this.save();
7610
+ }
7611
+ // ==================== 生命周期(curator 用,镜像 Python rule_store) ====================
7612
+ /** 长期未触发的 active/disputed 规则 → stale。 */
7613
+ markStale(staleDays = 30) {
7614
+ const cutoff = Date.now() - staleDays * 864e5;
7615
+ for (const r of this.data.rules) {
7616
+ if ((r.status === STATUS_ACTIVE || r.status === STATUS_DISPUTED) && r.lastTriggeredAt > 0 && r.lastTriggeredAt < cutoff) {
7617
+ r.status = STATUS_STALE;
7618
+ }
7619
+ }
7620
+ this.save();
7621
+ }
7622
+ /** 长期 stale 的规则 → archived。 */
7623
+ markArchived(archiveDays = 90) {
7624
+ const cutoff = Date.now() - archiveDays * 864e5;
7625
+ for (const r of this.data.rules) {
7626
+ if (r.status === STATUS_STALE && r.lastTriggeredAt > 0 && r.lastTriggeredAt < cutoff) {
7627
+ r.status = STATUS_ARCHIVED;
7628
+ }
7629
+ }
7630
+ this.save();
7631
+ }
7632
+ /** disputed 次数达阈值 → 自动降级 stale(自动纠错闭环)。 */
7633
+ autoDemoteDisputed(disputeThreshold = 3) {
7634
+ for (const r of this.data.rules) {
7635
+ if (r.status === STATUS_DISPUTED && r.disputedCount >= disputeThreshold) {
7636
+ r.status = STATUS_STALE;
7637
+ }
7638
+ }
7639
+ this.save();
7640
+ }
7641
+ // ==================== 中心化预留(镜像 Python) ====================
7642
+ /** 导出本地新增(非 central 来源)的 active 规则。 */
7643
+ getRulesForExport() {
7644
+ return this.data.rules.filter((r) => r.source !== "central" && r.status === STATUS_ACTIVE);
7645
+ }
7646
+ /** 导入中心下发规则(强制标 central 来源,避免被再次回流)。 */
7647
+ importRules(rules) {
7648
+ for (const rule of rules) {
7649
+ this.addRule({ ...rule, source: "central", originNode: "central" });
7650
+ }
7651
+ }
7289
7652
  getActiveRules(layer) {
7290
7653
  return this.data.rules.filter((r) => r.status === STATUS_ACTIVE && (layer === void 0 || r.layer === layer));
7291
7654
  }
@@ -7386,12 +7749,14 @@ var PatternExtractor = class {
7386
7749
  minOccurrences;
7387
7750
  builtinPatterns;
7388
7751
  reviewFn;
7752
+ skillManager;
7389
7753
  constructor(ruleStore, opts) {
7390
7754
  this.ruleStore = ruleStore;
7391
7755
  this.minConfidence = opts?.minConfidence ?? 0.9;
7392
7756
  this.minOccurrences = opts?.minOccurrences ?? 3;
7393
7757
  this.builtinPatterns = opts?.builtinPatterns ?? [];
7394
7758
  this.reviewFn = opts?.review ?? null;
7759
+ this.skillManager = opts?.skillManager ?? null;
7395
7760
  }
7396
7761
  async extract(event, llmResult) {
7397
7762
  const result = { rulesCreated: 0, patternsRecorded: 0, skippedReason: "", deduped: [] };
@@ -7480,8 +7845,96 @@ var PatternExtractor = class {
7480
7845
  result.rulesCreated += 1;
7481
7846
  }
7482
7847
  }
7848
+ if (this.skillManager) {
7849
+ const skill = this.buildSkill(actionDetail, reason, riskType, eventId);
7850
+ if (skill) {
7851
+ const id = this.skillManager.addSkill(skill);
7852
+ if (id) {
7853
+ result.skillsCreated = (result.skillsCreated ?? 0) + 1;
7854
+ }
7855
+ }
7856
+ }
7483
7857
  return result;
7484
7858
  }
7859
+ /**
7860
+ * 生成候选 Skill;不值得沉淀时返回 null(镜像 Python `_extract_skill` 的三道价值评估)。
7861
+ *
7862
+ * 评估 1:reason 太短(<50 字)说明没有可复用的判断逻辑,不沉淀;
7863
+ * 评估 2:同名 skill 已存在 → 跳过;
7864
+ * 评估 3:与已有 active skill 关键词重叠 >= 2 → 跳过(防同类案例重复沉淀,
7865
+ * 如 cat /etc/shadow 与 cat /etc/sudoers 生成两个近乎重复的 skill)。
7866
+ */
7867
+ buildSkill(actionDetail, reason, riskType, eventId) {
7868
+ if (!reason || reason.length < 50) return null;
7869
+ if (!riskType) return null;
7870
+ const detailWords = (actionDetail.toLowerCase().match(/[a-z]+/g) || []).slice(0, 3);
7871
+ const name = detailWords.length > 0 ? `brain_${riskType}_${detailWords.join("_")}` : `brain_${riskType}`;
7872
+ if (this.skillManager.getSkillMeta(name)) return null;
7873
+ const keywords = this.extractSkillKeywords(actionDetail, reason, riskType);
7874
+ if (this.isCoveredByExistingSkill(keywords)) return null;
7875
+ return {
7876
+ name,
7877
+ category: this.categorizeRisk(riskType),
7878
+ content: this.generateSkillContent(name, actionDetail, reason, riskType),
7879
+ keywords,
7880
+ sourceEventIds: eventId ? [eventId] : [],
7881
+ status: "active"
7882
+ };
7883
+ }
7884
+ /** 关键词提取(镜像 Python `_extract_keywords`)。 */
7885
+ extractSkillKeywords(actionDetail, reason, riskType) {
7886
+ const keywords = /* @__PURE__ */ new Set();
7887
+ if (riskType) keywords.add(riskType);
7888
+ for (const w of (actionDetail.toLowerCase().match(/[a-z_]{3,}/g) || []).slice(0, 10)) keywords.add(w);
7889
+ for (const w of (reason.toLowerCase().match(/[a-z_]{3,}/g) || []).slice(0, 5)) keywords.add(w);
7890
+ return [...keywords];
7891
+ }
7892
+ /** 与已有 active skill 关键词重叠 >= 2 → 视为已覆盖(镜像 `_is_covered_by_existing_skill`)。 */
7893
+ isCoveredByExistingSkill(keywords) {
7894
+ if (keywords.length === 0) return false;
7895
+ let existing = [];
7896
+ try {
7897
+ existing = this.skillManager.getActiveSkills();
7898
+ } catch {
7899
+ return false;
7900
+ }
7901
+ const cand = new Set(keywords.map((k) => k.toLowerCase()));
7902
+ for (const meta of existing) {
7903
+ const metaKeywords = (meta.keywords || []).map((k) => String(k).toLowerCase());
7904
+ if (metaKeywords.length === 0) continue;
7905
+ const overlap = metaKeywords.filter((k) => cand.has(k)).length;
7906
+ if (overlap >= 2) return true;
7907
+ }
7908
+ return false;
7909
+ }
7910
+ /** 风险类型 → skill 分类(镜像 Python `_categorize_risk`)。 */
7911
+ categorizeRisk(riskType) {
7912
+ if (!riskType) return "edge_case";
7913
+ if (["inject", "backdoor", "persist"].some((k) => riskType.includes(k))) return "attack_pattern";
7914
+ if (["exfil", "leak", "export"].some((k) => riskType.includes(k))) return "attack_pattern";
7915
+ return "defense_strategy";
7916
+ }
7917
+ /** skill 正文(与 Python `_generate_skill_content` 同结构,便于双端对拍)。 */
7918
+ generateSkillContent(name, actionDetail, reason, riskType) {
7919
+ return `# ${name}
7920
+
7921
+ ## \u6765\u6E90
7922
+ Brain \u81EA\u52A8\u63D0\u53D6\uFF08LLM \u5206\u6790\u6C89\u6DC0\uFF09
7923
+
7924
+ ## \u573A\u666F
7925
+ \u5F53 Agent \u6267\u884C\u4EE5\u4E0B\u7C7B\u578B\u64CD\u4F5C\u65F6\u89E6\u53D1\uFF1A
7926
+ - \u98CE\u9669\u7C7B\u578B: ${riskType}
7927
+ - \u64CD\u4F5C\u7279\u5F81: ${actionDetail.slice(0, 200)}
7928
+
7929
+ ## \u5224\u65AD\u4F9D\u636E
7930
+ ${reason.slice(0, 500)}
7931
+
7932
+ ## \u5904\u7406\u5EFA\u8BAE
7933
+ - \u4F18\u5148\u68C0\u67E5\u64CD\u4F5C\u7684\u4E0A\u4E0B\u6587\u548C\u610F\u56FE
7934
+ - \u7ED3\u5408 action_history \u5224\u65AD\u662F\u5426\u5B58\u5728\u6076\u610F\u94FE
7935
+ - \u5FC5\u8981\u65F6 Deny \u5E76\u8BB0\u5F55\u539F\u56E0
7936
+ `;
7937
+ }
7485
7938
  extractPatterns(actionDetail, reason, riskType) {
7486
7939
  const patterns = [];
7487
7940
  const detailLower = actionDetail.toLowerCase();
@@ -7669,6 +8122,209 @@ function parseReview(content) {
7669
8122
  };
7670
8123
  }
7671
8124
 
8125
+ // src/brain/skill-manager.ts
8126
+ init_esm_shims();
8127
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync13, renameSync, writeFileSync as writeFileSync4 } from "fs";
8128
+ import { homedir as homedir5 } from "os";
8129
+ import { join as join8 } from "path";
8130
+ import { randomUUID } from "crypto";
8131
+ var DEFAULT_SKILL_META_PATH = join8(homedir5(), ".agentmoss", "brain", "skill_meta.json");
8132
+ var ENV_SKILL_META_PATH = "AGENT_MOSS_BRAIN_SKILL_META_PATH";
8133
+ function getMetaPath(override) {
8134
+ return override || process.env[ENV_SKILL_META_PATH] || DEFAULT_SKILL_META_PATH;
8135
+ }
8136
+ var BrainSkillManager = class {
8137
+ constructor(skillsDir, metaPath) {
8138
+ this.skillsDir = skillsDir;
8139
+ this.metaPath = getMetaPath(metaPath);
8140
+ try {
8141
+ mkdirSync4(join8(this.metaPath, ".."), { recursive: true });
8142
+ } catch {
8143
+ }
8144
+ this.load();
8145
+ this.cleanupGhostSkills();
8146
+ }
8147
+ skillsDir;
8148
+ skills = [];
8149
+ metaPath;
8150
+ load() {
8151
+ if (!existsSync13(this.metaPath)) return;
8152
+ try {
8153
+ const data = JSON.parse(readFileSync13(this.metaPath, "utf-8"));
8154
+ this.skills = data.skills || [];
8155
+ } catch {
8156
+ this.skills = [];
8157
+ }
8158
+ }
8159
+ save() {
8160
+ const out = { version: 1, skills: this.skills };
8161
+ const tmp = `${this.metaPath}.tmp`;
8162
+ try {
8163
+ writeFileSync4(tmp, JSON.stringify(out, null, 2), "utf-8");
8164
+ renameSync(tmp, this.metaPath);
8165
+ } catch {
8166
+ }
8167
+ }
8168
+ /**
8169
+ * 清理"幽灵 skill":meta 记 active 但磁盘 .md 已不存在。
8170
+ * (沿用 Python 的处理:置 stale,避免污染关键词表并阻塞同名新 skill 生成。)
8171
+ */
8172
+ cleanupGhostSkills() {
8173
+ let changed = false;
8174
+ for (const sk of this.skills) {
8175
+ if (sk.status !== "active") continue;
8176
+ if (!existsSync13(join8(this.skillsDir, `${sk.name}.md`))) {
8177
+ sk.status = "stale";
8178
+ changed = true;
8179
+ }
8180
+ }
8181
+ if (changed) this.save();
8182
+ }
8183
+ /** 新增 Skill:写 .md + 记元数据。目录缺失或同名已存在时跳过(返回 "")。 */
8184
+ addSkill(skill) {
8185
+ const id = skill.id || randomUUID();
8186
+ const name = skill.name || `brain_${id.slice(0, 8)}`;
8187
+ if (!existsSync13(this.skillsDir)) return "";
8188
+ const mdPath = join8(this.skillsDir, `${name}.md`);
8189
+ if (existsSync13(mdPath)) return "";
8190
+ try {
8191
+ writeFileSync4(mdPath, skill.content, "utf-8");
8192
+ } catch {
8193
+ return "";
8194
+ }
8195
+ this.skills.push({
8196
+ id,
8197
+ name,
8198
+ category: skill.category || "",
8199
+ keywords: skill.keywords || [],
8200
+ source_event_ids: skill.sourceEventIds || [],
8201
+ status: skill.status || "draft",
8202
+ created_at: skill.createdAt || Date.now() / 1e3,
8203
+ usage_count: skill.usageCount || 0,
8204
+ origin_node: skill.originNode || "local"
8205
+ });
8206
+ this.save();
8207
+ return id;
8208
+ }
8209
+ getSkillMeta(name) {
8210
+ return this.skills.find((s) => s.name === name);
8211
+ }
8212
+ getActiveSkills() {
8213
+ return this.skills.filter((s) => s.status === "active").sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
8214
+ }
8215
+ recordUsage(name) {
8216
+ const sk = this.getSkillMeta(name);
8217
+ if (!sk) return;
8218
+ sk.usage_count = (sk.usage_count || 0) + 1;
8219
+ this.save();
8220
+ }
8221
+ /** 长期未使用(usage_count=0)且创建超过 staleDays 的 active skill → stale。 */
8222
+ markStale(staleDays = 60) {
8223
+ const cutoff = Date.now() / 1e3 - staleDays * 86400;
8224
+ let changed = false;
8225
+ for (const sk of this.skills) {
8226
+ if (sk.status === "active" && (sk.usage_count || 0) === 0 && (sk.created_at || 0) < cutoff) {
8227
+ sk.status = "stale";
8228
+ changed = true;
8229
+ }
8230
+ }
8231
+ if (changed) this.save();
8232
+ }
8233
+ /** 导出本地 active skill(含正文),供回流中心。 */
8234
+ exportSkills() {
8235
+ const out = [];
8236
+ for (const sk of this.skills) {
8237
+ if (sk.origin_node !== "local" || sk.status !== "active") continue;
8238
+ const mdPath = join8(this.skillsDir, `${sk.name}.md`);
8239
+ if (!existsSync13(mdPath)) continue;
8240
+ out.push({
8241
+ name: sk.name,
8242
+ category: sk.category || "",
8243
+ content: readFileSync13(mdPath, "utf-8"),
8244
+ keywords: sk.keywords || []
8245
+ });
8246
+ }
8247
+ return out;
8248
+ }
8249
+ /** 导入中心下发 skill(标 central,避免被再次回流)。 */
8250
+ importSkills(skills) {
8251
+ for (const s of skills) {
8252
+ this.addSkill({
8253
+ name: s.name,
8254
+ category: s.category || "",
8255
+ content: s.content,
8256
+ keywords: s.keywords || [],
8257
+ status: "active",
8258
+ originNode: "central"
8259
+ });
8260
+ }
8261
+ }
8262
+ /** 保留 close 签名(JSON 落盘已逐次持久化)。 */
8263
+ close() {
8264
+ this.save();
8265
+ }
8266
+ };
8267
+
8268
+ // src/brain/curator.ts
8269
+ init_esm_shims();
8270
+ var BrainCurator = class {
8271
+ constructor(ruleStore, skillManager) {
8272
+ this.ruleStore = ruleStore;
8273
+ this.skillManager = skillManager;
8274
+ }
8275
+ ruleStore;
8276
+ skillManager;
8277
+ /**
8278
+ * 执行一次维护,返回报告。
8279
+ *
8280
+ * autoApprove 默认 true:brain 提取的规则达阈值后自动生效(学习闭环);
8281
+ * 需人工审核时设 false(由 Console 手动 approve)。
8282
+ */
8283
+ runMaintenance(opts = {}) {
8284
+ const {
8285
+ staleRuleDays = 30,
8286
+ archiveRuleDays = 90,
8287
+ staleSkillDays = 60,
8288
+ disputeThreshold = 3,
8289
+ autoApprove = true
8290
+ } = opts;
8291
+ const autoApproved = autoApprove ? this.autoApproveDrafts() : [];
8292
+ this.ruleStore.markStale(staleRuleDays);
8293
+ this.ruleStore.markArchived(archiveRuleDays);
8294
+ this.ruleStore.autoDemoteDisputed(disputeThreshold);
8295
+ this.skillManager.markStale(staleSkillDays);
8296
+ return {
8297
+ rule_stats: this.ruleStore.getStats(),
8298
+ auto_approved: autoApproved
8299
+ };
8300
+ }
8301
+ /** draft → active(仅未 disputed 的 draft)。返回被激活规则的 pattern 前缀。 */
8302
+ autoApproveDrafts() {
8303
+ const approved = [];
8304
+ let drafts;
8305
+ try {
8306
+ drafts = this.ruleStore.getDraftRules();
8307
+ } catch {
8308
+ return approved;
8309
+ }
8310
+ for (const rule of drafts) {
8311
+ try {
8312
+ this.ruleStore.approveRule(rule.id);
8313
+ approved.push(rule.pattern.slice(0, 60));
8314
+ } catch {
8315
+ }
8316
+ }
8317
+ return approved;
8318
+ }
8319
+ };
8320
+
8321
+ // src/brain/contributor.ts
8322
+ init_esm_shims();
8323
+ init_asset_dir();
8324
+ import { dirname as dirname9 } from "path";
8325
+ import { fileURLToPath as fileURLToPath9 } from "url";
8326
+ var __dirname8 = dirname9(fileURLToPath9(import.meta.url));
8327
+
7672
8328
  // src/brain/index.ts
7673
8329
  function checkBrainRules(store, opts) {
7674
8330
  if (!store) return null;
@@ -7715,6 +8371,10 @@ function checkLayer2Context(pattern, actionHistory, actionDetail) {
7715
8371
  return true;
7716
8372
  }
7717
8373
 
8374
+ // src/engine/coordinator.ts
8375
+ init_asset_dir();
8376
+ init_runtime_config_bridge();
8377
+
7718
8378
  // src/engine/formal-engine/index.ts
7719
8379
  init_esm_shims();
7720
8380
 
@@ -8684,20 +9344,69 @@ function matchesAny(cmd, patterns) {
8684
9344
  }
8685
9345
  var _brainStore = null;
8686
9346
  var _brainExtractor = null;
9347
+ var _brainSkillManager = null;
9348
+ var _brainCurator = null;
8687
9349
  var _brainInitialized = false;
8688
- function getBrainStore() {
9350
+ var _brainLastMaintenance = 0;
9351
+ var BRAIN_MAINT_INTERVAL_MS = 3e5;
9352
+ function getBrainStore(osType) {
8689
9353
  if (!isBrainEnabled()) return null;
8690
9354
  if (!_brainInitialized) {
8691
9355
  _brainStore = new BrainRuleStore();
8692
- _brainExtractor = new PatternExtractor(_brainStore, { review: createBrainReviewer() });
9356
+ const skillsDir = resolveAssetDir(__dirname, "skills");
9357
+ _brainSkillManager = skillsDir ? new BrainSkillManager(skillsDir) : null;
9358
+ _brainExtractor = new PatternExtractor(_brainStore, {
9359
+ review: createBrainReviewer(),
9360
+ // 内置规则去重集(对齐 Python coordinator._ensure_brain 收集的 builtin_patterns):
9361
+ // 危险命令正则 + 敏感路径(deny_* 或 credential)。
9362
+ // 历史 bug:TS 这里只传 review,不传 builtinPatterns → 提取器拿不到内置规则,
9363
+ // `isCoveredByBuiltin` 恒 False → 把 L1/L2 本来就能拦的案例重复沉淀成 Brain 规则。
9364
+ builtinPatterns: collectBrainBuiltinPatterns(osType),
9365
+ skillManager: _brainSkillManager
9366
+ });
9367
+ if (_brainSkillManager) {
9368
+ _brainCurator = new BrainCurator(_brainStore, _brainSkillManager);
9369
+ }
8693
9370
  _brainInitialized = true;
8694
9371
  }
9372
+ const now = Date.now();
9373
+ if (_brainCurator && now - _brainLastMaintenance >= BRAIN_MAINT_INTERVAL_MS) {
9374
+ try {
9375
+ _brainCurator.runMaintenance({ autoApprove: true });
9376
+ } catch {
9377
+ }
9378
+ _brainLastMaintenance = now;
9379
+ }
8695
9380
  return _brainStore;
8696
9381
  }
9382
+ function collectBrainBuiltinPatterns(osType) {
9383
+ const bridge = runtimeConfigBridge.current;
9384
+ if (!bridge) return [];
9385
+ const out = [];
9386
+ try {
9387
+ for (const cp of bridge.effectiveDangerousPatterns(osType)) {
9388
+ if (cp?.pattern?.source) out.push(cp.pattern.source);
9389
+ }
9390
+ } catch {
9391
+ }
9392
+ try {
9393
+ for (const sp of bridge.effectiveSensitivePaths(osType)) {
9394
+ const denyMode = String(sp?.denyMode || "");
9395
+ if (["deny_both", "deny_write", "deny_read"].includes(denyMode) || sp?.credential) {
9396
+ if (sp?.path) out.push(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
9397
+ }
9398
+ }
9399
+ } catch {
9400
+ }
9401
+ return out.filter(Boolean);
9402
+ }
8697
9403
  function _resetBrainForTest() {
8698
9404
  _brainStore = null;
8699
9405
  _brainExtractor = null;
9406
+ _brainSkillManager = null;
9407
+ _brainCurator = null;
8700
9408
  _brainInitialized = false;
9409
+ _brainLastMaintenance = 0;
8701
9410
  }
8702
9411
  var formalEngine = new FormalEngine();
8703
9412
  async function runAnalysis(input) {
@@ -8750,7 +9459,7 @@ async function runAnalysis(input) {
8750
9459
  }
8751
9460
  }
8752
9461
  }
8753
- const brainStore = getBrainStore();
9462
+ const brainStore = getBrainStore(input.osType);
8754
9463
  if (brainStore) {
8755
9464
  const brainL1 = checkBrainRules(brainStore, {
8756
9465
  layer: 1,