evolcore 0.0.3 → 0.0.4

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -12,19 +12,22 @@
12
12
  */
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
+ import crypto from 'crypto';
15
16
  import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agentRelationConfig, agentDir, agentRelationsDir, agentRolesIndex, agentRoleConfig, } from '../paths.js';
16
17
  import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
17
18
  import { fileCache } from '../core/daemon-file-cache.js';
18
19
  import { isValidAid } from '../aun/aid/validation.js';
19
20
  import { isExplicitGroupId } from '../aun/group-identity.js';
20
21
  import { parseContactAlias } from './contact-alias.js';
21
- import { loadSchema, currentVersion, isSchemaName, } from './schema-registry.js';
22
+ import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
22
23
  import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
23
24
  import { normalizeAgentLifecycle } from './lifecycle.js';
24
25
  import { isBehaviorConfigFieldPath } from './config-field-policy.js';
25
26
  import { clearRoleStoreCache, isRoleName } from './role-store.js';
26
27
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
28
+ import { mutateContactBookTransactionSync } from './contact-book-store.js';
27
29
  import { DAEMON_SCHEMA_NAME } from '../product.js';
30
+ import { validateAccessPolicyDomain } from './access-policy-domain.js';
28
31
  export var ConfigTarget;
29
32
  (function (ConfigTarget) {
30
33
  ConfigTarget["Process"] = "process";
@@ -36,6 +39,8 @@ export var ConfigTarget;
36
39
  ConfigTarget["Role"] = "role";
37
40
  })(ConfigTarget || (ConfigTarget = {}));
38
41
  const configWriteListeners = new Set();
42
+ const CONFIG_MUTATION_LOCK_WAIT_MS = 5_000;
43
+ const CONFIG_MUTATION_LOCK_RETRY_MS = 20;
39
44
  /** Observe successful in-process config writes after cache invalidation. */
40
45
  export function onConfigWrite(listener) {
41
46
  configWriteListeners.add(listener);
@@ -224,10 +229,13 @@ export function assertContactBookV2Ready() {
224
229
  if (!fs.existsSync(agentsRoot))
225
230
  return;
226
231
  const issues = [];
227
- const agentSchema = loadSchema('agent-config', 6);
228
- const contactSchema = loadSchema('contact-book', 2);
229
- const relationSchema = loadSchema('relation-config', 5);
230
- const inspect = (file, schema) => {
232
+ const supportedSchemas = (name, minimumVersion) => (listSchemaVersions(name)
233
+ .filter(version => version >= minimumVersion)
234
+ .map(version => loadSchema(name, version)));
235
+ const agentSchemas = supportedSchemas('agent-config', 6);
236
+ const contactSchemas = supportedSchemas('contact-book', 2);
237
+ const relationSchemas = supportedSchemas('relation-config', 5);
238
+ const inspect = (file, schemas) => {
231
239
  let value;
232
240
  try {
233
241
  value = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -236,8 +244,9 @@ export function assertContactBookV2Ready() {
236
244
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
237
245
  return;
238
246
  }
239
- if (!schema.validate(value)) {
240
- const detail = (schema.validate.errors ?? [])
247
+ const candidates = Array.isArray(schemas) ? schemas : [schemas];
248
+ if (!candidates.some(schema => schema.validate(value))) {
249
+ const detail = (candidates.at(-1)?.validate.errors ?? [])
241
250
  .map(error => `${error.instancePath || '/'} ${error.message}`)
242
251
  .join('; ');
243
252
  issues.push({ file, detail });
@@ -263,14 +272,14 @@ export function assertContactBookV2Ready() {
263
272
  const agentFile = path.join(dir, 'config.json');
264
273
  if (!fs.existsSync(agentFile))
265
274
  continue;
266
- inspect(agentFile, agentSchema);
275
+ inspect(agentFile, agentSchemas);
267
276
  const contactFile = path.join(dir, 'contact.json');
268
277
  if (fs.existsSync(contactFile)) {
269
- inspect(contactFile, contactSchema);
278
+ inspect(contactFile, contactSchemas);
270
279
  inspectContactBookDomain(contactFile, agentFile, issues);
271
280
  }
272
281
  for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
273
- inspect(relationFile, relationSchema);
282
+ inspect(relationFile, relationSchemas);
274
283
  }
275
284
  }
276
285
  const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
@@ -394,7 +403,7 @@ function findRelationRoleRemovedByExperimentalMigration(root) {
394
403
  catch {
395
404
  continue;
396
405
  }
397
- if (currentValue?.$schema_version === 5) {
406
+ if (typeof currentValue?.$schema_version === 'number' && currentValue.$schema_version >= 5) {
398
407
  return currentFile;
399
408
  }
400
409
  }
@@ -621,7 +630,12 @@ export function write(target, value, sel, opts = {}) {
621
630
  finishConfigWrite(target, sel, file);
622
631
  return;
623
632
  }
624
- const schema = loadSchema(TARGET_SCHEMA[target]);
633
+ const requestedVersion = value && typeof value === 'object'
634
+ ? Number(value.$schema_version)
635
+ : NaN;
636
+ const schema = target === ConfigTarget.Contact && requestedVersion === 2
637
+ ? loadSchema('contact-book', 2)
638
+ : loadSchema(TARGET_SCHEMA[target]);
625
639
  const withVer = ensureSchemaVersion(value, schema.version);
626
640
  const migrated = withVer;
627
641
  // Agent config 写入规范化(aid 校验、projects 字段清理)
@@ -635,15 +649,166 @@ export function write(target, value, sel, opts = {}) {
635
649
  validateOrThrow(schema, normalized, target);
636
650
  // 1b. responseModeParams 桶内容校验:主 schema 把 responseModeParams 当黑箱,
637
651
  // 此处使用“模式桶 schema”逐桶校验内容(桶键 = 已登记的响应模式 id)。
638
- if (target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
652
+ if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
639
653
  validateResponseModeParams(normalized?.responseModeParams, target);
640
654
  }
655
+ if (target === ConfigTarget.Agent) {
656
+ const selfAid = sel?.self || String(normalized?.aid || '').trim();
657
+ const issue = validateAccessPolicyDomain(selfAid, normalized);
658
+ if (issue)
659
+ throw new ConfigError(issue.code, issue.message);
660
+ }
661
+ }
662
+ // Owner 是实际联系人。将 Agent 配置和缺失的 Owner 联系人写入同一可恢复事务,
663
+ // 避免渠道身份绑定承担联系人创建职责。
664
+ const ownerIds = target === ConfigTarget.Agent && Array.isArray(normalized.owners)
665
+ ? [...new Set(normalized.owners
666
+ .map((value) => String(value || '').trim())
667
+ .filter((value) => isValidAid(value)))]
668
+ : [];
669
+ if (target === ConfigTarget.Agent && sel?.self && ownerIds.length > 0) {
670
+ const before = readTextOrNull(file);
671
+ const after = `${JSON.stringify(normalized, null, 2)}\n`;
672
+ mutateContactBookTransactionSync({
673
+ selfAid: sel.self,
674
+ actor: 'agent-owner-sync',
675
+ mutation: { type: 'ensure-contacts', primaryIds: ownerIds },
676
+ }, [{ file, before, after }]);
677
+ finishConfigWrite(target, sel, file);
678
+ return;
641
679
  }
642
680
  // 2. 写入文件
643
681
  fs.mkdirSync(path.dirname(file), { recursive: true });
644
682
  atomicWriteJson(file, normalized);
645
683
  finishConfigWrite(target, sel, file);
646
684
  }
685
+ /**
686
+ * Mutate one configuration document while holding a file-scoped, cross-process lock.
687
+ *
688
+ * Callers must keep the callback synchronous. This is the safe primitive for
689
+ * read-modify-write paths; direct `read()` followed by `write()` cannot protect
690
+ * unrelated fields from another process writing the same config document.
691
+ */
692
+ export function mutateConfig(target, sel, mutate) {
693
+ if (target === ConfigTarget.RoleRegistry || target === ConfigTarget.Role || target === ConfigTarget.Contact) {
694
+ throw new ConfigError('MUTATION_UNSUPPORTED', `mutateConfig does not support ${target}`);
695
+ }
696
+ const file = targetPath(target, sel);
697
+ const release = acquireConfigMutationLock(file);
698
+ try {
699
+ const current = read(target, sel, { cache: false });
700
+ const next = current ? cloneConfigValue(current) : configSkeleton(target, sel);
701
+ const before = stableConfigValue(current ?? next);
702
+ const value = mutate(next);
703
+ const changed = before !== stableConfigValue(next);
704
+ if (changed)
705
+ write(target, next, sel);
706
+ return { value, changed };
707
+ }
708
+ finally {
709
+ release();
710
+ }
711
+ }
712
+ function cloneConfigValue(value) {
713
+ return JSON.parse(JSON.stringify(value));
714
+ }
715
+ function stableConfigValue(value) {
716
+ return JSON.stringify(sortConfigValue(value));
717
+ }
718
+ function sortConfigValue(value) {
719
+ if (Array.isArray(value))
720
+ return value.map(sortConfigValue);
721
+ if (!value || typeof value !== 'object')
722
+ return value;
723
+ return Object.fromEntries(Object.entries(value)
724
+ .sort(([left], [right]) => left.localeCompare(right))
725
+ .map(([key, nested]) => [key, sortConfigValue(nested)]));
726
+ }
727
+ function acquireConfigMutationLock(file) {
728
+ const lockDir = `${file}.mutation.lock`;
729
+ const ownerFile = path.join(lockDir, 'owner.json');
730
+ const token = crypto.randomBytes(16).toString('hex');
731
+ const candidate = `${lockDir}.candidate-${process.pid}-${token}`;
732
+ const candidateOwner = path.join(candidate, 'owner.json');
733
+ fs.mkdirSync(path.dirname(file), { recursive: true });
734
+ fs.mkdirSync(candidate);
735
+ fs.writeFileSync(candidateOwner, `${JSON.stringify({ pid: process.pid, token })}\n`, { mode: 0o600 });
736
+ let acquired = false;
737
+ const deadline = Date.now() + CONFIG_MUTATION_LOCK_WAIT_MS;
738
+ try {
739
+ while (true) {
740
+ try {
741
+ fs.renameSync(candidate, lockDir);
742
+ acquired = true;
743
+ return () => releaseConfigMutationLock(lockDir, ownerFile, token);
744
+ }
745
+ catch (error) {
746
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
747
+ throw error;
748
+ if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
749
+ continue;
750
+ if (Date.now() >= deadline) {
751
+ throw new ConfigError('LOCK_TIMEOUT', `Timed out waiting for config mutation lock: ${file}`);
752
+ }
753
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, CONFIG_MUTATION_LOCK_RETRY_MS);
754
+ }
755
+ }
756
+ }
757
+ finally {
758
+ if (!acquired)
759
+ fs.rmSync(candidate, { recursive: true, force: true });
760
+ }
761
+ }
762
+ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
763
+ let raw;
764
+ let pid;
765
+ try {
766
+ raw = fs.readFileSync(ownerFile, 'utf8');
767
+ pid = Number(JSON.parse(raw).pid);
768
+ }
769
+ catch {
770
+ return false;
771
+ }
772
+ if (!Number.isInteger(pid) || pid <= 0)
773
+ return false;
774
+ try {
775
+ process.kill(pid, 0);
776
+ return false;
777
+ }
778
+ catch (error) {
779
+ if (error?.code !== 'ESRCH')
780
+ return false;
781
+ const reclaimed = `${lockDir}.reclaimed-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
782
+ try {
783
+ fs.renameSync(lockDir, reclaimed);
784
+ fs.rmSync(reclaimed, { recursive: true, force: true });
785
+ return true;
786
+ }
787
+ catch (renameError) {
788
+ return renameError?.code === 'ENOENT';
789
+ }
790
+ }
791
+ }
792
+ function releaseConfigMutationLock(lockDir, ownerFile, token) {
793
+ try {
794
+ const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
795
+ if (owner.token === token)
796
+ fs.rmSync(lockDir, { recursive: true, force: true });
797
+ }
798
+ catch {
799
+ // A replaced lock is not ours to release.
800
+ }
801
+ }
802
+ function readTextOrNull(file) {
803
+ try {
804
+ return fs.readFileSync(file, 'utf8');
805
+ }
806
+ catch (error) {
807
+ if (error?.code === 'ENOENT')
808
+ return null;
809
+ throw error;
810
+ }
811
+ }
647
812
  function finishConfigWrite(target, sel, file) {
648
813
  if (fileCacheAvailable())
649
814
  fileCache.invalidate(file);
@@ -688,10 +853,9 @@ function validateOrThrow(schema, value, target) {
688
853
  function validateProcessOnlyFields(target, value) {
689
854
  if (target === ConfigTarget.Process || !value || typeof value !== 'object')
690
855
  return [];
691
- const debug = value.debug;
692
- if (!debug || typeof debug !== 'object' || !Object.prototype.hasOwnProperty.call(debug, 'eckSnapshots'))
856
+ if (!Object.prototype.hasOwnProperty.call(value, 'debug'))
693
857
  return [];
694
- return ['/debug/eckSnapshots is only allowed in process config'];
858
+ return ['/debug is only allowed in process config'];
695
859
  }
696
860
  /**
697
861
  * responseModeParams 桶专项校验。
@@ -736,7 +900,12 @@ function validateResponseModeParams(params, target) {
736
900
  * 供 config-store.validateAgentConfig 等只校验不落盘的场景使用。
737
901
  */
738
902
  export function validateConfig(target, value) {
739
- const schema = loadSchema(TARGET_SCHEMA[target]);
903
+ const requestedVersion = value && typeof value === 'object'
904
+ ? Number(value.$schema_version)
905
+ : NaN;
906
+ const schema = target === ConfigTarget.Contact && requestedVersion === 2
907
+ ? loadSchema('contact-book', 2)
908
+ : loadSchema(TARGET_SCHEMA[target]);
740
909
  const withVer = ensureSchemaVersion(value, schema.version);
741
910
  const ok = schema.validate(withVer);
742
911
  const errors = validateProcessOnlyFields(target, withVer);
@@ -772,6 +941,11 @@ export function ensureFile(target, sel) {
772
941
  const file = targetPath(target, sel);
773
942
  if (fs.existsSync(file))
774
943
  return;
944
+ const skeleton = configSkeleton(target, sel);
945
+ fs.mkdirSync(path.dirname(file), { recursive: true });
946
+ atomicWriteJson(file, skeleton);
947
+ }
948
+ function configSkeleton(target, sel) {
775
949
  const schema = loadSchema(TARGET_SCHEMA[target]);
776
950
  const skeleton = { $schema_version: schema.version };
777
951
  const props = schema.raw.properties || {};
@@ -791,8 +965,7 @@ export function ensureFile(target, sel) {
791
965
  else if (spec?.type === 'object')
792
966
  skeleton[key] = {};
793
967
  }
794
- fs.mkdirSync(path.dirname(file), { recursive: true });
795
- atomicWriteJson(file, skeleton);
968
+ return skeleton;
796
969
  }
797
970
  // ── schema 版本迁移 ───────────────────────────────────────────────────────
798
971
  //
@@ -823,9 +996,11 @@ export function resolveAgentConfig(sel, opts = {}) {
823
996
  function stripStaticOwnerField(config) {
824
997
  if (!config)
825
998
  return config;
826
- if (!Object.prototype.hasOwnProperty.call(config, 'owners') && !Object.prototype.hasOwnProperty.call(config, 'admins'))
999
+ if (!Object.prototype.hasOwnProperty.call(config, 'owners')
1000
+ && !Object.prototype.hasOwnProperty.call(config, 'admins')
1001
+ && !Object.prototype.hasOwnProperty.call(config, 'access'))
827
1002
  return config;
828
- const { owners: _owners, admins: _admins, ...rest } = config;
1003
+ const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
829
1004
  return rest;
830
1005
  }
831
1006
  function stripRelationRoleData(config) {
@@ -853,7 +1028,6 @@ export function resolveEffective(sel, opts = {}) {
853
1028
  models: config.models,
854
1029
  projects: config.projects,
855
1030
  capabilities: config.capabilities,
856
- debug: config.debug,
857
1031
  observable: config.observable,
858
1032
  extra_backup: config.extra_backup,
859
1033
  // Runtime configuration parameters
@@ -868,6 +1042,7 @@ export function resolveEffective(sel, opts = {}) {
868
1042
  debounce: config.debounce,
869
1043
  show_activities: config.show_activities,
870
1044
  session_renew: config.session_renew,
1045
+ groupRules: config.groupRules,
871
1046
  render: config.render,
872
1047
  sessionManifests: config.sessionManifests,
873
1048
  enable_rich_content: config.enable_rich_content,
@@ -895,6 +1070,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
895
1070
  'debounce',
896
1071
  'show_activities',
897
1072
  'session_renew',
1073
+ 'groupRules',
898
1074
  'render',
899
1075
  'sessionManifests',
900
1076
  'enable_rich_content',
@@ -1,4 +1,4 @@
1
- import { ConfigError, ConfigTarget, ensureFile, listFields, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile, write, } from './config-manager.js';
1
+ import { ConfigError, ConfigTarget, ensureFile, listFields, mutateConfig, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile, } from './config-manager.js';
2
2
  import { collectConfigFiles, diffVersions, listAllVersions, prune, readCurrent, restore, snapshot, isConfigSnapshotsEnabled, } from './snapshot.js';
3
3
  import { readBootLog } from './boot-log.js';
4
4
  import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, listSchemaVersions, readRawSchema, schemaHistoryEntry, } from './schema-registry.js';
@@ -302,27 +302,26 @@ function executeSet(op, options) {
302
302
  if (!op.route)
303
303
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
304
304
  const selector = selectorFor(op, options);
305
- const existing = read(op.route.target, selector) || {};
306
- if (op.route.merge === 'list') {
307
- const previous = getNested(existing, op.field);
308
- const previousItems = Array.isArray(previous) ? previous : [];
309
- const nextItems = Array.isArray(op.value) ? op.value : [op.value];
310
- const merged = [...new Set([...previousItems, ...nextItems].map(value => typeof value === 'object' ? JSON.stringify(value) : value))]
311
- .map(value => {
312
- try {
313
- return typeof value === 'string' && value.startsWith('{') ? JSON.parse(value) : value;
314
- }
315
- catch {
316
- return value;
317
- }
318
- });
319
- setNested(existing, op.field, merged);
320
- }
321
- else {
322
- setNested(existing, op.field, op.value);
323
- }
324
- ensureFile(op.route.target, selector);
325
- write(op.route.target, existing, selector);
305
+ mutateConfig(op.route.target, selector, existing => {
306
+ if (op.route.merge === 'list') {
307
+ const previous = getNested(existing, op.field);
308
+ const previousItems = Array.isArray(previous) ? previous : [];
309
+ const nextItems = Array.isArray(op.value) ? op.value : [op.value];
310
+ const merged = [...new Set([...previousItems, ...nextItems].map(value => typeof value === 'object' ? JSON.stringify(value) : value))]
311
+ .map(value => {
312
+ try {
313
+ return typeof value === 'string' && value.startsWith('{') ? JSON.parse(value) : value;
314
+ }
315
+ catch {
316
+ return value;
317
+ }
318
+ });
319
+ setNested(existing, op.field, merged);
320
+ }
321
+ else {
322
+ setNested(existing, op.field, op.value);
323
+ }
324
+ });
326
325
  return {
327
326
  ok: true,
328
327
  subcommand: 'set',
@@ -336,22 +335,20 @@ function executeUnset(op, options) {
336
335
  if (!op.route)
337
336
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
338
337
  const selector = selectorFor(op, options);
339
- const existing = read(op.route.target, selector);
340
- if (!existing) {
341
- return { ok: true, subcommand: 'unset', field: op.field, removed: false, scope: op.configScope };
342
- }
343
- const parts = op.field.split('.');
344
- let current = existing;
345
- for (let i = 0; i < parts.length - 1; i++) {
346
- if (current == null)
347
- break;
348
- current = current[parts[i]];
349
- }
350
- const leaf = parts[parts.length - 1];
351
- const removed = !!current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, leaf);
352
- if (removed)
353
- delete current[leaf];
354
- write(op.route.target, existing, selector);
338
+ const { value: removed } = mutateConfig(op.route.target, selector, existing => {
339
+ const parts = op.field.split('.');
340
+ let current = existing;
341
+ for (let i = 0; i < parts.length - 1; i++) {
342
+ if (current == null)
343
+ break;
344
+ current = current[parts[i]];
345
+ }
346
+ const leaf = parts[parts.length - 1];
347
+ const exists = !!current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, leaf);
348
+ if (exists)
349
+ delete current[leaf];
350
+ return exists;
351
+ });
355
352
  return { ok: true, subcommand: 'unset', field: op.field, removed, scope: op.configScope };
356
353
  }
357
354
  function selectorFor(command, options = {}) {