evolcore 0.0.2 → 0.0.3

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 (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -1,4 +1,6 @@
1
1
  import { readBuiltinRoleTemplate } from './builtin-role-templates.js';
2
+ import { expectedRoleRank } from './role-ranks.js';
3
+ export { expectedRoleRank, ROLE_RANKS } from './role-ranks.js';
2
4
  export const BUILTIN_USER_ROLES = ['member', 'visitor'];
3
5
  export const MANAGEMENT_ROLES = ['owner', 'admin'];
4
6
  export function isManagementRole(role) {
@@ -22,8 +24,10 @@ export function getBuiltinRolesConfig() {
22
24
  * Management grants are a code-level ceiling, not configurable role policy.
23
25
  * A role file may narrow these rules but can never add a management grant.
24
26
  */
25
- export function getManagementCommandPermissions(_role) {
27
+ export function getManagementCommandPermissions(role) {
26
28
  return {
29
+ 'contact.read': { allow: true, scopes: ['agent'] },
30
+ 'contact.block': { allow: true, scopes: ['agent'] },
27
31
  'role.assign': { allow: true, scopes: ['agent'] },
28
32
  'role.revoke': { allow: true, scopes: ['agent'] },
29
33
  'role.policy.read': { allow: true, scopes: ['agent'] },
@@ -37,6 +41,7 @@ export function getManagementCommandPermissions(_role) {
37
41
  export function applyManagementRoleCeiling(role, configured) {
38
42
  const fallback = {
39
43
  description: role === 'owner' ? 'Agent owner' : 'Agent administrator',
44
+ rank: expectedRoleRank(role),
40
45
  allowAccess: true,
41
46
  permissions: {
42
47
  permissionMode: { default: 'request', allowOverride: false },
@@ -46,6 +51,7 @@ export function applyManagementRoleCeiling(role, configured) {
46
51
  const policy = configured ?? readBuiltinRoleTemplate(role).definition ?? fallback;
47
52
  return {
48
53
  ...policy,
54
+ rank: expectedRoleRank(role),
49
55
  allowAccess: true,
50
56
  commandPermissions: narrowManagementCommandPermissions(getManagementCommandPermissions(role), policy.commandPermissions || {}),
51
57
  };
@@ -16,6 +16,8 @@ import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agent
16
16
  import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
17
17
  import { fileCache } from '../core/daemon-file-cache.js';
18
18
  import { isValidAid } from '../aun/aid/validation.js';
19
+ import { isExplicitGroupId } from '../aun/group-identity.js';
20
+ import { parseContactAlias } from './contact-alias.js';
19
21
  import { loadSchema, currentVersion, isSchemaName, } from './schema-registry.js';
20
22
  import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
21
23
  import { normalizeAgentLifecycle } from './lifecycle.js';
@@ -76,10 +78,7 @@ export function assertRoleConfigV4Ready() {
76
78
  if (!fs.existsSync(agentsRoot))
77
79
  return;
78
80
  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) => {
81
+ const inspect = (file, logicalName, minimumVersion) => {
83
82
  let value;
84
83
  try {
85
84
  value = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -88,18 +87,25 @@ export function assertRoleConfigV4Ready() {
88
87
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
89
88
  return;
90
89
  }
91
- const expectedVersion = schema.version;
92
90
  const actualVersion = value && typeof value === 'object'
93
91
  ? value.$schema_version
94
92
  : undefined;
95
- const acceptedLegacyAgent = legacyAgent && actualVersion === 4;
96
- if (actualVersion !== expectedVersion && !acceptedLegacyAgent) {
93
+ if (typeof actualVersion !== 'number' || actualVersion < minimumVersion) {
97
94
  issues.push({
98
95
  file,
99
- detail: `$schema_version must be ${expectedVersion}, got ${String(actualVersion)}`,
96
+ detail: `$schema_version must be at least ${minimumVersion}, got ${String(actualVersion)}`,
100
97
  });
98
+ return;
99
+ }
100
+ let schema;
101
+ try {
102
+ schema = loadSchema(logicalName, actualVersion);
101
103
  }
102
- if (!schema.validate(value) && !acceptedLegacyAgent) {
104
+ catch {
105
+ issues.push({ file, detail: `unsupported ${logicalName} schema version ${actualVersion}` });
106
+ return;
107
+ }
108
+ if (!schema.validate(value)) {
103
109
  const detail = (schema.validate.errors ?? []).map(error => {
104
110
  const extra = typeof error.params?.additionalProperty === 'string'
105
111
  ? ` (additionalProperty=${error.params.additionalProperty})`
@@ -128,12 +134,10 @@ export function assertRoleConfigV4Ready() {
128
134
  const agentDirPath = path.join(agentsRoot, entry.name);
129
135
  const agentFile = path.join(agentDirPath, 'config.json');
130
136
  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);
137
+ inspect(agentFile, 'agent-config', 4);
134
138
  }
135
139
  for (const relationFile of walkRelationConfigs(path.join(agentDirPath, 'relations'))) {
136
- inspect(relationFile, relationSchema);
140
+ inspect(relationFile, 'relation-config', 4);
137
141
  }
138
142
  }
139
143
  if (!issues.length)
@@ -156,7 +160,6 @@ export function assertRoleConfigV5Ready() {
156
160
  if (!fs.existsSync(agentsRoot))
157
161
  return;
158
162
  const issues = [];
159
- const agentSchema = loadSchema('agent-config', 5);
160
163
  const registrySchema = loadSchema('role-registry');
161
164
  const readJson = (file) => {
162
165
  try {
@@ -177,9 +180,22 @@ export function assertRoleConfigV5Ready() {
177
180
  if (!fs.existsSync(agentFile))
178
181
  continue;
179
182
  const agent = readJson(agentFile);
180
- if (!agent || !agentSchema.validate(agent)) {
183
+ const agentVersion = agent?.$schema_version;
184
+ let agentSchema = null;
185
+ if (typeof agentVersion === 'number' && agentVersion >= 5) {
186
+ try {
187
+ agentSchema = loadSchema('agent-config', agentVersion);
188
+ }
189
+ catch { }
190
+ }
191
+ if (!agent || !agentSchema || !agentSchema.validate(agent)) {
181
192
  if (agent)
182
- issues.push({ file: agentFile, detail: schemaErrors(agentSchema) });
193
+ issues.push({
194
+ file: agentFile,
195
+ detail: agentSchema
196
+ ? schemaErrors(agentSchema)
197
+ : `agent schema version must be supported and >= 5, got ${String(agentVersion)}`,
198
+ });
183
199
  continue;
184
200
  }
185
201
  const registryFile = agentRolesIndex(aid);
@@ -201,6 +217,191 @@ export function assertRoleConfigV5Ready() {
201
217
  `node kits/migrations/migrate-role-config-v5.mjs --home ${JSON.stringify(root)}\n` +
202
218
  `node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(root)}`, { issues });
203
219
  }
220
+ /** Final startup gate for the contact-book authorization migration. */
221
+ export function assertContactBookV2Ready() {
222
+ const root = resolvePaths().root;
223
+ const agentsRoot = resolvePaths().agentsDir;
224
+ if (!fs.existsSync(agentsRoot))
225
+ return;
226
+ 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) => {
231
+ let value;
232
+ try {
233
+ value = JSON.parse(fs.readFileSync(file, 'utf8'));
234
+ }
235
+ catch (error) {
236
+ issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
237
+ return;
238
+ }
239
+ if (!schema.validate(value)) {
240
+ const detail = (schema.validate.errors ?? [])
241
+ .map(error => `${error.instancePath || '/'} ${error.message}`)
242
+ .join('; ');
243
+ issues.push({ file, detail });
244
+ }
245
+ };
246
+ const walkRelationConfigs = (dir) => {
247
+ if (!fs.existsSync(dir))
248
+ return [];
249
+ const files = [];
250
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
251
+ const full = path.join(dir, entry.name);
252
+ if (entry.isDirectory())
253
+ files.push(...walkRelationConfigs(full));
254
+ else if (entry.isFile() && entry.name === 'config.json')
255
+ files.push(full);
256
+ }
257
+ return files;
258
+ };
259
+ for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
260
+ if (!entry.isDirectory() || !isValidAid(entry.name))
261
+ continue;
262
+ const dir = path.join(agentsRoot, entry.name);
263
+ const agentFile = path.join(dir, 'config.json');
264
+ if (!fs.existsSync(agentFile))
265
+ continue;
266
+ inspect(agentFile, agentSchema);
267
+ const contactFile = path.join(dir, 'contact.json');
268
+ if (fs.existsSync(contactFile)) {
269
+ inspect(contactFile, contactSchema);
270
+ inspectContactBookDomain(contactFile, agentFile, issues);
271
+ }
272
+ for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
273
+ inspect(relationFile, relationSchema);
274
+ }
275
+ }
276
+ const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
277
+ if (!hasContactRoleAuthorityReceipt(correctionReceipt)) {
278
+ const recoverable = findRelationRoleRemovedByExperimentalMigration(root);
279
+ if (recoverable) {
280
+ issues.push({
281
+ file: recoverable,
282
+ detail: 'experimental contact-book migration backup must be reviewed and relation role authority correction recorded',
283
+ });
284
+ }
285
+ }
286
+ if (!issues.length)
287
+ return;
288
+ const shown = issues.slice(0, 20)
289
+ .map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`)
290
+ .join('\n');
291
+ const omitted = issues.length > 20 ? `\n- ... ${issues.length - 20} more issue(s)` : '';
292
+ throw new ConfigError('CONTACT_BOOK_V2_MIGRATION_REQUIRED', `Contact book authorization migration is required before daemon startup:\n${shown}${omitted}\n` +
293
+ `node kits/migrations/migrate-contact-book-v2.mjs --home ${JSON.stringify(root)}\n` +
294
+ `node kits/migrations/migrate-contact-book-v2.mjs --apply --home ${JSON.stringify(root)}`, { issues });
295
+ }
296
+ function inspectContactBookDomain(contactFile, agentFile, issues) {
297
+ let contact;
298
+ let agent;
299
+ try {
300
+ contact = JSON.parse(fs.readFileSync(contactFile, 'utf8'));
301
+ agent = JSON.parse(fs.readFileSync(agentFile, 'utf8'));
302
+ }
303
+ catch {
304
+ return;
305
+ }
306
+ const owners = new Set(Array.isArray(agent?.owners) ? agent.owners.map(String) : []);
307
+ const configuredChannelKeys = new Set((Array.isArray(agent?.channels) ? agent.channels : [])
308
+ .filter((channel) => channel && typeof channel === 'object' && channel.type !== 'aun')
309
+ .map((channel) => {
310
+ const type = String(channel.type || '').trim().toLowerCase();
311
+ const name = String(channel.name || '').trim();
312
+ return type && name ? `${type}#${String(agent?.aid || '').trim()}#${name}` : '';
313
+ })
314
+ .filter(Boolean));
315
+ const aliases = new Map();
316
+ for (const [primaryId, entry] of Object.entries(contact?.contacts ?? {})) {
317
+ if (isExplicitGroupId(primaryId)) {
318
+ issues.push({ file: contactFile, detail: `group contact ${primaryId} is not supported` });
319
+ }
320
+ if (entry?.status === 'blocked' && owners.has(primaryId)) {
321
+ issues.push({ file: contactFile, detail: `owner contact ${primaryId} cannot be blocked` });
322
+ }
323
+ for (const alias of Array.isArray(entry?.aliases) ? entry.aliases : []) {
324
+ const parsed = parseContactAlias(alias, agent?.aid);
325
+ if (!parsed) {
326
+ issues.push({ file: contactFile, detail: `invalid Contact alias ${String(alias)}; expected <channelKey>:<urlEncode(peerId)>` });
327
+ continue;
328
+ }
329
+ if (!configuredChannelKeys.has(parsed.channelKey)) {
330
+ issues.push({ file: contactFile, detail: `alias ${alias} references an unconfigured channel instance` });
331
+ continue;
332
+ }
333
+ const previous = aliases.get(parsed.alias);
334
+ if (previous && previous !== primaryId) {
335
+ issues.push({ file: contactFile, detail: `alias ${parsed.alias} is assigned to both ${previous} and ${primaryId}` });
336
+ }
337
+ else {
338
+ aliases.set(parsed.alias, primaryId);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ function hasContactRoleAuthorityReceipt(file) {
344
+ try {
345
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
346
+ return value?.migration === 'contact-book-v2-relation-role-authority' && value?.designVersion === 2;
347
+ }
348
+ catch {
349
+ return false;
350
+ }
351
+ }
352
+ function findRelationRoleRemovedByExperimentalMigration(root) {
353
+ const backupsRoot = path.join(root, 'backups', 'contact-book-v2-migration');
354
+ if (!fs.existsSync(backupsRoot))
355
+ return null;
356
+ for (const stamp of fs.readdirSync(backupsRoot, { withFileTypes: true })) {
357
+ if (!stamp.isDirectory())
358
+ continue;
359
+ try {
360
+ const manifest = JSON.parse(fs.readFileSync(path.join(backupsRoot, stamp.name, 'manifest.json'), 'utf8'));
361
+ if (manifest?.designVersion >= 2)
362
+ continue;
363
+ }
364
+ catch { }
365
+ const agentsRoot = path.join(backupsRoot, stamp.name, 'files', 'agents');
366
+ if (!fs.existsSync(agentsRoot))
367
+ continue;
368
+ for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
369
+ if (!agent.isDirectory() || !isValidAid(agent.name))
370
+ continue;
371
+ const relationsRoot = path.join(agentsRoot, agent.name, 'relations');
372
+ if (!fs.existsSync(relationsRoot))
373
+ continue;
374
+ for (const relation of fs.readdirSync(relationsRoot, { withFileTypes: true })) {
375
+ if (!relation.isDirectory() || relation.name.startsWith('_'))
376
+ continue;
377
+ const backupFile = path.join(relationsRoot, relation.name, 'config.json');
378
+ if (!fs.existsSync(backupFile))
379
+ continue;
380
+ let backupValue;
381
+ try {
382
+ backupValue = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
383
+ }
384
+ catch {
385
+ continue;
386
+ }
387
+ if (typeof backupValue?.role !== 'string' || backupValue.role === 'owner')
388
+ continue;
389
+ const currentFile = path.join(root, 'agents', agent.name, 'relations', relation.name, 'config.json');
390
+ let currentValue;
391
+ try {
392
+ currentValue = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
393
+ }
394
+ catch {
395
+ continue;
396
+ }
397
+ if (currentValue?.$schema_version === 5) {
398
+ return currentFile;
399
+ }
400
+ }
401
+ }
402
+ }
403
+ return null;
404
+ }
204
405
  /**
205
406
  * 启动时检查 schema 版本(daemon 启动时调用)。
206
407
  * 扫描所有配置文件,如有版本不匹配则警告一次。
@@ -459,6 +660,10 @@ function finishConfigWrite(target, sel, file) {
459
660
  }
460
661
  }
461
662
  }
663
+ /** Notify cache and security listeners after a caller-owned atomic transaction commits. */
664
+ export function notifyConfigWrite(target, sel) {
665
+ finishConfigWrite(target, sel, targetPath(target, sel));
666
+ }
462
667
  function ensureSchemaVersion(value, version) {
463
668
  return value && typeof value === 'object'
464
669
  ? { ...value, $schema_version: version }
@@ -643,7 +848,6 @@ export function resolveEffective(sel, opts = {}) {
643
848
  lifecycle: config.lifecycle,
644
849
  initialized: config.initialized,
645
850
  owners: config.owners,
646
- admins: config.admins,
647
851
  aun: config.aun,
648
852
  channels: config.channels ?? [],
649
853
  models: config.models,
@@ -0,0 +1,68 @@
1
+ import { isValidAid } from '../aun/aid/validation.js';
2
+ const CHANNEL_SEGMENT = /^[A-Za-z0-9_-]+$/;
3
+ /** Parse the canonical channel instance key used by Contact aliases. */
4
+ export function parseContactChannelKey(value) {
5
+ if (typeof value !== 'string')
6
+ return null;
7
+ const text = value.trim();
8
+ const parts = text.split('#');
9
+ if (parts.length !== 3)
10
+ return null;
11
+ const [rawType, selfAid, channelName] = parts;
12
+ const channelType = rawType.toLowerCase();
13
+ if (!CHANNEL_SEGMENT.test(channelType)
14
+ || rawType !== channelType
15
+ || !isValidAid(selfAid)
16
+ || !CHANNEL_SEGMENT.test(channelName))
17
+ return null;
18
+ return {
19
+ channelType,
20
+ selfAid,
21
+ channelName,
22
+ channelKey: `${channelType}#${selfAid}#${channelName}`,
23
+ };
24
+ }
25
+ /** Validate that a runtime channelKey belongs to the expected Agent and channel type. */
26
+ export function normalizeContactChannelKey(selfAid, channelType, channelKey) {
27
+ const parsed = parseContactChannelKey(channelKey);
28
+ const expectedType = String(channelType || '').trim().toLowerCase();
29
+ const expectedSelf = String(selfAid || '').trim();
30
+ if (!parsed || parsed.channelType !== expectedType || parsed.selfAid !== expectedSelf)
31
+ return null;
32
+ return parsed.channelKey;
33
+ }
34
+ /** Format `<channelKey>:<urlEncode(peerId)>`. */
35
+ export function formatContactAlias(channelKey, peerId) {
36
+ const parsed = parseContactChannelKey(channelKey);
37
+ const peer = String(peerId || '').trim();
38
+ if (!parsed || parsed.channelType === 'aun' || !peer) {
39
+ throw new Error(`Invalid Contact alias input: ${String(channelKey)}:${String(peerId)}`);
40
+ }
41
+ return `${parsed.channelKey}:${encodeURIComponent(peer)}`;
42
+ }
43
+ /** Parse and canonicalize a stored Contact alias. Non-canonical URL encoding is rejected. */
44
+ export function parseContactAlias(value, expectedSelfAid) {
45
+ if (typeof value !== 'string')
46
+ return null;
47
+ const text = value.trim();
48
+ const separator = text.indexOf(':');
49
+ if (separator <= 0 || separator === text.length - 1)
50
+ return null;
51
+ const parsedChannel = parseContactChannelKey(text.slice(0, separator));
52
+ if (!parsedChannel || parsedChannel.channelType === 'aun')
53
+ return null;
54
+ if (expectedSelfAid !== undefined && parsedChannel.selfAid !== String(expectedSelfAid || '').trim())
55
+ return null;
56
+ const encodedPeerId = text.slice(separator + 1);
57
+ let peerId;
58
+ try {
59
+ peerId = decodeURIComponent(encodedPeerId);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ if (!peerId || encodeURIComponent(peerId) !== encodedPeerId)
65
+ return null;
66
+ const alias = `${parsedChannel.channelKey}:${encodedPeerId}`;
67
+ return { ...parsedChannel, peerId, alias };
68
+ }