@kenhkl/agent-moss 0.16.7 → 0.16.12

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.
@@ -406,58 +406,57 @@ function parseDirEnv(envName) {
406
406
  if (!raw) return [];
407
407
  return raw.split(/[,;]/).map((s) => s.trim()).filter(Boolean).map((s) => resolve(s));
408
408
  }
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
- }
409
+ function rulesDirs() {
410
+ const dirs = [];
411
+ for (const env of RULES_DIR_ENVS) dirs.push(...parseDirEnv(env));
412
+ dirs.push(resolve(dataRootDir(), "rules"));
413
+ return [...new Set(dirs)];
424
414
  }
425
415
  function findRulesFile(name) {
426
- for (const dir of overrideRulesDirs()) {
427
- const p = resolve(dir, name);
428
- if (existsSync(p)) return p;
429
- }
430
416
  for (const dir of RULES_DIR_CANDS) {
431
417
  const p = resolve(dir, name);
432
418
  if (existsSync(p)) return p;
433
419
  }
434
420
  return null;
435
421
  }
436
- function isOverridePath(name, p) {
437
- return overrideRulesDirs().some((dir) => resolve(dir, name) === p);
438
- }
439
422
  function loadJson(name) {
440
423
  const p = findRulesFile(name);
441
424
  if (!p) return {};
442
- const isOverride = isOverridePath(name, p);
443
- let data;
444
425
  try {
445
- data = JSON.parse(readFileSync2(p, "utf-8")) || {};
426
+ return JSON.parse(readFileSync2(p, "utf-8")) || {};
446
427
  } catch {
447
- if (isOverride) warnOverride(name, p, {});
448
428
  return {};
449
429
  }
450
- if (isOverride) warnOverride(name, p, data);
451
- return data;
452
430
  }
453
431
  function resolveScenarioRulesFile(scenarioId) {
454
432
  const rel = join2("scenarios", scenarioId, "rules.json");
455
- for (const dir of overrideRulesDirs()) {
433
+ for (const dir of rulesDirs()) {
456
434
  const p = resolve(dir, rel);
457
- if (existsSync(p)) return p;
435
+ if (!existsSync(p)) continue;
436
+ let reason = "";
437
+ try {
438
+ reason = invalidScenarioRulesReason(JSON.parse(readFileSync2(p, "utf-8")) || {});
439
+ } catch (err) {
440
+ reason = `\u8BFB\u53D6/\u89E3\u6790\u5931\u8D25: ${String(err)}`;
441
+ }
442
+ if (reason) {
443
+ console.error(
444
+ `[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}
445
+ \u539F\u56E0: ${reason}`
446
+ );
447
+ return null;
448
+ }
449
+ 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`);
450
+ return p;
458
451
  }
459
452
  return null;
460
453
  }
454
+ function invalidScenarioRulesReason(data) {
455
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
456
+ return `\u9876\u5C42\u5FC5\u987B\u662F JSON \u5BF9\u8C61\uFF0C\u5B9E\u9645\u662F ${Array.isArray(data) ? "array" : typeof data}`;
457
+ }
458
+ 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");
459
+ }
461
460
  function loadL1() {
462
461
  return loadJson("l1_rules.json");
463
462
  }
@@ -465,10 +464,40 @@ function loadL2() {
465
464
  return loadJson("l2_rules.json");
466
465
  }
467
466
  function loadL3() {
468
- return loadJson("l3_rules.json");
467
+ const data = { ...loadJson("l3_rules.json") };
468
+ try {
469
+ const ov = loadOverrideRules().skills;
470
+ const ex = loadExtraRules().skills || {};
471
+ if (ov && typeof ov === "object") {
472
+ for (const [cat, skills] of Object.entries(ov)) data[cat] = { ...skills };
473
+ }
474
+ if (ex && typeof ex === "object") {
475
+ for (const [cat, skills] of Object.entries(ex)) data[cat] = { ...data[cat] || {}, ...skills };
476
+ }
477
+ } catch {
478
+ }
479
+ return data;
480
+ }
481
+ function loadMCRaw() {
482
+ const mc = { ...loadJson("mc_rules.json") };
483
+ let rules = [...mc.rules || []];
484
+ try {
485
+ const ov = loadOverrideRules().ltl_rules;
486
+ const ex = loadExtraRules().ltl_rules || [];
487
+ if (Array.isArray(ov)) rules = [...ov];
488
+ if (Array.isArray(ex) && ex.length > 0) {
489
+ const byId = /* @__PURE__ */ new Map();
490
+ for (const r of rules) if (r && r.id) byId.set(r.id, r);
491
+ for (const r of ex) if (r && r.id) byId.set(r.id, r);
492
+ rules = [...byId.values()];
493
+ }
494
+ } catch {
495
+ }
496
+ mc.rules = rules;
497
+ return mc;
469
498
  }
470
499
  function loadMC() {
471
- const mc = loadJson("mc_rules.json");
500
+ const mc = loadMCRaw();
472
501
  const rules = mc.rules || [];
473
502
  try {
474
503
  const rt = runtimeConfigBridge.current;
@@ -488,140 +517,278 @@ function loadMC() {
488
517
  return mc;
489
518
  }
490
519
  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;
520
+ const data = { ...loadJson("tool_registry.json") };
521
+ let tools = { ...data.tools || {} };
499
522
  try {
500
- entries = readdirSync(dir, { withFileTypes: true }).map((d) => d.name);
523
+ const ov = loadOverrideRules().tools;
524
+ const ex = loadExtraRules().tools || {};
525
+ if (ov && typeof ov === "object") tools = { ...ov };
526
+ if (ex && typeof ex === "object") tools = { ...tools, ...ex };
501
527
  } catch {
502
- return out;
503
528
  }
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;
529
+ data.tools = tools;
530
+ return data;
531
+ }
532
+ function scanStructEntry(cat, value, path5) {
533
+ if (cat === "skills") {
534
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
535
+ 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`);
536
+ return null;
537
+ }
538
+ const out = {};
539
+ for (const [skCat, skills] of Object.entries(value)) {
540
+ if (!skills || typeof skills !== "object" || Array.isArray(skills)) {
541
+ 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`);
542
+ continue;
543
+ }
544
+ const keep = {};
545
+ for (const [name, kws] of Object.entries(skills)) {
546
+ if (Array.isArray(kws)) keep[name] = kws;
547
+ 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`);
548
+ }
549
+ if (Object.keys(keep).length > 0) out[skCat] = keep;
511
550
  }
512
- if (isDir) out.push(...listJsonFilesDeep(p));
513
- else if (name.toLowerCase().endsWith(".json")) out.push(p);
551
+ return Object.keys(out).length > 0 ? out : null;
514
552
  }
515
- return out;
553
+ if (cat === "ltl_rules") {
554
+ if (!Array.isArray(value)) {
555
+ console.error(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 ltl_rules \u5E94\u4E3A\u5BF9\u8C61\u6570\u7EC4 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u3002`);
556
+ return null;
557
+ }
558
+ const keep = value.filter((r) => r && typeof r === "object" && r.id);
559
+ if (keep.length !== value.length) {
560
+ 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`);
561
+ }
562
+ return keep.length > 0 ? keep : null;
563
+ }
564
+ if (cat === "tools") {
565
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
566
+ 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`);
567
+ return null;
568
+ }
569
+ const keep = {};
570
+ for (const [k, v] of Object.entries(value)) {
571
+ if (v && typeof v === "object" && !Array.isArray(v)) keep[k] = v;
572
+ else console.warn(`[AgentMoss] \u89C4\u5219\u6587\u4EF6 ${path5} \u7684 tools.${k} \u4E0D\u662F\u5BF9\u8C61 \u2014\u2014 \u5DF2\u8DF3\u8FC7\u8BE5\u6761\u3002`);
573
+ }
574
+ return Object.keys(keep).length > 0 ? keep : null;
575
+ }
576
+ return null;
516
577
  }
517
- function scanExtraFiles(files) {
518
- const data = {};
519
- const loaded = [];
520
- const seenIds = /* @__PURE__ */ new Map();
578
+ function scanRulesFile(path5, kind) {
579
+ const out = {};
580
+ const label = kind === "override" ? "\u8986\u76D6" : "\u8FFD\u52A0";
581
+ let parsed;
582
+ try {
583
+ parsed = JSON.parse(readFileSync2(path5, "utf-8"));
584
+ } catch (e) {
585
+ 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}`);
586
+ return out;
587
+ }
521
588
  const classifyFlat = (r) => {
522
589
  for (const [keys, cat] of EXTRA_FLAT_FIELD) {
523
590
  if (r && typeof r === "object" && keys.every((k) => r[k])) return cat;
524
591
  }
525
592
  return null;
526
593
  };
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`);
594
+ let pairs = [];
595
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
596
+ pairs = Object.entries(parsed);
597
+ } else if (Array.isArray(parsed)) {
598
+ let flatBad = 0;
599
+ for (const r of parsed) {
600
+ const cat = classifyFlat(r);
601
+ if (cat === null) {
602
+ flatBad += 1;
603
+ continue;
604
+ }
605
+ pairs.push([cat, [r]]);
606
+ }
607
+ if (flatBad > 0) {
608
+ console.warn(
609
+ `[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`
610
+ );
611
+ }
612
+ } else {
613
+ 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`);
614
+ return out;
615
+ }
616
+ for (const [cat, rules] of pairs) {
617
+ if (String(cat).startsWith("_")) continue;
618
+ if (STRUCT_KEYS.includes(cat)) {
619
+ const got = scanStructEntry(cat, rules, path5);
620
+ if (got !== null) out[cat] = got;
533
621
  continue;
534
622
  }
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 {
623
+ const required = EXTRA_CATEGORIES[cat];
624
+ if (!required) {
554
625
  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`
626
+ `[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`
627
+ );
628
+ continue;
629
+ }
630
+ if (rules && typeof rules === "object" && !Array.isArray(rules)) {
631
+ console.error(
632
+ `[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
633
+ \u6B63\u786E\u5199\u6CD5: {"${cat}": [{"pattern": "...", "risk_level": "high", ...}]}
634
+ \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
635
  );
557
636
  continue;
558
637
  }
559
- let nLoaded = 0;
560
- for (const [cat, rules] of pairs) {
561
- const required = EXTRA_CATEGORIES[cat];
562
- if (!required) {
638
+ if (!Array.isArray(rules)) {
639
+ 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`);
640
+ continue;
641
+ }
642
+ const items = [];
643
+ for (const r of rules) {
644
+ if (!r || typeof r !== "object" || !required.every((k) => r[k])) {
563
645
  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`
646
+ `[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
647
  );
566
648
  continue;
567
649
  }
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`);
650
+ items.push(r);
651
+ }
652
+ if (items.length > 0) out[cat] = (out[cat] || []).concat(items);
653
+ }
654
+ if (Object.keys(out).length > 0) {
655
+ console.info(
656
+ `[AgentMoss] \u5DF2\u52A0\u8F7D${label}\u89C4\u5219\u6587\u4EF6 ${path5}\uFF1A` + Object.entries(out).map(([k, v]) => `${k}\xD7${v.length}`).join(", ")
657
+ );
658
+ }
659
+ return out;
660
+ }
661
+ function scanRuleDirs() {
662
+ const override = {};
663
+ const extra = {};
664
+ const loaded = [];
665
+ const ignored = [];
666
+ for (const d of rulesDirs()) {
667
+ if (!existsSync(d)) continue;
668
+ let entries = [];
669
+ try {
670
+ entries = readdirSync(d).sort();
671
+ } catch {
672
+ continue;
673
+ }
674
+ for (const name of entries) {
675
+ const f = resolve(d, name);
676
+ try {
677
+ if (statSync(f).isDirectory()) continue;
678
+ } catch {
570
679
  continue;
571
680
  }
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;
681
+ if (name === OVERRIDE_FILENAME) {
682
+ const got = scanRulesFile(f, "override");
683
+ if (Object.keys(got).length > 0) loaded.push(f);
684
+ for (const [cat, items] of Object.entries(got)) {
685
+ if (Array.isArray(items)) {
686
+ override[cat] = (override[cat] || []).concat(items);
687
+ } else {
688
+ override[cat] = { ...override[cat] || {}, ...items };
689
+ }
578
690
  }
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;
691
+ } else if (name === EXTRA_FILENAME) {
692
+ const got = scanRulesFile(f, "extra");
693
+ if (Object.values(got).some((v) => v.length > 0)) loaded.push(f);
694
+ for (const [cat, items] of Object.entries(got)) {
695
+ if (Array.isArray(items)) {
696
+ extra[cat] = (extra[cat] || []).concat(items);
697
+ } else {
698
+ extra[cat] = { ...extra[cat] || {}, ...items };
586
699
  }
587
- seenIds.set(rid, p);
588
700
  }
589
- (data[cat] = data[cat] || []).push(r);
590
- nLoaded += 1;
701
+ } else if (name.toLowerCase().endsWith(".json")) {
702
+ ignored.push(f);
591
703
  }
592
704
  }
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`);
705
+ }
706
+ if (Object.keys(override).length > 0 && identicalToBuiltin(override)) {
707
+ const key = "override-identical::" + Object.entries(override).map(([c, v]) => `${c}=${v.length}`).sort().join("|");
708
+ if (!warnedIgnored.has(key)) {
709
+ warnedIgnored.add(key);
710
+ console.warn(
711
+ `[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
712
+ \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
713
+ \u53EA\u60F3\u8FFD\u52A0\u89C4\u5219\uFF1A\u5220\u6389 ${OVERRIDE_FILENAME}\uFF0C\u6539\u7528 ${EXTRA_FILENAME}\u3002`
714
+ );
596
715
  }
597
716
  }
598
- return { data, loaded };
717
+ if (Object.keys(override).length > 0) {
718
+ const key = "override-summary::" + Object.entries(override).map(([c, v]) => `${c}=${v.length}`).sort().join("|");
719
+ if (!warnedIgnored.has(key)) {
720
+ warnedIgnored.add(key);
721
+ const l1 = loadL1();
722
+ const l2 = loadL2();
723
+ const builtinCounts = {
724
+ dangerous_commands: (l1.dangerous_commands || []).length,
725
+ injection_detection: (l1.injection_detection || []).length,
726
+ sensitive_path_access: (l1.sensitive_path_access || []).length,
727
+ intent_consistency: (l2.intent_consistency || []).length,
728
+ password_modify_consent: (l2.password_modify_consent || []).length,
729
+ user_deletion_consent: (l2.user_deletion_consent || []).length
730
+ };
731
+ 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`);
732
+ for (const [cat, items] of Object.entries(override).sort()) {
733
+ console.warn(` \u7C7B\u522B ${cat}\uFF1A\u5185\u7F6E ${builtinCounts[cat] ?? 0} \u6761 \u2192 \u4F60\u7684 ${items.length} \u6761`);
734
+ }
735
+ 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`);
736
+ }
737
+ }
738
+ if (ignored.length > 0) {
739
+ const key = "ignored::" + ignored.join("|");
740
+ if (!warnedIgnored.has(key)) {
741
+ warnedIgnored.add(key);
742
+ console.warn(
743
+ `[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(", ")}
744
+ \u8981\u8986\u76D6\u5185\u7F6E\u89C4\u5219 \u2192 \u5185\u5BB9\u653E\u8FDB ${OVERRIDE_FILENAME}\uFF1B\u8981\u8FFD\u52A0 \u2192 \u653E\u8FDB ${EXTRA_FILENAME}\u3002`
745
+ );
746
+ }
747
+ }
748
+ return { override, extra, loaded, ignored };
599
749
  }
600
- function loadExtraRules() {
601
- const raw = (process.env.AGENT_MOSS_EXTRA_RULES_DIR || "").trim();
602
- const dirs = extraRulesDirs();
603
- let files = [];
750
+ function rulesCached() {
751
+ const dirs = rulesDirs();
752
+ const sig = RULES_DIR_ENVS.map((e) => `${e}=${process.env[e] || ""}`).join("|") + "|" + dirs.join("|");
753
+ const files = [];
604
754
  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`);
755
+ if (!existsSync(d)) continue;
756
+ try {
757
+ for (const name of readdirSync(d).sort()) {
758
+ if (name.toLowerCase().endsWith(".json")) files.push(resolve(d, name));
759
+ }
760
+ } catch {
761
+ }
607
762
  }
608
- files = files.sort();
609
- const fp = files.map((p) => {
763
+ const fp = JSON.stringify(files.map((p) => {
610
764
  try {
611
765
  const st = statSync(p);
612
766
  return [p, st.mtimeMs, st.size];
613
767
  } catch {
614
768
  return [p, -1, -1];
615
769
  }
616
- });
617
- if (extraCache && extraCache.raw === raw && JSON.stringify(extraCache.fp) === JSON.stringify(fp)) {
618
- return extraCache.data;
770
+ }));
771
+ if (_RULES_CACHE && _RULES_CACHE.sig === sig && _RULES_CACHE.fp === fp) return _RULES_CACHE;
772
+ _RULES_CACHE = { sig, fp, ...scanRuleDirs() };
773
+ return _RULES_CACHE;
774
+ }
775
+ function identicalToBuiltin(override) {
776
+ const l1 = loadL1();
777
+ const l2 = loadL2();
778
+ for (const [cat, items] of Object.entries(override)) {
779
+ const builtin = l1[cat] ?? l2[cat];
780
+ if (!Array.isArray(builtin) || builtin.length !== items.length) return false;
781
+ if (JSON.stringify(builtin) !== JSON.stringify(items)) return false;
619
782
  }
620
- const { data, loaded } = scanExtraFiles(files);
621
- extraCache = { raw, fp, data, loaded };
622
- return data;
783
+ return Object.keys(override).length > 0;
784
+ }
785
+ function loadOverrideRules() {
786
+ return rulesCached().override;
623
787
  }
624
- var __filename2, __dirname2, EXPECTED_KEYS, warnedOverride, RULES_DIR_CANDS, EXTRA_CATEGORIES, EXTRA_FLAT_FIELD, extraCache;
788
+ function loadExtraRules() {
789
+ return rulesCached().extra;
790
+ }
791
+ var __filename2, __dirname2, OVERRIDE_FILENAME, EXTRA_FILENAME, RULES_DIR_ENVS, RULES_DIR_CANDS, SCENARIO_LIST_KEYS, EXTRA_CATEGORIES, STRUCT_KEYS, EXTRA_FLAT_FIELD, _RULES_CACHE, warnedIgnored;
625
792
  var init_rules_loader = __esm({
626
793
  "src/infra/rules-loader.ts"() {
627
794
  "use strict";
@@ -630,20 +797,13 @@ var init_rules_loader = __esm({
630
797
  init_runtime_config_bridge();
631
798
  __filename2 = fileURLToPath2(import.meta.url);
632
799
  __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();
800
+ OVERRIDE_FILENAME = "override.json";
801
+ EXTRA_FILENAME = "extra.json";
802
+ RULES_DIR_ENVS = ["AGENT_MOSS_EXTRA_RULES_DIR", "AGENT_MOSS_OVERRIDE_RULES_DIR"];
641
803
  RULES_DIR_CANDS = (() => {
642
804
  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"),
805
+ // 注:`<data_root>/rules/` 不再是"按内置文件名覆盖"的查找位 —— 2026-10 起
806
+ // 规则目录只认 override.json / extra.json(见 rulesDirs())。
647
807
  // dist: __dirname=dist/ → dist/rules;__dirname=dist/engine/ → dist/engine/../rules=dist/rules
648
808
  resolve(__dirname2, "../rules"),
649
809
  resolve(__dirname2, "rules"),
@@ -656,6 +816,7 @@ var init_rules_loader = __esm({
656
816
  ];
657
817
  return cands.filter((c) => existsSync(c));
658
818
  })();
819
+ SCENARIO_LIST_KEYS = ["extra_command_patterns", "extra_injection_keywords", "extra_sensitive_paths"];
659
820
  EXTRA_CATEGORIES = {
660
821
  dangerous_commands: ["pattern"],
661
822
  injection_detection: ["keyword", "lang"],
@@ -664,6 +825,7 @@ var init_rules_loader = __esm({
664
825
  password_modify_consent: ["pattern"],
665
826
  user_deletion_consent: ["pattern"]
666
827
  };
828
+ STRUCT_KEYS = ["skills", "ltl_rules", "tools"];
667
829
  EXTRA_FLAT_FIELD = [
668
830
  [["path"], "sensitive_path_access"],
669
831
  [["keyword", "lang"], "injection_detection"],
@@ -671,7 +833,8 @@ var init_rules_loader = __esm({
671
833
  [["pattern", "risk_level"], "dangerous_commands"],
672
834
  [["pattern", "risk_type"], "dangerous_commands"]
673
835
  ];
674
- extraCache = null;
836
+ _RULES_CACHE = null;
837
+ warnedIgnored = /* @__PURE__ */ new Set();
675
838
  }
676
839
  });
677
840
 
@@ -2029,18 +2192,34 @@ function _mergeUserCustom(primary, runtime, layers, key) {
2029
2192
  const extra = _userCustomRules(runtime, layers, key);
2030
2193
  return extra.length > 0 ? [...primary, ...extra] : [...primary];
2031
2194
  }
2032
- function _mergeExtraDirRules(primary, category) {
2195
+ function _dedup(rules, category) {
2196
+ const keys = _DEDUP_KEYS[category];
2197
+ if (!keys) return rules;
2198
+ const seen = /* @__PURE__ */ new Set();
2199
+ const out = [];
2200
+ for (const r of rules) {
2201
+ const k = keys.map((x) => String(r?.[x] ?? "")).join("\0");
2202
+ if (seen.has(k)) continue;
2203
+ seen.add(k);
2204
+ out.push(r);
2205
+ }
2206
+ return out;
2207
+ }
2208
+ function _mergeRulesFromDirs(primary, category) {
2209
+ let base = [...primary];
2033
2210
  try {
2211
+ const override = loadOverrideRules()[category];
2212
+ if (override !== void 0 && override !== null) base = [...override];
2034
2213
  const extra = loadExtraRules()[category] || [];
2035
- return extra.length > 0 ? [...primary, ...extra] : [...primary];
2214
+ return _dedup([...base, ...extra], category);
2036
2215
  } catch {
2037
- return [...primary];
2216
+ return base;
2038
2217
  }
2039
2218
  }
2040
2219
  function getEnabledL1CommandPatterns(runtime, osType) {
2041
2220
  const cat = runtime.L1_rules?.dangerous_commands || { category_enabled: true, rules: [] };
2042
2221
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2043
- rules = _mergeExtraDirRules(rules, "dangerous_commands");
2222
+ rules = _mergeRulesFromDirs(rules, "dangerous_commands");
2044
2223
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "pattern");
2045
2224
  if (osType) rules = _filterRulesByOs(rules, osType);
2046
2225
  return rules.map((r) => ({
@@ -2056,7 +2235,7 @@ function getEnabledL2SensitivePaths(runtime, osType) {
2056
2235
  cat = runtime.L2_rules?.sensitive_path_access || { category_enabled: true, rules: [] };
2057
2236
  }
2058
2237
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2059
- rules = _mergeExtraDirRules(rules, "sensitive_path_access");
2238
+ rules = _mergeRulesFromDirs(rules, "sensitive_path_access");
2060
2239
  rules = _mergeUserCustom(rules, runtime, ["L1_rules", "L2_rules"], "path");
2061
2240
  if (osType) rules = _filterRulesByOs(rules, osType);
2062
2241
  return rules.map((r) => {
@@ -2441,25 +2620,11 @@ function generateSourceDefaults() {
2441
2620
  };
2442
2621
  }
2443
2622
  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
2623
  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
- }
2624
+ try {
2625
+ mc = loadMCRaw();
2626
+ } catch {
2627
+ mc = {};
2463
2628
  }
2464
2629
  const rules = mc.rules || [];
2465
2630
  return {
@@ -2688,7 +2853,7 @@ function _migrateCategoriesAcrossLayers(cfg, srcLayer, dstLayer, catNames) {
2688
2853
  function getEnabledL1InjectionKeywords(runtime, osType) {
2689
2854
  const cat = runtime.L1_rules?.injection_detection || { category_enabled: true, rules: [] };
2690
2855
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2691
- rules = _mergeExtraDirRules(rules, "injection_detection");
2856
+ rules = _mergeRulesFromDirs(rules, "injection_detection");
2692
2857
  rules = _mergeUserCustom(rules, runtime, "L1_rules", "keyword");
2693
2858
  if (osType) rules = _filterRulesByOs(rules, osType);
2694
2859
  return rules.map((r) => ({
@@ -2700,7 +2865,7 @@ function getEnabledL1InjectionKeywords(runtime, osType) {
2700
2865
  function getEnabledL2IntentPatterns(runtime, osType) {
2701
2866
  const cat = runtime.L2_rules?.intent_consistency || { category_enabled: true, rules: [] };
2702
2867
  let rules = filterEnabledRules(cat.rules || [], cat.category_enabled);
2703
- rules = _mergeExtraDirRules(rules, "intent_consistency");
2868
+ rules = _mergeRulesFromDirs(rules, "intent_consistency");
2704
2869
  rules = _mergeUserCustom(rules, runtime, "L2_rules", "intent_keywords");
2705
2870
  if (osType) rules = _filterRulesByOs(rules, osType);
2706
2871
  return rules.map((r) => ({
@@ -2712,7 +2877,7 @@ function getEnabledL2IntentPatterns(runtime, osType) {
2712
2877
  function getEnabledL2PasswordPatterns(runtime) {
2713
2878
  const cat = runtime.L2_rules?.password_modify_consent || { category_enabled: true, rules: [] };
2714
2879
  const rules = _mergeUserCustom(
2715
- _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2880
+ _mergeRulesFromDirs(filterEnabledRules(cat.rules || [], cat.category_enabled), "password_modify_consent"),
2716
2881
  runtime,
2717
2882
  "L2_rules",
2718
2883
  "pattern"
@@ -2722,7 +2887,7 @@ function getEnabledL2PasswordPatterns(runtime) {
2722
2887
  function getEnabledL2UserDeletionPatterns(runtime) {
2723
2888
  const cat = runtime.L2_rules?.user_deletion_consent || { category_enabled: true, rules: [] };
2724
2889
  const rules = _mergeUserCustom(
2725
- _mergeExtraDirRules(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2890
+ _mergeRulesFromDirs(filterEnabledRules(cat.rules || [], cat.category_enabled), "user_deletion_consent"),
2726
2891
  runtime,
2727
2892
  "L2_rules",
2728
2893
  "pattern"
@@ -2759,7 +2924,7 @@ function getEnabledL1CommandPatternsOrDefault(runtime, osType) {
2759
2924
  return getEnabledL1CommandPatterns(runtime, osType);
2760
2925
  }
2761
2926
  return _mergeUserCustom(
2762
- _mergeExtraDirRules(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2927
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l1CommandPatternsToRules(), osType), "dangerous_commands"),
2763
2928
  runtime,
2764
2929
  "L1_rules",
2765
2930
  "pattern"
@@ -2771,7 +2936,7 @@ function getEnabledL1InjectionKeywordsOrDefault(runtime, osType) {
2771
2936
  return getEnabledL1InjectionKeywords(runtime, osType);
2772
2937
  }
2773
2938
  return _mergeUserCustom(
2774
- _mergeExtraDirRules(_l1InjectionKeywordsToRules(), "injection_detection"),
2939
+ _mergeRulesFromDirs(_l1InjectionKeywordsToRules(), "injection_detection"),
2775
2940
  runtime,
2776
2941
  "L1_rules",
2777
2942
  "keyword"
@@ -2783,7 +2948,7 @@ function getEnabledL2SensitivePathsOrDefault(runtime, osType) {
2783
2948
  return getEnabledL2SensitivePaths(runtime, osType);
2784
2949
  }
2785
2950
  return _mergeUserCustom(
2786
- _mergeExtraDirRules(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2951
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l2SensitivePathsToRules(), osType), "sensitive_path_access"),
2787
2952
  runtime,
2788
2953
  ["L1_rules", "L2_rules"],
2789
2954
  "path"
@@ -2795,7 +2960,7 @@ function getEnabledL2IntentPatternsOrDefault(runtime, osType) {
2795
2960
  return getEnabledL2IntentPatterns(runtime, osType);
2796
2961
  }
2797
2962
  return _mergeUserCustom(
2798
- _mergeExtraDirRules(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2963
+ _mergeRulesFromDirs(_filterDefaultsByOs(_l2IntentPatternsToRules(), osType), "intent_consistency"),
2799
2964
  runtime,
2800
2965
  "L2_rules",
2801
2966
  "intent_keywords"
@@ -3186,7 +3351,7 @@ function updateScenarioCategoryEnabled(scenarioId, category, enabled) {
3186
3351
  saveRuntimeConfig(runtime);
3187
3352
  return runtime;
3188
3353
  }
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;
3354
+ 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
3355
  var init_runtime_config = __esm({
3191
3356
  "src/infra/runtime-config.ts"() {
3192
3357
  "use strict";
@@ -3219,6 +3384,14 @@ var init_runtime_config = __esm({
3219
3384
  deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
3220
3385
  };
3221
3386
  runtimeConfigCache = null;
3387
+ _DEDUP_KEYS = {
3388
+ dangerous_commands: ["pattern"],
3389
+ password_modify_consent: ["pattern"],
3390
+ user_deletion_consent: ["pattern"],
3391
+ sensitive_path_access: ["path"],
3392
+ injection_detection: ["keyword", "lang"],
3393
+ intent_consistency: ["intent_keywords", "reason"]
3394
+ };
3222
3395
  hasRuntimeConfigCache = /* @__PURE__ */ new Map();
3223
3396
  _AGENTS_DIR_PROD = join3(__dirname4, "agents");
3224
3397
  _AGENTS_DIR_DEV = join3(__dirname4, "..", "..", "..", "agent_moss", "agents");
@@ -3244,6 +3417,8 @@ var init_runtime_config = __esm({
3244
3417
  var config_exports = {};
3245
3418
  __export(config_exports, {
3246
3419
  detectProvider: () => detectProvider,
3420
+ getAgentMossSetting: () => getAgentMossSetting,
3421
+ getCacheOptions: () => getCacheOptions,
3247
3422
  getCustomRules: () => getCustomRules,
3248
3423
  getLLMConfig: () => getLLMConfig,
3249
3424
  getLLMFailMode: () => getLLMFailMode,
@@ -3255,6 +3430,8 @@ __export(config_exports, {
3255
3430
  isHeuristicEnabled: () => isHeuristicEnabled,
3256
3431
  isLLMAvailable: () => isLLMAvailable,
3257
3432
  isLogicRulesEnabled: () => isLogicRulesEnabled,
3433
+ isPolicyGenEnabled: () => isPolicyGenEnabled,
3434
+ isPolicySaveEnabled: () => isPolicySaveEnabled,
3258
3435
  probeLLMConnection: () => probeLLMConnection,
3259
3436
  updateLLMConfig: () => updateLLMConfig,
3260
3437
  userRuleVariantPattern: () => userRuleVariantPattern,
@@ -3277,8 +3454,10 @@ function resolveConfigPath() {
3277
3454
  const userConfig = path2.join(getConfigDir(), "config.json");
3278
3455
  if (fs2.existsSync(userConfig)) return userConfig;
3279
3456
  const defaultCandidates = [
3280
- path2.join(__dirname, "config.json.example"),
3281
- path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example")
3457
+ path2.join(__configModuleDir, "config.json.example"),
3458
+ path2.join(__configModuleDir, "..", "config.json.example"),
3459
+ path2.join(__configModuleDir, "..", "..", "agent_moss", "infra", "config.json.example"),
3460
+ path2.join(__configModuleDir, "..", "..", "..", "agent_moss", "infra", "config.json.example")
3282
3461
  ];
3283
3462
  const defaultConfig = defaultCandidates.find((p) => fs2.existsSync(p));
3284
3463
  if (defaultConfig) {
@@ -3287,8 +3466,8 @@ function resolveConfigPath() {
3287
3466
  const pkgConfig = loadConfigFile(defaultConfig);
3288
3467
  const safeConfig = pkgConfig ? { ...pkgConfig, llm: { ...pkgConfig.llm, api_key: "" } } : {};
3289
3468
  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`);
3469
+ console.error(`[AgentMoss] \u5DF2\u521B\u5EFA\u7528\u6237\u914D\u7F6E\u6587\u4EF6: ${userConfig}`);
3470
+ console.error(`[AgentMoss] \u8BF7\u7F16\u8F91\u8BE5\u6587\u4EF6\u914D\u7F6E\u4F60\u7684 LLM API Key`);
3292
3471
  }
3293
3472
  return userConfig;
3294
3473
  }
@@ -3460,7 +3639,32 @@ function layerEnabledOrDefault(key) {
3460
3639
  }
3461
3640
  }
3462
3641
  function isBrainEnabled() {
3463
- return process.env.AGENT_MOSS_BRAIN_ENABLED === "1";
3642
+ return getAgentMossSetting("AGENT_MOSS_BRAIN_ENABLED", "") === "1";
3643
+ }
3644
+ function getAgentMossSetting(key, def = "") {
3645
+ const envValue = process.env[key];
3646
+ if (envValue !== void 0) return envValue;
3647
+ const cfg = loadConfigFile(resolveConfigPath());
3648
+ if (cfg && key in cfg) return String(cfg[key]);
3649
+ return def;
3650
+ }
3651
+ function isPolicyGenEnabled() {
3652
+ return getAgentMossSetting("AGENT_MOSS_ENABLE_POLICY_GEN", "") === "1";
3653
+ }
3654
+ function getCacheOptions() {
3655
+ let enabled = true;
3656
+ let maxSize = 1e3;
3657
+ const cfg = loadConfigFile(resolveConfigPath());
3658
+ const cache3 = cfg?.cache;
3659
+ if (cache3 && typeof cache3 === "object") {
3660
+ if (typeof cache3.enabled === "boolean") enabled = cache3.enabled;
3661
+ if (typeof cache3.max_size === "number" && cache3.max_size > 0) maxSize = cache3.max_size;
3662
+ }
3663
+ if (process.env.AGENT_MOSS_DISABLE_CACHE === "1") enabled = false;
3664
+ return { enabled, maxSize };
3665
+ }
3666
+ function isPolicySaveEnabled() {
3667
+ return getAgentMossSetting("AGENT_MOSS_SAVE_POLICY", "1") !== "0";
3464
3668
  }
3465
3669
  function isLogicRulesEnabled() {
3466
3670
  if (process.env.AGENT_MOSS_DISABLE_LOGIC_RULES === "1") return false;
@@ -3469,13 +3673,20 @@ function isLogicRulesEnabled() {
3469
3673
  function isFormalObserveMode() {
3470
3674
  return process.env.AGENT_MOSS_FORMAL_OBSERVE === "1";
3471
3675
  }
3472
- var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
3676
+ var __configModuleDir, timeoutConfig, retryConfig, llmConfig, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
3473
3677
  var init_config = __esm({
3474
3678
  "src/config.ts"() {
3475
3679
  "use strict";
3476
3680
  init_esm_shims();
3477
3681
  init_paths();
3478
3682
  init_runtime_config();
3683
+ __configModuleDir = (() => {
3684
+ try {
3685
+ return path2.dirname(fileURLToPath5(import.meta.url));
3686
+ } catch {
3687
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
3688
+ }
3689
+ })();
3479
3690
  llmConfig = (() => {
3480
3691
  const configPath = resolveConfigPath();
3481
3692
  const fileConfig = fs2.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
@@ -3493,13 +3704,6 @@ var init_config = __esm({
3493
3704
  timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
3494
3705
  };
3495
3706
  })();
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
3707
  USER_RULES_PATHS = [
3504
3708
  path2.join(__configModuleDir, "rules", "user_rules.json"),
3505
3709
  // 源在 ts/src/rules(一般不存在)
@@ -7286,6 +7490,56 @@ var BrainRuleStore = class {
7286
7490
  getRule(ruleId) {
7287
7491
  return this.data.rules.find((r) => r.id === ruleId);
7288
7492
  }
7493
+ /** 质疑:active → disputed,并把 disputed_count +1(镜像 Python dispute_rule)。 */
7494
+ disputeRule(ruleId, _reason = "") {
7495
+ const rule = this.getRule(ruleId);
7496
+ if (!rule) throw new Error(`\u89C4\u5219\u4E0D\u5B58\u5728: ${ruleId}`);
7497
+ if (rule.status !== STATUS_ACTIVE) throw new Error(`\u53EA\u6709 active \u72B6\u6001\u53EF\u8D28\u7591\uFF0C\u5F53\u524D: ${rule.status}`);
7498
+ rule.status = STATUS_DISPUTED;
7499
+ rule.disputedCount += 1;
7500
+ this.save();
7501
+ }
7502
+ // ==================== 生命周期(curator 用,镜像 Python rule_store) ====================
7503
+ /** 长期未触发的 active/disputed 规则 → stale。 */
7504
+ markStale(staleDays = 30) {
7505
+ const cutoff = Date.now() - staleDays * 864e5;
7506
+ for (const r of this.data.rules) {
7507
+ if ((r.status === STATUS_ACTIVE || r.status === STATUS_DISPUTED) && r.lastTriggeredAt > 0 && r.lastTriggeredAt < cutoff) {
7508
+ r.status = STATUS_STALE;
7509
+ }
7510
+ }
7511
+ this.save();
7512
+ }
7513
+ /** 长期 stale 的规则 → archived。 */
7514
+ markArchived(archiveDays = 90) {
7515
+ const cutoff = Date.now() - archiveDays * 864e5;
7516
+ for (const r of this.data.rules) {
7517
+ if (r.status === STATUS_STALE && r.lastTriggeredAt > 0 && r.lastTriggeredAt < cutoff) {
7518
+ r.status = STATUS_ARCHIVED;
7519
+ }
7520
+ }
7521
+ this.save();
7522
+ }
7523
+ /** disputed 次数达阈值 → 自动降级 stale(自动纠错闭环)。 */
7524
+ autoDemoteDisputed(disputeThreshold = 3) {
7525
+ for (const r of this.data.rules) {
7526
+ if (r.status === STATUS_DISPUTED && r.disputedCount >= disputeThreshold) {
7527
+ r.status = STATUS_STALE;
7528
+ }
7529
+ }
7530
+ this.save();
7531
+ }
7532
+ // ==================== 中心化预留(镜像 Python) ====================
7533
+ /** 导出本地新增(非 central 来源)的 active 规则。 */
7534
+ getRulesForExport() {
7535
+ return this.data.rules.filter((r) => r.source !== "central" && r.status === STATUS_ACTIVE);
7536
+ }
7537
+ /** 导入中心下发规则(强制标 central 来源,避免被再次回流)。 */
7538
+ importRules(rules) {
7539
+ for (const rule of rules) {
7540
+ this.addRule({ ...rule, source: "central", originNode: "central" });
7541
+ }
7542
+ }
7289
7543
  getActiveRules(layer) {
7290
7544
  return this.data.rules.filter((r) => r.status === STATUS_ACTIVE && (layer === void 0 || r.layer === layer));
7291
7545
  }
@@ -7386,12 +7640,14 @@ var PatternExtractor = class {
7386
7640
  minOccurrences;
7387
7641
  builtinPatterns;
7388
7642
  reviewFn;
7643
+ skillManager;
7389
7644
  constructor(ruleStore, opts) {
7390
7645
  this.ruleStore = ruleStore;
7391
7646
  this.minConfidence = opts?.minConfidence ?? 0.9;
7392
7647
  this.minOccurrences = opts?.minOccurrences ?? 3;
7393
7648
  this.builtinPatterns = opts?.builtinPatterns ?? [];
7394
7649
  this.reviewFn = opts?.review ?? null;
7650
+ this.skillManager = opts?.skillManager ?? null;
7395
7651
  }
7396
7652
  async extract(event, llmResult) {
7397
7653
  const result = { rulesCreated: 0, patternsRecorded: 0, skippedReason: "", deduped: [] };
@@ -7480,8 +7736,96 @@ var PatternExtractor = class {
7480
7736
  result.rulesCreated += 1;
7481
7737
  }
7482
7738
  }
7739
+ if (this.skillManager) {
7740
+ const skill = this.buildSkill(actionDetail, reason, riskType, eventId);
7741
+ if (skill) {
7742
+ const id = this.skillManager.addSkill(skill);
7743
+ if (id) {
7744
+ result.skillsCreated = (result.skillsCreated ?? 0) + 1;
7745
+ }
7746
+ }
7747
+ }
7483
7748
  return result;
7484
7749
  }
7750
+ /**
7751
+ * 生成候选 Skill;不值得沉淀时返回 null(镜像 Python `_extract_skill` 的三道价值评估)。
7752
+ *
7753
+ * 评估 1:reason 太短(<50 字)说明没有可复用的判断逻辑,不沉淀;
7754
+ * 评估 2:同名 skill 已存在 → 跳过;
7755
+ * 评估 3:与已有 active skill 关键词重叠 >= 2 → 跳过(防同类案例重复沉淀,
7756
+ * 如 cat /etc/shadow 与 cat /etc/sudoers 生成两个近乎重复的 skill)。
7757
+ */
7758
+ buildSkill(actionDetail, reason, riskType, eventId) {
7759
+ if (!reason || reason.length < 50) return null;
7760
+ if (!riskType) return null;
7761
+ const detailWords = (actionDetail.toLowerCase().match(/[a-z]+/g) || []).slice(0, 3);
7762
+ const name = detailWords.length > 0 ? `brain_${riskType}_${detailWords.join("_")}` : `brain_${riskType}`;
7763
+ if (this.skillManager.getSkillMeta(name)) return null;
7764
+ const keywords = this.extractSkillKeywords(actionDetail, reason, riskType);
7765
+ if (this.isCoveredByExistingSkill(keywords)) return null;
7766
+ return {
7767
+ name,
7768
+ category: this.categorizeRisk(riskType),
7769
+ content: this.generateSkillContent(name, actionDetail, reason, riskType),
7770
+ keywords,
7771
+ sourceEventIds: eventId ? [eventId] : [],
7772
+ status: "active"
7773
+ };
7774
+ }
7775
+ /** 关键词提取(镜像 Python `_extract_keywords`)。 */
7776
+ extractSkillKeywords(actionDetail, reason, riskType) {
7777
+ const keywords = /* @__PURE__ */ new Set();
7778
+ if (riskType) keywords.add(riskType);
7779
+ for (const w of (actionDetail.toLowerCase().match(/[a-z_]{3,}/g) || []).slice(0, 10)) keywords.add(w);
7780
+ for (const w of (reason.toLowerCase().match(/[a-z_]{3,}/g) || []).slice(0, 5)) keywords.add(w);
7781
+ return [...keywords];
7782
+ }
7783
+ /** 与已有 active skill 关键词重叠 >= 2 → 视为已覆盖(镜像 `_is_covered_by_existing_skill`)。 */
7784
+ isCoveredByExistingSkill(keywords) {
7785
+ if (keywords.length === 0) return false;
7786
+ let existing = [];
7787
+ try {
7788
+ existing = this.skillManager.getActiveSkills();
7789
+ } catch {
7790
+ return false;
7791
+ }
7792
+ const cand = new Set(keywords.map((k) => k.toLowerCase()));
7793
+ for (const meta of existing) {
7794
+ const metaKeywords = (meta.keywords || []).map((k) => String(k).toLowerCase());
7795
+ if (metaKeywords.length === 0) continue;
7796
+ const overlap = metaKeywords.filter((k) => cand.has(k)).length;
7797
+ if (overlap >= 2) return true;
7798
+ }
7799
+ return false;
7800
+ }
7801
+ /** 风险类型 → skill 分类(镜像 Python `_categorize_risk`)。 */
7802
+ categorizeRisk(riskType) {
7803
+ if (!riskType) return "edge_case";
7804
+ if (["inject", "backdoor", "persist"].some((k) => riskType.includes(k))) return "attack_pattern";
7805
+ if (["exfil", "leak", "export"].some((k) => riskType.includes(k))) return "attack_pattern";
7806
+ return "defense_strategy";
7807
+ }
7808
+ /** skill 正文(与 Python `_generate_skill_content` 同结构,便于双端对拍)。 */
7809
+ generateSkillContent(name, actionDetail, reason, riskType) {
7810
+ return `# ${name}
7811
+
7812
+ ## \u6765\u6E90
7813
+ Brain \u81EA\u52A8\u63D0\u53D6\uFF08LLM \u5206\u6790\u6C89\u6DC0\uFF09
7814
+
7815
+ ## \u573A\u666F
7816
+ \u5F53 Agent \u6267\u884C\u4EE5\u4E0B\u7C7B\u578B\u64CD\u4F5C\u65F6\u89E6\u53D1\uFF1A
7817
+ - \u98CE\u9669\u7C7B\u578B: ${riskType}
7818
+ - \u64CD\u4F5C\u7279\u5F81: ${actionDetail.slice(0, 200)}
7819
+
7820
+ ## \u5224\u65AD\u4F9D\u636E
7821
+ ${reason.slice(0, 500)}
7822
+
7823
+ ## \u5904\u7406\u5EFA\u8BAE
7824
+ - \u4F18\u5148\u68C0\u67E5\u64CD\u4F5C\u7684\u4E0A\u4E0B\u6587\u548C\u610F\u56FE
7825
+ - \u7ED3\u5408 action_history \u5224\u65AD\u662F\u5426\u5B58\u5728\u6076\u610F\u94FE
7826
+ - \u5FC5\u8981\u65F6 Deny \u5E76\u8BB0\u5F55\u539F\u56E0
7827
+ `;
7828
+ }
7485
7829
  extractPatterns(actionDetail, reason, riskType) {
7486
7830
  const patterns = [];
7487
7831
  const detailLower = actionDetail.toLowerCase();
@@ -7669,6 +8013,209 @@ function parseReview(content) {
7669
8013
  };
7670
8014
  }
7671
8015
 
8016
+ // src/brain/skill-manager.ts
8017
+ init_esm_shims();
8018
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync13, renameSync, writeFileSync as writeFileSync4 } from "fs";
8019
+ import { homedir as homedir5 } from "os";
8020
+ import { join as join8 } from "path";
8021
+ import { randomUUID } from "crypto";
8022
+ var DEFAULT_SKILL_META_PATH = join8(homedir5(), ".agentmoss", "brain", "skill_meta.json");
8023
+ var ENV_SKILL_META_PATH = "AGENT_MOSS_BRAIN_SKILL_META_PATH";
8024
+ function getMetaPath(override) {
8025
+ return override || process.env[ENV_SKILL_META_PATH] || DEFAULT_SKILL_META_PATH;
8026
+ }
8027
+ var BrainSkillManager = class {
8028
+ constructor(skillsDir, metaPath) {
8029
+ this.skillsDir = skillsDir;
8030
+ this.metaPath = getMetaPath(metaPath);
8031
+ try {
8032
+ mkdirSync4(join8(this.metaPath, ".."), { recursive: true });
8033
+ } catch {
8034
+ }
8035
+ this.load();
8036
+ this.cleanupGhostSkills();
8037
+ }
8038
+ skillsDir;
8039
+ skills = [];
8040
+ metaPath;
8041
+ load() {
8042
+ if (!existsSync13(this.metaPath)) return;
8043
+ try {
8044
+ const data = JSON.parse(readFileSync13(this.metaPath, "utf-8"));
8045
+ this.skills = data.skills || [];
8046
+ } catch {
8047
+ this.skills = [];
8048
+ }
8049
+ }
8050
+ save() {
8051
+ const out = { version: 1, skills: this.skills };
8052
+ const tmp = `${this.metaPath}.tmp`;
8053
+ try {
8054
+ writeFileSync4(tmp, JSON.stringify(out, null, 2), "utf-8");
8055
+ renameSync(tmp, this.metaPath);
8056
+ } catch {
8057
+ }
8058
+ }
8059
+ /**
8060
+ * 清理"幽灵 skill":meta 记 active 但磁盘 .md 已不存在。
8061
+ * (沿用 Python 的处理:置 stale,避免污染关键词表并阻塞同名新 skill 生成。)
8062
+ */
8063
+ cleanupGhostSkills() {
8064
+ let changed = false;
8065
+ for (const sk of this.skills) {
8066
+ if (sk.status !== "active") continue;
8067
+ if (!existsSync13(join8(this.skillsDir, `${sk.name}.md`))) {
8068
+ sk.status = "stale";
8069
+ changed = true;
8070
+ }
8071
+ }
8072
+ if (changed) this.save();
8073
+ }
8074
+ /** 新增 Skill:写 .md + 记元数据。目录缺失或同名已存在时跳过(返回 "")。 */
8075
+ addSkill(skill) {
8076
+ const id = skill.id || randomUUID();
8077
+ const name = skill.name || `brain_${id.slice(0, 8)}`;
8078
+ if (!existsSync13(this.skillsDir)) return "";
8079
+ const mdPath = join8(this.skillsDir, `${name}.md`);
8080
+ if (existsSync13(mdPath)) return "";
8081
+ try {
8082
+ writeFileSync4(mdPath, skill.content, "utf-8");
8083
+ } catch {
8084
+ return "";
8085
+ }
8086
+ this.skills.push({
8087
+ id,
8088
+ name,
8089
+ category: skill.category || "",
8090
+ keywords: skill.keywords || [],
8091
+ source_event_ids: skill.sourceEventIds || [],
8092
+ status: skill.status || "draft",
8093
+ created_at: skill.createdAt || Date.now() / 1e3,
8094
+ usage_count: skill.usageCount || 0,
8095
+ origin_node: skill.originNode || "local"
8096
+ });
8097
+ this.save();
8098
+ return id;
8099
+ }
8100
+ getSkillMeta(name) {
8101
+ return this.skills.find((s) => s.name === name);
8102
+ }
8103
+ getActiveSkills() {
8104
+ return this.skills.filter((s) => s.status === "active").sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
8105
+ }
8106
+ recordUsage(name) {
8107
+ const sk = this.getSkillMeta(name);
8108
+ if (!sk) return;
8109
+ sk.usage_count = (sk.usage_count || 0) + 1;
8110
+ this.save();
8111
+ }
8112
+ /** 长期未使用(usage_count=0)且创建超过 staleDays 的 active skill → stale。 */
8113
+ markStale(staleDays = 60) {
8114
+ const cutoff = Date.now() / 1e3 - staleDays * 86400;
8115
+ let changed = false;
8116
+ for (const sk of this.skills) {
8117
+ if (sk.status === "active" && (sk.usage_count || 0) === 0 && (sk.created_at || 0) < cutoff) {
8118
+ sk.status = "stale";
8119
+ changed = true;
8120
+ }
8121
+ }
8122
+ if (changed) this.save();
8123
+ }
8124
+ /** 导出本地 active skill(含正文),供回流中心。 */
8125
+ exportSkills() {
8126
+ const out = [];
8127
+ for (const sk of this.skills) {
8128
+ if (sk.origin_node !== "local" || sk.status !== "active") continue;
8129
+ const mdPath = join8(this.skillsDir, `${sk.name}.md`);
8130
+ if (!existsSync13(mdPath)) continue;
8131
+ out.push({
8132
+ name: sk.name,
8133
+ category: sk.category || "",
8134
+ content: readFileSync13(mdPath, "utf-8"),
8135
+ keywords: sk.keywords || []
8136
+ });
8137
+ }
8138
+ return out;
8139
+ }
8140
+ /** 导入中心下发 skill(标 central,避免被再次回流)。 */
8141
+ importSkills(skills) {
8142
+ for (const s of skills) {
8143
+ this.addSkill({
8144
+ name: s.name,
8145
+ category: s.category || "",
8146
+ content: s.content,
8147
+ keywords: s.keywords || [],
8148
+ status: "active",
8149
+ originNode: "central"
8150
+ });
8151
+ }
8152
+ }
8153
+ /** 保留 close 签名(JSON 落盘已逐次持久化)。 */
8154
+ close() {
8155
+ this.save();
8156
+ }
8157
+ };
8158
+
8159
+ // src/brain/curator.ts
8160
+ init_esm_shims();
8161
+ var BrainCurator = class {
8162
+ constructor(ruleStore, skillManager) {
8163
+ this.ruleStore = ruleStore;
8164
+ this.skillManager = skillManager;
8165
+ }
8166
+ ruleStore;
8167
+ skillManager;
8168
+ /**
8169
+ * 执行一次维护,返回报告。
8170
+ *
8171
+ * autoApprove 默认 true:brain 提取的规则达阈值后自动生效(学习闭环);
8172
+ * 需人工审核时设 false(由 Console 手动 approve)。
8173
+ */
8174
+ runMaintenance(opts = {}) {
8175
+ const {
8176
+ staleRuleDays = 30,
8177
+ archiveRuleDays = 90,
8178
+ staleSkillDays = 60,
8179
+ disputeThreshold = 3,
8180
+ autoApprove = true
8181
+ } = opts;
8182
+ const autoApproved = autoApprove ? this.autoApproveDrafts() : [];
8183
+ this.ruleStore.markStale(staleRuleDays);
8184
+ this.ruleStore.markArchived(archiveRuleDays);
8185
+ this.ruleStore.autoDemoteDisputed(disputeThreshold);
8186
+ this.skillManager.markStale(staleSkillDays);
8187
+ return {
8188
+ rule_stats: this.ruleStore.getStats(),
8189
+ auto_approved: autoApproved
8190
+ };
8191
+ }
8192
+ /** draft → active(仅未 disputed 的 draft)。返回被激活规则的 pattern 前缀。 */
8193
+ autoApproveDrafts() {
8194
+ const approved = [];
8195
+ let drafts;
8196
+ try {
8197
+ drafts = this.ruleStore.getDraftRules();
8198
+ } catch {
8199
+ return approved;
8200
+ }
8201
+ for (const rule of drafts) {
8202
+ try {
8203
+ this.ruleStore.approveRule(rule.id);
8204
+ approved.push(rule.pattern.slice(0, 60));
8205
+ } catch {
8206
+ }
8207
+ }
8208
+ return approved;
8209
+ }
8210
+ };
8211
+
8212
+ // src/brain/contributor.ts
8213
+ init_esm_shims();
8214
+ init_asset_dir();
8215
+ import { dirname as dirname9 } from "path";
8216
+ import { fileURLToPath as fileURLToPath9 } from "url";
8217
+ var __dirname8 = dirname9(fileURLToPath9(import.meta.url));
8218
+
7672
8219
  // src/brain/index.ts
7673
8220
  function checkBrainRules(store, opts) {
7674
8221
  if (!store) return null;
@@ -7715,6 +8262,10 @@ function checkLayer2Context(pattern, actionHistory, actionDetail) {
7715
8262
  return true;
7716
8263
  }
7717
8264
 
8265
+ // src/engine/coordinator.ts
8266
+ init_asset_dir();
8267
+ init_runtime_config_bridge();
8268
+
7718
8269
  // src/engine/formal-engine/index.ts
7719
8270
  init_esm_shims();
7720
8271
 
@@ -8684,20 +9235,69 @@ function matchesAny(cmd, patterns) {
8684
9235
  }
8685
9236
  var _brainStore = null;
8686
9237
  var _brainExtractor = null;
9238
+ var _brainSkillManager = null;
9239
+ var _brainCurator = null;
8687
9240
  var _brainInitialized = false;
8688
- function getBrainStore() {
9241
+ var _brainLastMaintenance = 0;
9242
+ var BRAIN_MAINT_INTERVAL_MS = 3e5;
9243
+ function getBrainStore(osType) {
8689
9244
  if (!isBrainEnabled()) return null;
8690
9245
  if (!_brainInitialized) {
8691
9246
  _brainStore = new BrainRuleStore();
8692
- _brainExtractor = new PatternExtractor(_brainStore, { review: createBrainReviewer() });
9247
+ const skillsDir = resolveAssetDir(__dirname, "skills");
9248
+ _brainSkillManager = skillsDir ? new BrainSkillManager(skillsDir) : null;
9249
+ _brainExtractor = new PatternExtractor(_brainStore, {
9250
+ review: createBrainReviewer(),
9251
+ // 内置规则去重集(对齐 Python coordinator._ensure_brain 收集的 builtin_patterns):
9252
+ // 危险命令正则 + 敏感路径(deny_* 或 credential)。
9253
+ // 历史 bug:TS 这里只传 review,不传 builtinPatterns → 提取器拿不到内置规则,
9254
+ // `isCoveredByBuiltin` 恒 False → 把 L1/L2 本来就能拦的案例重复沉淀成 Brain 规则。
9255
+ builtinPatterns: collectBrainBuiltinPatterns(osType),
9256
+ skillManager: _brainSkillManager
9257
+ });
9258
+ if (_brainSkillManager) {
9259
+ _brainCurator = new BrainCurator(_brainStore, _brainSkillManager);
9260
+ }
8693
9261
  _brainInitialized = true;
8694
9262
  }
9263
+ const now = Date.now();
9264
+ if (_brainCurator && now - _brainLastMaintenance >= BRAIN_MAINT_INTERVAL_MS) {
9265
+ try {
9266
+ _brainCurator.runMaintenance({ autoApprove: true });
9267
+ } catch {
9268
+ }
9269
+ _brainLastMaintenance = now;
9270
+ }
8695
9271
  return _brainStore;
8696
9272
  }
9273
+ function collectBrainBuiltinPatterns(osType) {
9274
+ const bridge = runtimeConfigBridge.current;
9275
+ if (!bridge) return [];
9276
+ const out = [];
9277
+ try {
9278
+ for (const cp of bridge.effectiveDangerousPatterns(osType)) {
9279
+ if (cp?.pattern?.source) out.push(cp.pattern.source);
9280
+ }
9281
+ } catch {
9282
+ }
9283
+ try {
9284
+ for (const sp of bridge.effectiveSensitivePaths(osType)) {
9285
+ const denyMode = String(sp?.denyMode || "");
9286
+ if (["deny_both", "deny_write", "deny_read"].includes(denyMode) || sp?.credential) {
9287
+ if (sp?.path) out.push(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
9288
+ }
9289
+ }
9290
+ } catch {
9291
+ }
9292
+ return out.filter(Boolean);
9293
+ }
8697
9294
  function _resetBrainForTest() {
8698
9295
  _brainStore = null;
8699
9296
  _brainExtractor = null;
9297
+ _brainSkillManager = null;
9298
+ _brainCurator = null;
8700
9299
  _brainInitialized = false;
9300
+ _brainLastMaintenance = 0;
8701
9301
  }
8702
9302
  var formalEngine = new FormalEngine();
8703
9303
  async function runAnalysis(input) {
@@ -8750,7 +9350,7 @@ async function runAnalysis(input) {
8750
9350
  }
8751
9351
  }
8752
9352
  }
8753
- const brainStore = getBrainStore();
9353
+ const brainStore = getBrainStore(input.osType);
8754
9354
  if (brainStore) {
8755
9355
  const brainL1 = checkBrainRules(brainStore, {
8756
9356
  layer: 1,