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
@@ -18,6 +18,7 @@ import { buildSessionTurnList } from '../session/session-turns.js';
18
18
  import { resolveConfigCommand } from '../../config/resolved-config-op.js';
19
19
  import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
20
20
  import { hashArgv } from '../auth/authorization-audit.js';
21
+ import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
21
22
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
22
23
  import { validateModelSelectionForRole } from '../model/model-permission.js';
23
24
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
@@ -353,10 +354,13 @@ export class CommandHandler {
353
354
  validateTriggerRuntimeFields(definition, role, baseagent) {
354
355
  if (!role)
355
356
  return undefined;
357
+ const executionBaseagent = definition.execution.type === 'trigger_session'
358
+ ? definition.execution.baseagent ?? baseagent
359
+ : baseagent;
356
360
  if (definition.execution.model) {
357
361
  const modelDecision = validateModelSelectionForRole({
358
362
  role,
359
- baseagent,
363
+ baseagent: executionBaseagent,
360
364
  requestedModel: definition.execution.model,
361
365
  selfAid: definition.agentAid,
362
366
  });
@@ -364,7 +368,7 @@ export class CommandHandler {
364
368
  return modelDecision.message || '当前角色不允许使用该 Trigger 模型';
365
369
  }
366
370
  if (definition.execution.effort) {
367
- const base = baseagent || 'claude';
371
+ const base = executionBaseagent || 'claude';
368
372
  const effortDecision = validateRuntimeStringFieldOverride({
369
373
  selfAid: definition.agentAid,
370
374
  role,
@@ -447,6 +451,29 @@ export class CommandHandler {
447
451
  const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid);
448
452
  if (permissionError)
449
453
  return { error: permissionError };
454
+ if (parsed.baseagent !== undefined) {
455
+ if (typeof parsed.baseagent !== 'string' || !parsed.baseagent.trim()) {
456
+ return { error: 'Trigger baseagent 不能为空' };
457
+ }
458
+ if (parsed.executionType !== 'trigger_session') {
459
+ return { error: 'Trigger baseagent 仅适用于 trigger_session' };
460
+ }
461
+ }
462
+ const triggerOwner = this.agentRegistry?.get?.(schedulerAid) ?? this.getOwningAgent(channel);
463
+ const executionBaseagent = parsed.executionType === 'trigger_session'
464
+ ? parsed.baseagent?.trim() ?? triggerOwner?.baseagent ?? triggerRole.baseagent
465
+ : undefined;
466
+ if (parsed.executionType === 'trigger_session') {
467
+ if (!executionBaseagent)
468
+ return { error: '无法确定 Trigger 会话的 baseagent' };
469
+ const ownerName = triggerOwner?.name || '<unknown>';
470
+ const available = this.getAvailableBaseagentsForOwner(ownerName);
471
+ if (!available.includes(executionBaseagent)) {
472
+ return {
473
+ error: `Trigger baseagent 不可用:${executionBaseagent}(可用:${available.join(' / ') || '无'})`,
474
+ };
475
+ }
476
+ }
450
477
  const definition = {
451
478
  $schema_version: 3.1,
452
479
  id,
@@ -460,7 +487,10 @@ export class CommandHandler {
460
487
  execution: {
461
488
  type: parsed.executionType,
462
489
  ...(script ? { script } : { prompt: parsed.prompt || '' }),
463
- ...(parsed.executionType === 'trigger_session' ? { thread: parsed.triggerThread ?? 'by_trigger' } : {}),
490
+ ...(parsed.executionType === 'trigger_session' ? {
491
+ thread: parsed.triggerThread ?? 'by_trigger',
492
+ baseagent: executionBaseagent,
493
+ } : {}),
464
494
  model: parsed.model,
465
495
  effort: parsed.effort,
466
496
  ...(permissionMode !== undefined ? { permissionMode } : {}),
@@ -955,9 +985,9 @@ export class CommandHandler {
955
985
  /**
956
986
  * 主命令处理入口
957
987
  */
958
- async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject) {
988
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
959
989
  try {
960
- const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject);
990
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
961
991
  return result;
962
992
  }
963
993
  catch (error) {
@@ -968,8 +998,8 @@ export class CommandHandler {
968
998
  throw error;
969
999
  }
970
1000
  }
971
- async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject) {
972
- return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject);
1001
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
1002
+ return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
973
1003
  }
974
1004
  async handleTriggerCommand(scheduler, content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId) {
975
1005
  if (content === '/trigger') {
@@ -1117,8 +1147,13 @@ export class CommandHandler {
1117
1147
  const nextStr = updated.trigger.nextFireAt ? new Date(updated.trigger.nextFireAt).toLocaleString() : '未计算';
1118
1148
  return `✅ 触发器已更新:**${updated.trigger.name}**\n下次触发:${nextStr}`;
1119
1149
  }
1120
- if (sub.startsWith('set ')) {
1121
- const args = sub.slice('set '.length);
1150
+ const createSubcommand = sub.startsWith('create ')
1151
+ ? 'create'
1152
+ : sub.startsWith('set ')
1153
+ ? 'set'
1154
+ : undefined;
1155
+ if (createSubcommand) {
1156
+ const args = sub.slice(`${createSubcommand} `.length);
1122
1157
  const result = parseTriggerSet(args);
1123
1158
  if (!result.ok)
1124
1159
  return `❌ ${result.error}`;
@@ -1131,7 +1166,7 @@ export class CommandHandler {
1131
1166
  return `❌ 未知子命令。用法:
1132
1167
  /trigger — 查看活跃触发器
1133
1168
  /trigger list [--all] — 查看所有触发器
1134
- /trigger set <参数> — 注册触发器
1169
+ /trigger create <参数> — 创建触发器(set 为兼容别名)
1135
1170
  /trigger update <名称|ID> <参数> — 修改触发器
1136
1171
  常用参数:--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1137
1172
  /trigger enable <名称|ID> — 启用触发器
@@ -1477,6 +1512,66 @@ export class CommandHandler {
1477
1512
  ? { ok: true, result }
1478
1513
  : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
1479
1514
  }
1515
+ /** Agent-managed contact CLI entrypoint. The daemon owns actor resolution and authorization. */
1516
+ async handleContactOperation(argv, sessionId, delegationToken) {
1517
+ if (!this.agentDelegationRegistry) {
1518
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1519
+ }
1520
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
1521
+ if (!delegation.ok)
1522
+ return { ok: false, code: delegation.code, error: delegation.reason };
1523
+ const session = await this.sessionManager.getSessionById(sessionId);
1524
+ if (!session)
1525
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1526
+ const grant = delegation.grant;
1527
+ let conversationId;
1528
+ try {
1529
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1530
+ }
1531
+ catch {
1532
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1533
+ }
1534
+ const resolved = resolveContactCommand(argv);
1535
+ if (!resolved.ok)
1536
+ return { ok: false, code: resolved.code, error: resolved.reason };
1537
+ if (resolved.command.selfAid !== grant.selfAid) {
1538
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent contact book can be targeted' };
1539
+ }
1540
+ const subject = buildAuthSubject({
1541
+ selfAid: grant.selfAid,
1542
+ actorId: grant.actorId,
1543
+ channel: grant.channel,
1544
+ channelType: grant.channelType,
1545
+ channelId: conversationId,
1546
+ chatType: grant.chatType,
1547
+ conversationId,
1548
+ processOwners: loadDaemonConfig().owners ?? [],
1549
+ });
1550
+ const decision = authorizeOperation({
1551
+ source: 'agent-tool',
1552
+ subject,
1553
+ intent: {
1554
+ operation: resolved.command.operationId,
1555
+ scope: 'agent',
1556
+ source: 'agent-tool',
1557
+ args: {
1558
+ self: resolved.command.selfAid,
1559
+ ...('primaryId' in resolved.command ? { primaryId: resolved.command.primaryId } : {}),
1560
+ },
1561
+ },
1562
+ auditMetadata: {
1563
+ argvHash: hashArgv(resolved.command.canonicalArgv),
1564
+ taskId: grant.taskId,
1565
+ messageId: grant.messageId,
1566
+ },
1567
+ });
1568
+ if (!decision.allow)
1569
+ return { ok: false, code: decision.code, error: decision.reason };
1570
+ const result = await executeResolvedContactCommand(resolved.command, grant.actorId);
1571
+ return result.ok
1572
+ ? { ok: true, result }
1573
+ : { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
1574
+ }
1480
1575
  setAgentDelegationRegistry(registry) {
1481
1576
  this.agentDelegationRegistry = registry;
1482
1577
  }
@@ -0,0 +1,374 @@
1
+ import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
2
+ import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
3
+ import { mutateContactBook } from '../../config/contact-book-store.js';
4
+ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
5
+ /**
6
+ * Connect Menu — 联系人与关系管理协议
7
+ *
8
+ * 定位:role menu 之上的**薄委派层**。
9
+ * - 角色分配 / admin(assignments、resource=assignment/admin)→ 委派给 role-menu,
10
+ * 复用其 agent 级互斥锁、CAS revision、owner/admin 安全护栏与审计链,绝不自行写 relation 文件。
11
+ * - 群组列表(view=groups)→ 委派给 role-menu 的 view=targets(数据源是 AUN live 目录),
12
+ * 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
13
+ * - 联系人(view=contacts / contact,add / block / unblock)→ 直接读写 Contact Book v2,
14
+ * contact.json 是联系人列表的唯一事实来源。
15
+ */
16
+ const MENU_NAME = 'connect';
17
+ const DEFAULT_PAGE_SIZE = 50;
18
+ const MAX_PAGE_SIZE = 200;
19
+ /**
20
+ * Map a connect menu request to its authorization operation id.
21
+ * Read views/options → connect.read; contact writes → connect.write;
22
+ * role assignment/admin delegate to the role.* operations role-menu enforces.
23
+ */
24
+ export function connectMenuOperation(kind, args, value) {
25
+ if (kind === 'options' || kind === 'query')
26
+ return 'connect.read';
27
+ if (kind === 'action')
28
+ return 'connect.write';
29
+ const resource = String(args?.resource ?? '');
30
+ if (resource === 'admin')
31
+ return 'connect.admin';
32
+ const decoded = decodeUpdateValue(value);
33
+ if (decoded === 'admin')
34
+ return 'connect.admin';
35
+ return decoded === null || decoded === false || decoded === 'revoke'
36
+ ? 'connect.revoke'
37
+ : 'connect.assign';
38
+ }
39
+ // ── Menu Entry ──
40
+ export async function handleConnectMenu(req, context) {
41
+ const { self } = context;
42
+ if (!self)
43
+ return fail('INVALID_ARGUMENT', 'selfAid is required for connect menu');
44
+ if (req.args?.self !== self) {
45
+ return fail(req.args && Object.prototype.hasOwnProperty.call(req.args, 'self') ? 'PERMISSION_DENIED' : 'INVALID_ARGUMENT', req.args && Object.prototype.hasOwnProperty.call(req.args, 'self')
46
+ ? 'args.self does not match the receiving Agent'
47
+ : 'args.self is required for connect menu');
48
+ }
49
+ try {
50
+ if (req.subtype === 'options')
51
+ return await handleOptions(self);
52
+ if (req.subtype === 'query')
53
+ return await handleQuery(req, self, context);
54
+ if (req.subtype === 'update')
55
+ return await handleUpdate(req, context);
56
+ if (req.subtype === 'action')
57
+ return await handleAction(req, self, context);
58
+ }
59
+ catch (error) {
60
+ return fail(errorCode(error), errorMessage(error), errorData(error));
61
+ }
62
+ return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
63
+ }
64
+ // ── Options ──
65
+ async function handleOptions(self) {
66
+ const snapshot = getContactSnapshot(self);
67
+ const contactCount = snapshot.contacts.size;
68
+ const blockedCount = snapshot.blockedIndex.size;
69
+ return ok({
70
+ options: [
71
+ {
72
+ option: 'contacts',
73
+ label: '联系人列表',
74
+ description: `查看所有联系人(${contactCount} 人,${blockedCount} 人已拉黑)`,
75
+ },
76
+ {
77
+ option: 'groups',
78
+ label: '群组列表',
79
+ description: '查看所在群组',
80
+ },
81
+ ],
82
+ });
83
+ }
84
+ // ── Query ──
85
+ async function handleQuery(req, self, context) {
86
+ const args = req.args ?? {};
87
+ const view = String(args.view ?? '');
88
+ if (view === 'contacts')
89
+ return queryContacts(args, self);
90
+ if (view === 'contact')
91
+ return queryContact(req, self);
92
+ // Groups and role assignments are delegated to role-menu (authoritative source).
93
+ // Role *definitions* are out of scope here — use `menu name=role` for those.
94
+ if (view === 'groups') {
95
+ const data = await roleMenuQuery(context, { ...args, view: 'targets', targetType: 'group' });
96
+ return ok({
97
+ self,
98
+ view: 'groups',
99
+ items: (data.items ?? []).map((item) => ({
100
+ groupId: item.groupId,
101
+ peerKey: item.peerKey,
102
+ displayName: item.displayName,
103
+ status: item.status,
104
+ memberCount: item.memberCount,
105
+ roleId: item.explicitDefaultRole ?? null,
106
+ effectiveRoleId: item.baselineRole ?? null,
107
+ effectiveRoleSource: item.baselineRoleSource ?? null,
108
+ relationRevision: item.relationRevision,
109
+ })),
110
+ nextCursor: data.nextCursor ?? null,
111
+ total: data.total,
112
+ dataSource: data.dataSource,
113
+ });
114
+ }
115
+ if (view === 'assignments') {
116
+ const data = await roleMenuQuery(context, { ...args, view: 'assignments', includeDefaults: false });
117
+ return ok({
118
+ self,
119
+ view: 'assignments',
120
+ ...(args.roleId === undefined ? {} : { roleId: args.roleId }),
121
+ policyRevision: data.policyRevision,
122
+ items: (data.items ?? []).map(normalizeAssignmentItem),
123
+ nextCursor: data.nextCursor ?? null,
124
+ dataSource: { ...data.dataSource, kind: 'relation-config' },
125
+ });
126
+ }
127
+ return fail('INVALID_ARGUMENT', `Unknown view: ${String(view)}`);
128
+ }
129
+ function queryContacts(args, self) {
130
+ const snapshot = getContactSnapshot(self);
131
+ const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
132
+ const items = page.items.map(primaryId => contactItem(self, primaryId));
133
+ return ok({
134
+ self,
135
+ view: 'contacts',
136
+ items,
137
+ contactRevision: snapshot.contactRevision,
138
+ nextCursor: page.nextCursor,
139
+ dataSource: { kind: 'contact-book', authoritative: true, complete: true },
140
+ });
141
+ }
142
+ function queryContact(req, self) {
143
+ const primaryId = String(req.args?.primaryId ?? '').trim();
144
+ if (!primaryId)
145
+ return fail('INVALID_ARGUMENT', 'primaryId is required');
146
+ const snapshot = getContactSnapshot(self);
147
+ if (!snapshot.contacts.has(primaryId))
148
+ return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
149
+ return ok({ self, view: 'contact', ...contactItem(self, primaryId), contactRevision: snapshot.contactRevision });
150
+ }
151
+ function contactItem(self, primaryId) {
152
+ const view = resolveContactView(self, primaryId);
153
+ return {
154
+ primaryId,
155
+ displayName: view.displayName ?? null,
156
+ status: view.status,
157
+ blocked: view.blocked,
158
+ isOwner: view.isOwner,
159
+ aliases: view.aliases,
160
+ };
161
+ }
162
+ function normalizeAssignmentItem(item) {
163
+ const peerKey = item?.target?.peerKey;
164
+ if (typeof peerKey !== 'string')
165
+ return item;
166
+ try {
167
+ const { channelType, channelId } = parsePeerKey(peerKey);
168
+ if (channelType === 'aun')
169
+ return item;
170
+ return {
171
+ ...item,
172
+ target: {
173
+ kind: item.target.kind,
174
+ channelType,
175
+ channelId,
176
+ peerKey,
177
+ },
178
+ };
179
+ }
180
+ catch {
181
+ return item;
182
+ }
183
+ }
184
+ // ── Update (delegated to role-menu) ──
185
+ async function handleUpdate(req, context) {
186
+ const args = req.args ?? {};
187
+ const resource = args.resource;
188
+ if (resource !== 'assignment' && resource !== 'admin') {
189
+ return fail('INVALID_ARGUMENT', `Unknown resource: ${String(resource)}`);
190
+ }
191
+ if (req.value === undefined || !req.value.trim()) {
192
+ return fail('INVALID_ARGUMENT', 'value is required');
193
+ }
194
+ validateWritableTarget(args.target);
195
+ const value = roleMenuUpdateValue(resource, req.value);
196
+ const data = await roleMenuUpdate(context, args, value);
197
+ return ok(data);
198
+ }
199
+ // ── Action (Contact Book v2) ──
200
+ async function handleAction(req, self, context) {
201
+ const action = req.action;
202
+ if (action === 'add')
203
+ return actionAdd(req, self, context);
204
+ if (action === 'block')
205
+ return actionSetStatus(req, self, context, true);
206
+ if (action === 'unblock')
207
+ return actionSetStatus(req, self, context, false);
208
+ return fail('INVALID_ARGUMENT', `Unknown action: ${String(action)}`);
209
+ }
210
+ async function actionAdd(req, self, context) {
211
+ const args = req.args ?? {};
212
+ const primaryId = String(args.primaryId ?? '').trim();
213
+ if (!primaryId)
214
+ return fail('INVALID_ARGUMENT', 'primaryId is required');
215
+ const expectedContactRevision = requireContactRevision(args);
216
+ const hasDisplayName = Object.prototype.hasOwnProperty.call(args, 'displayName');
217
+ if (hasDisplayName && args.displayName !== null && typeof args.displayName !== 'string') {
218
+ return fail('INVALID_ARGUMENT', 'displayName must be a string or null');
219
+ }
220
+ const current = getContactSnapshot(self).contacts.get(primaryId);
221
+ const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
222
+ const result = await mutateContactBook({
223
+ selfAid: self,
224
+ actor: context.actorAid ?? 'connect-menu',
225
+ expectedContactRevision,
226
+ mutation: { type: 'set-display-name', primaryId, displayName },
227
+ });
228
+ const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
229
+ return ok({
230
+ self,
231
+ action: 'add',
232
+ primaryId,
233
+ displayName: storedDisplayName,
234
+ changed: result.changed,
235
+ contactRevision: result.contactRevision,
236
+ });
237
+ }
238
+ async function actionSetStatus(req, self, context, blocked) {
239
+ const args = req.args ?? {};
240
+ const primaryId = String(args.primaryId ?? '').trim();
241
+ if (!primaryId)
242
+ return fail('INVALID_ARGUMENT', 'primaryId is required');
243
+ const expectedContactRevision = requireContactRevision(args);
244
+ const result = await mutateContactBook({
245
+ selfAid: self,
246
+ actor: context.actorAid ?? 'connect-menu',
247
+ expectedContactRevision,
248
+ mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
249
+ });
250
+ return ok({
251
+ self,
252
+ action: blocked ? 'block' : 'unblock',
253
+ primaryId,
254
+ changed: result.changed,
255
+ contactRevision: result.contactRevision,
256
+ });
257
+ }
258
+ // ── Helpers ──
259
+ function ok(data) {
260
+ return { type: 'menu.response', id: '', name: MENU_NAME, data };
261
+ }
262
+ function fail(code, message, data) {
263
+ return {
264
+ type: 'menu.response',
265
+ id: '',
266
+ name: MENU_NAME,
267
+ error: { code, message, ...(data === undefined ? {} : { data }) },
268
+ };
269
+ }
270
+ function errorCode(error) {
271
+ const code = error?.code;
272
+ if (code === 'INVALID_AID')
273
+ return 'INVALID_ARGUMENT';
274
+ if (code === 'OWNER_PROTECTED')
275
+ return 'NOT_ALLOWED';
276
+ if (code === 'ALIAS_CONFLICT')
277
+ return 'CONFLICT';
278
+ if (code === 'INVALID_CONTACT_BOOK')
279
+ return 'INTERNAL_ERROR';
280
+ return typeof code === 'string' ? code : 'INTERNAL_ERROR';
281
+ }
282
+ function errorMessage(error) {
283
+ if (error instanceof Error)
284
+ return error.message;
285
+ const message = error?.message;
286
+ return typeof message === 'string' ? message : String(error);
287
+ }
288
+ function errorData(error) {
289
+ return error && typeof error === 'object' ? error.data : undefined;
290
+ }
291
+ function requireContactRevision(args) {
292
+ if (!Object.prototype.hasOwnProperty.call(args, 'expectedContactRevision')) {
293
+ throw connectError('INVALID_ARGUMENT', 'expectedContactRevision is required');
294
+ }
295
+ if (typeof args.expectedContactRevision !== 'string' || !args.expectedContactRevision) {
296
+ throw connectError('INVALID_ARGUMENT', 'expectedContactRevision must be a non-empty string');
297
+ }
298
+ return args.expectedContactRevision;
299
+ }
300
+ function decodeUpdateValue(value) {
301
+ const text = String(value ?? '').trim();
302
+ if (!text)
303
+ return undefined;
304
+ try {
305
+ const decoded = JSON.parse(text);
306
+ if (decoded === null || typeof decoded === 'string' || typeof decoded === 'boolean')
307
+ return decoded;
308
+ }
309
+ catch { }
310
+ return text;
311
+ }
312
+ function roleMenuUpdateValue(resource, value) {
313
+ const decoded = decodeUpdateValue(value);
314
+ if (resource === 'assignment') {
315
+ if (decoded !== null && typeof decoded !== 'string') {
316
+ throw connectError('INVALID_ARGUMENT', 'assignment value must be a role ID or null');
317
+ }
318
+ return JSON.stringify(decoded);
319
+ }
320
+ if (![true, false, null, 'grant', 'revoke'].includes(decoded)) {
321
+ throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
322
+ }
323
+ return JSON.stringify(decoded);
324
+ }
325
+ function validateWritableTarget(target) {
326
+ if (!target || typeof target !== 'object' || Array.isArray(target))
327
+ return;
328
+ const value = target;
329
+ if (typeof value.channelType === 'string' && value.channelType !== 'aun') {
330
+ throw connectError('NOT_SUPPORTED', 'Native non-AUN assignment writes are not supported in Phase 1');
331
+ }
332
+ const kind = value.kind;
333
+ const logicalId = kind === 'private' ? value.peerAid : kind === 'group' ? value.groupId : undefined;
334
+ if (value.peerKey === undefined || typeof logicalId !== 'string')
335
+ return;
336
+ const expected = formatPeerKey('aun', logicalId);
337
+ if (value.peerKey !== expected) {
338
+ throw connectError('INVALID_ARGUMENT', 'target.peerKey does not match the logical target', {
339
+ expectedPeerKey: expected,
340
+ peerKey: value.peerKey,
341
+ });
342
+ }
343
+ }
344
+ function paginate(items, args, self, key) {
345
+ const requestedLimit = Number(args.limit ?? DEFAULT_PAGE_SIZE);
346
+ const limit = Number.isInteger(requestedLimit) && requestedLimit > 0
347
+ ? Math.min(requestedLimit, MAX_PAGE_SIZE)
348
+ : DEFAULT_PAGE_SIZE;
349
+ let start = 0;
350
+ if (args.cursor !== undefined && args.cursor !== null) {
351
+ try {
352
+ const decoded = JSON.parse(Buffer.from(String(args.cursor), 'base64url').toString('utf8'));
353
+ if (!decoded || typeof decoded.after !== 'string')
354
+ throw new Error();
355
+ start = items.findIndex(item => key(item) > decoded.after);
356
+ if (start < 0)
357
+ start = items.length;
358
+ }
359
+ catch {
360
+ throw connectError('INVALID_ARGUMENT', 'cursor is invalid', { self, field: 'cursor' });
361
+ }
362
+ }
363
+ const page = items.slice(start, start + limit);
364
+ const hasMore = start + page.length < items.length;
365
+ return {
366
+ items: page,
367
+ nextCursor: hasMore && page.length
368
+ ? Buffer.from(JSON.stringify({ after: key(page[page.length - 1]) }), 'utf8').toString('base64url')
369
+ : null,
370
+ };
371
+ }
372
+ function connectError(code, message, data) {
373
+ return Object.assign(new Error(message), { code, ...(data === undefined ? {} : { data }) });
374
+ }