evolcore 0.0.2 → 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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  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 +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  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/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -12,17 +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
- import { loadSchema, currentVersion, isSchemaName, } from './schema-registry.js';
20
+ import { isExplicitGroupId } from '../aun/group-identity.js';
21
+ import { parseContactAlias } from './contact-alias.js';
22
+ import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
20
23
  import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
21
24
  import { normalizeAgentLifecycle } from './lifecycle.js';
22
25
  import { isBehaviorConfigFieldPath } from './config-field-policy.js';
23
26
  import { clearRoleStoreCache, isRoleName } from './role-store.js';
24
27
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
28
+ import { mutateContactBookTransactionSync } from './contact-book-store.js';
25
29
  import { DAEMON_SCHEMA_NAME } from '../product.js';
30
+ import { validateAccessPolicyDomain } from './access-policy-domain.js';
26
31
  export var ConfigTarget;
27
32
  (function (ConfigTarget) {
28
33
  ConfigTarget["Process"] = "process";
@@ -34,6 +39,8 @@ export var ConfigTarget;
34
39
  ConfigTarget["Role"] = "role";
35
40
  })(ConfigTarget || (ConfigTarget = {}));
36
41
  const configWriteListeners = new Set();
42
+ const CONFIG_MUTATION_LOCK_WAIT_MS = 5_000;
43
+ const CONFIG_MUTATION_LOCK_RETRY_MS = 20;
37
44
  /** Observe successful in-process config writes after cache invalidation. */
38
45
  export function onConfigWrite(listener) {
39
46
  configWriteListeners.add(listener);
@@ -76,10 +83,7 @@ export function assertRoleConfigV4Ready() {
76
83
  if (!fs.existsSync(agentsRoot))
77
84
  return;
78
85
  const issues = [];
79
- const agentSchema = loadSchema('agent-config');
80
- const agentSchemaV4 = loadSchema('agent-config', 4);
81
- const relationSchema = loadSchema('relation-config');
82
- const inspect = (file, schema, legacyAgent = false) => {
86
+ const inspect = (file, logicalName, minimumVersion) => {
83
87
  let value;
84
88
  try {
85
89
  value = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -88,18 +92,25 @@ export function assertRoleConfigV4Ready() {
88
92
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
89
93
  return;
90
94
  }
91
- const expectedVersion = schema.version;
92
95
  const actualVersion = value && typeof value === 'object'
93
96
  ? value.$schema_version
94
97
  : undefined;
95
- const acceptedLegacyAgent = legacyAgent && actualVersion === 4;
96
- if (actualVersion !== expectedVersion && !acceptedLegacyAgent) {
98
+ if (typeof actualVersion !== 'number' || actualVersion < minimumVersion) {
97
99
  issues.push({
98
100
  file,
99
- detail: `$schema_version must be ${expectedVersion}, got ${String(actualVersion)}`,
101
+ detail: `$schema_version must be at least ${minimumVersion}, got ${String(actualVersion)}`,
100
102
  });
103
+ return;
104
+ }
105
+ let schema;
106
+ try {
107
+ schema = loadSchema(logicalName, actualVersion);
108
+ }
109
+ catch {
110
+ issues.push({ file, detail: `unsupported ${logicalName} schema version ${actualVersion}` });
111
+ return;
101
112
  }
102
- if (!schema.validate(value) && !acceptedLegacyAgent) {
113
+ if (!schema.validate(value)) {
103
114
  const detail = (schema.validate.errors ?? []).map(error => {
104
115
  const extra = typeof error.params?.additionalProperty === 'string'
105
116
  ? ` (additionalProperty=${error.params.additionalProperty})`
@@ -128,12 +139,10 @@ export function assertRoleConfigV4Ready() {
128
139
  const agentDirPath = path.join(agentsRoot, entry.name);
129
140
  const agentFile = path.join(agentDirPath, 'config.json');
130
141
  if (fs.existsSync(agentFile)) {
131
- const raw = JSON.parse(fs.readFileSync(agentFile, 'utf8'));
132
- const legacyVersion = typeof raw?.$schema_version === 'number' && raw.$schema_version <= 4;
133
- inspect(agentFile, legacyVersion ? agentSchemaV4 : agentSchema, raw?.$schema_version === 4);
142
+ inspect(agentFile, 'agent-config', 4);
134
143
  }
135
144
  for (const relationFile of walkRelationConfigs(path.join(agentDirPath, 'relations'))) {
136
- inspect(relationFile, relationSchema);
145
+ inspect(relationFile, 'relation-config', 4);
137
146
  }
138
147
  }
139
148
  if (!issues.length)
@@ -156,7 +165,6 @@ export function assertRoleConfigV5Ready() {
156
165
  if (!fs.existsSync(agentsRoot))
157
166
  return;
158
167
  const issues = [];
159
- const agentSchema = loadSchema('agent-config', 5);
160
168
  const registrySchema = loadSchema('role-registry');
161
169
  const readJson = (file) => {
162
170
  try {
@@ -177,9 +185,22 @@ export function assertRoleConfigV5Ready() {
177
185
  if (!fs.existsSync(agentFile))
178
186
  continue;
179
187
  const agent = readJson(agentFile);
180
- if (!agent || !agentSchema.validate(agent)) {
188
+ const agentVersion = agent?.$schema_version;
189
+ let agentSchema = null;
190
+ if (typeof agentVersion === 'number' && agentVersion >= 5) {
191
+ try {
192
+ agentSchema = loadSchema('agent-config', agentVersion);
193
+ }
194
+ catch { }
195
+ }
196
+ if (!agent || !agentSchema || !agentSchema.validate(agent)) {
181
197
  if (agent)
182
- issues.push({ file: agentFile, detail: schemaErrors(agentSchema) });
198
+ issues.push({
199
+ file: agentFile,
200
+ detail: agentSchema
201
+ ? schemaErrors(agentSchema)
202
+ : `agent schema version must be supported and >= 5, got ${String(agentVersion)}`,
203
+ });
183
204
  continue;
184
205
  }
185
206
  const registryFile = agentRolesIndex(aid);
@@ -201,6 +222,195 @@ export function assertRoleConfigV5Ready() {
201
222
  `node kits/migrations/migrate-role-config-v5.mjs --home ${JSON.stringify(root)}\n` +
202
223
  `node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(root)}`, { issues });
203
224
  }
225
+ /** Final startup gate for the contact-book authorization migration. */
226
+ export function assertContactBookV2Ready() {
227
+ const root = resolvePaths().root;
228
+ const agentsRoot = resolvePaths().agentsDir;
229
+ if (!fs.existsSync(agentsRoot))
230
+ return;
231
+ const issues = [];
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) => {
239
+ let value;
240
+ try {
241
+ value = JSON.parse(fs.readFileSync(file, 'utf8'));
242
+ }
243
+ catch (error) {
244
+ issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
245
+ return;
246
+ }
247
+ const candidates = Array.isArray(schemas) ? schemas : [schemas];
248
+ if (!candidates.some(schema => schema.validate(value))) {
249
+ const detail = (candidates.at(-1)?.validate.errors ?? [])
250
+ .map(error => `${error.instancePath || '/'} ${error.message}`)
251
+ .join('; ');
252
+ issues.push({ file, detail });
253
+ }
254
+ };
255
+ const walkRelationConfigs = (dir) => {
256
+ if (!fs.existsSync(dir))
257
+ return [];
258
+ const files = [];
259
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
260
+ const full = path.join(dir, entry.name);
261
+ if (entry.isDirectory())
262
+ files.push(...walkRelationConfigs(full));
263
+ else if (entry.isFile() && entry.name === 'config.json')
264
+ files.push(full);
265
+ }
266
+ return files;
267
+ };
268
+ for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
269
+ if (!entry.isDirectory() || !isValidAid(entry.name))
270
+ continue;
271
+ const dir = path.join(agentsRoot, entry.name);
272
+ const agentFile = path.join(dir, 'config.json');
273
+ if (!fs.existsSync(agentFile))
274
+ continue;
275
+ inspect(agentFile, agentSchemas);
276
+ const contactFile = path.join(dir, 'contact.json');
277
+ if (fs.existsSync(contactFile)) {
278
+ inspect(contactFile, contactSchemas);
279
+ inspectContactBookDomain(contactFile, agentFile, issues);
280
+ }
281
+ for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
282
+ inspect(relationFile, relationSchemas);
283
+ }
284
+ }
285
+ const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
286
+ if (!hasContactRoleAuthorityReceipt(correctionReceipt)) {
287
+ const recoverable = findRelationRoleRemovedByExperimentalMigration(root);
288
+ if (recoverable) {
289
+ issues.push({
290
+ file: recoverable,
291
+ detail: 'experimental contact-book migration backup must be reviewed and relation role authority correction recorded',
292
+ });
293
+ }
294
+ }
295
+ if (!issues.length)
296
+ return;
297
+ const shown = issues.slice(0, 20)
298
+ .map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`)
299
+ .join('\n');
300
+ const omitted = issues.length > 20 ? `\n- ... ${issues.length - 20} more issue(s)` : '';
301
+ throw new ConfigError('CONTACT_BOOK_V2_MIGRATION_REQUIRED', `Contact book authorization migration is required before daemon startup:\n${shown}${omitted}\n` +
302
+ `node kits/migrations/migrate-contact-book-v2.mjs --home ${JSON.stringify(root)}\n` +
303
+ `node kits/migrations/migrate-contact-book-v2.mjs --apply --home ${JSON.stringify(root)}`, { issues });
304
+ }
305
+ function inspectContactBookDomain(contactFile, agentFile, issues) {
306
+ let contact;
307
+ let agent;
308
+ try {
309
+ contact = JSON.parse(fs.readFileSync(contactFile, 'utf8'));
310
+ agent = JSON.parse(fs.readFileSync(agentFile, 'utf8'));
311
+ }
312
+ catch {
313
+ return;
314
+ }
315
+ const owners = new Set(Array.isArray(agent?.owners) ? agent.owners.map(String) : []);
316
+ const configuredChannelKeys = new Set((Array.isArray(agent?.channels) ? agent.channels : [])
317
+ .filter((channel) => channel && typeof channel === 'object' && channel.type !== 'aun')
318
+ .map((channel) => {
319
+ const type = String(channel.type || '').trim().toLowerCase();
320
+ const name = String(channel.name || '').trim();
321
+ return type && name ? `${type}#${String(agent?.aid || '').trim()}#${name}` : '';
322
+ })
323
+ .filter(Boolean));
324
+ const aliases = new Map();
325
+ for (const [primaryId, entry] of Object.entries(contact?.contacts ?? {})) {
326
+ if (isExplicitGroupId(primaryId)) {
327
+ issues.push({ file: contactFile, detail: `group contact ${primaryId} is not supported` });
328
+ }
329
+ if (entry?.status === 'blocked' && owners.has(primaryId)) {
330
+ issues.push({ file: contactFile, detail: `owner contact ${primaryId} cannot be blocked` });
331
+ }
332
+ for (const alias of Array.isArray(entry?.aliases) ? entry.aliases : []) {
333
+ const parsed = parseContactAlias(alias, agent?.aid);
334
+ if (!parsed) {
335
+ issues.push({ file: contactFile, detail: `invalid Contact alias ${String(alias)}; expected <channelKey>:<urlEncode(peerId)>` });
336
+ continue;
337
+ }
338
+ if (!configuredChannelKeys.has(parsed.channelKey)) {
339
+ issues.push({ file: contactFile, detail: `alias ${alias} references an unconfigured channel instance` });
340
+ continue;
341
+ }
342
+ const previous = aliases.get(parsed.alias);
343
+ if (previous && previous !== primaryId) {
344
+ issues.push({ file: contactFile, detail: `alias ${parsed.alias} is assigned to both ${previous} and ${primaryId}` });
345
+ }
346
+ else {
347
+ aliases.set(parsed.alias, primaryId);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ function hasContactRoleAuthorityReceipt(file) {
353
+ try {
354
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
355
+ return value?.migration === 'contact-book-v2-relation-role-authority' && value?.designVersion === 2;
356
+ }
357
+ catch {
358
+ return false;
359
+ }
360
+ }
361
+ function findRelationRoleRemovedByExperimentalMigration(root) {
362
+ const backupsRoot = path.join(root, 'backups', 'contact-book-v2-migration');
363
+ if (!fs.existsSync(backupsRoot))
364
+ return null;
365
+ for (const stamp of fs.readdirSync(backupsRoot, { withFileTypes: true })) {
366
+ if (!stamp.isDirectory())
367
+ continue;
368
+ try {
369
+ const manifest = JSON.parse(fs.readFileSync(path.join(backupsRoot, stamp.name, 'manifest.json'), 'utf8'));
370
+ if (manifest?.designVersion >= 2)
371
+ continue;
372
+ }
373
+ catch { }
374
+ const agentsRoot = path.join(backupsRoot, stamp.name, 'files', 'agents');
375
+ if (!fs.existsSync(agentsRoot))
376
+ continue;
377
+ for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
378
+ if (!agent.isDirectory() || !isValidAid(agent.name))
379
+ continue;
380
+ const relationsRoot = path.join(agentsRoot, agent.name, 'relations');
381
+ if (!fs.existsSync(relationsRoot))
382
+ continue;
383
+ for (const relation of fs.readdirSync(relationsRoot, { withFileTypes: true })) {
384
+ if (!relation.isDirectory() || relation.name.startsWith('_'))
385
+ continue;
386
+ const backupFile = path.join(relationsRoot, relation.name, 'config.json');
387
+ if (!fs.existsSync(backupFile))
388
+ continue;
389
+ let backupValue;
390
+ try {
391
+ backupValue = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
392
+ }
393
+ catch {
394
+ continue;
395
+ }
396
+ if (typeof backupValue?.role !== 'string' || backupValue.role === 'owner')
397
+ continue;
398
+ const currentFile = path.join(root, 'agents', agent.name, 'relations', relation.name, 'config.json');
399
+ let currentValue;
400
+ try {
401
+ currentValue = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
402
+ }
403
+ catch {
404
+ continue;
405
+ }
406
+ if (typeof currentValue?.$schema_version === 'number' && currentValue.$schema_version >= 5) {
407
+ return currentFile;
408
+ }
409
+ }
410
+ }
411
+ }
412
+ return null;
413
+ }
204
414
  /**
205
415
  * 启动时检查 schema 版本(daemon 启动时调用)。
206
416
  * 扫描所有配置文件,如有版本不匹配则警告一次。
@@ -420,7 +630,12 @@ export function write(target, value, sel, opts = {}) {
420
630
  finishConfigWrite(target, sel, file);
421
631
  return;
422
632
  }
423
- 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]);
424
639
  const withVer = ensureSchemaVersion(value, schema.version);
425
640
  const migrated = withVer;
426
641
  // Agent config 写入规范化(aid 校验、projects 字段清理)
@@ -434,15 +649,166 @@ export function write(target, value, sel, opts = {}) {
434
649
  validateOrThrow(schema, normalized, target);
435
650
  // 1b. responseModeParams 桶内容校验:主 schema 把 responseModeParams 当黑箱,
436
651
  // 此处使用“模式桶 schema”逐桶校验内容(桶键 = 已登记的响应模式 id)。
437
- if (target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
652
+ if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
438
653
  validateResponseModeParams(normalized?.responseModeParams, target);
439
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;
440
679
  }
441
680
  // 2. 写入文件
442
681
  fs.mkdirSync(path.dirname(file), { recursive: true });
443
682
  atomicWriteJson(file, normalized);
444
683
  finishConfigWrite(target, sel, file);
445
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
+ }
446
812
  function finishConfigWrite(target, sel, file) {
447
813
  if (fileCacheAvailable())
448
814
  fileCache.invalidate(file);
@@ -459,6 +825,10 @@ function finishConfigWrite(target, sel, file) {
459
825
  }
460
826
  }
461
827
  }
828
+ /** Notify cache and security listeners after a caller-owned atomic transaction commits. */
829
+ export function notifyConfigWrite(target, sel) {
830
+ finishConfigWrite(target, sel, targetPath(target, sel));
831
+ }
462
832
  function ensureSchemaVersion(value, version) {
463
833
  return value && typeof value === 'object'
464
834
  ? { ...value, $schema_version: version }
@@ -483,10 +853,9 @@ function validateOrThrow(schema, value, target) {
483
853
  function validateProcessOnlyFields(target, value) {
484
854
  if (target === ConfigTarget.Process || !value || typeof value !== 'object')
485
855
  return [];
486
- const debug = value.debug;
487
- if (!debug || typeof debug !== 'object' || !Object.prototype.hasOwnProperty.call(debug, 'eckSnapshots'))
856
+ if (!Object.prototype.hasOwnProperty.call(value, 'debug'))
488
857
  return [];
489
- return ['/debug/eckSnapshots is only allowed in process config'];
858
+ return ['/debug is only allowed in process config'];
490
859
  }
491
860
  /**
492
861
  * responseModeParams 桶专项校验。
@@ -531,7 +900,12 @@ function validateResponseModeParams(params, target) {
531
900
  * 供 config-store.validateAgentConfig 等只校验不落盘的场景使用。
532
901
  */
533
902
  export function validateConfig(target, value) {
534
- 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]);
535
909
  const withVer = ensureSchemaVersion(value, schema.version);
536
910
  const ok = schema.validate(withVer);
537
911
  const errors = validateProcessOnlyFields(target, withVer);
@@ -567,6 +941,11 @@ export function ensureFile(target, sel) {
567
941
  const file = targetPath(target, sel);
568
942
  if (fs.existsSync(file))
569
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) {
570
949
  const schema = loadSchema(TARGET_SCHEMA[target]);
571
950
  const skeleton = { $schema_version: schema.version };
572
951
  const props = schema.raw.properties || {};
@@ -586,8 +965,7 @@ export function ensureFile(target, sel) {
586
965
  else if (spec?.type === 'object')
587
966
  skeleton[key] = {};
588
967
  }
589
- fs.mkdirSync(path.dirname(file), { recursive: true });
590
- atomicWriteJson(file, skeleton);
968
+ return skeleton;
591
969
  }
592
970
  // ── schema 版本迁移 ───────────────────────────────────────────────────────
593
971
  //
@@ -618,9 +996,11 @@ export function resolveAgentConfig(sel, opts = {}) {
618
996
  function stripStaticOwnerField(config) {
619
997
  if (!config)
620
998
  return config;
621
- 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'))
622
1002
  return config;
623
- const { owners: _owners, admins: _admins, ...rest } = config;
1003
+ const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
624
1004
  return rest;
625
1005
  }
626
1006
  function stripRelationRoleData(config) {
@@ -643,13 +1023,11 @@ export function resolveEffective(sel, opts = {}) {
643
1023
  lifecycle: config.lifecycle,
644
1024
  initialized: config.initialized,
645
1025
  owners: config.owners,
646
- admins: config.admins,
647
1026
  aun: config.aun,
648
1027
  channels: config.channels ?? [],
649
1028
  models: config.models,
650
1029
  projects: config.projects,
651
1030
  capabilities: config.capabilities,
652
- debug: config.debug,
653
1031
  observable: config.observable,
654
1032
  extra_backup: config.extra_backup,
655
1033
  // Runtime configuration parameters
@@ -664,6 +1042,7 @@ export function resolveEffective(sel, opts = {}) {
664
1042
  debounce: config.debounce,
665
1043
  show_activities: config.show_activities,
666
1044
  session_renew: config.session_renew,
1045
+ groupRules: config.groupRules,
667
1046
  render: config.render,
668
1047
  sessionManifests: config.sessionManifests,
669
1048
  enable_rich_content: config.enable_rich_content,
@@ -691,6 +1070,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
691
1070
  'debounce',
692
1071
  'show_activities',
693
1072
  'session_renew',
1073
+ 'groupRules',
694
1074
  'render',
695
1075
  'sessionManifests',
696
1076
  'enable_rich_content',