evolcore 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -1,6 +1,7 @@
1
1
  import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
2
2
  import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
3
- import { mutateContactBook } from '../../config/contact-book-store.js';
3
+ import { listConfiguredChannelKeys, mutateAccessPolicy, readAccessPolicySnapshot, } from '../../config/access-policy.js';
4
+ import { expirePendingContactRequests, mutateContactWithOperation, reviewContactRequest, } from '../../config/contact-request-service.js';
4
5
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
5
6
  /**
6
7
  * Connect Menu — 联系人与关系管理协议
@@ -10,8 +11,10 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
10
11
  * 复用其 agent 级互斥锁、CAS revision、owner/admin 安全护栏与审计链,绝不自行写 relation 文件。
11
12
  * - 群组列表(view=groups)→ 委派给 role-menu 的 view=targets(数据源是 AUN live 目录),
12
13
  * 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
13
- * - 联系人(view=contacts / contact,add / block / unblock)→ 直接读写 Contact Book v2,
14
- * contact.json 是联系人列表的唯一事实来源。
14
+ * - 联系人与申请(view=contacts / contact,add / block / unblock / review)→ 直接读写
15
+ * Contact Book v3;contact.json 是当前状态和 pending 版本的唯一事实来源。
16
+ * - 访问设置(view=access / option=allowedGroupChannels / resource=<access field>)→
17
+ * 查询策略、列举群聊渠道候选项并逐字段写入 Agent config.json 的 access 字段。
15
18
  */
16
19
  const MENU_NAME = 'connect';
17
20
  const DEFAULT_PAGE_SIZE = 50;
@@ -19,6 +22,7 @@ const MAX_PAGE_SIZE = 200;
19
22
  /**
20
23
  * Map a connect menu request to its authorization operation id.
21
24
  * Read views/options → connect.read; contact writes → connect.write;
25
+ * owner-only access fields → connect.access.write;
22
26
  * role assignment/admin delegate to the role.* operations role-menu enforces.
23
27
  */
24
28
  export function connectMenuOperation(kind, args, value) {
@@ -27,6 +31,11 @@ export function connectMenuOperation(kind, args, value) {
27
31
  if (kind === 'action')
28
32
  return 'connect.write';
29
33
  const resource = String(args?.resource ?? '');
34
+ if (resource === 'allowedGroupChannels')
35
+ return 'connect.write';
36
+ if (resource === 'policyMode' || resource === 'requestLimit') {
37
+ return 'connect.access.write';
38
+ }
30
39
  if (resource === 'admin')
31
40
  return 'connect.admin';
32
41
  const decoded = decodeUpdateValue(value);
@@ -41,14 +50,12 @@ export async function handleConnectMenu(req, context) {
41
50
  const { self } = context;
42
51
  if (!self)
43
52
  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');
53
+ if (req.args?.self !== undefined && req.args.self !== self) {
54
+ return fail('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
48
55
  }
49
56
  try {
50
57
  if (req.subtype === 'options')
51
- return await handleOptions(self);
58
+ return await handleOptions(self, req.args);
52
59
  if (req.subtype === 'query')
53
60
  return await handleQuery(req, self, context);
54
61
  if (req.subtype === 'update')
@@ -62,22 +69,43 @@ export async function handleConnectMenu(req, context) {
62
69
  return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
63
70
  }
64
71
  // ── Options ──
65
- async function handleOptions(self) {
72
+ async function handleOptions(self, args) {
73
+ const option = String(args?.option ?? '').trim();
74
+ if (option) {
75
+ if (option !== 'allowedGroupChannels') {
76
+ return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
77
+ }
78
+ const selected = new Set(readAccessPolicySnapshot(self).policy.allowedGroupChannels);
79
+ return ok({
80
+ options: listConfiguredChannelKeys(self).map(channelKey => ({
81
+ option: channelKey,
82
+ label: channelKey,
83
+ selected: selected.has(channelKey),
84
+ })),
85
+ });
86
+ }
87
+ await expirePendingContactRequests(self);
66
88
  const snapshot = getContactSnapshot(self);
67
89
  const contactCount = snapshot.contacts.size;
68
90
  const blockedCount = snapshot.blockedIndex.size;
91
+ const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
69
92
  return ok({
70
93
  options: [
71
94
  {
72
95
  option: 'contacts',
73
96
  label: '联系人列表',
74
- description: `查看所有联系人(${contactCount} 人,${blockedCount} 人已拉黑)`,
97
+ description: `查看所有状态(${contactCount} 人,${pendingCount} 个待审核,${blockedCount} 人已拉黑)`,
75
98
  },
76
99
  {
77
100
  option: 'groups',
78
101
  label: '群组列表',
79
102
  description: '查看所在群组',
80
103
  },
104
+ {
105
+ option: 'access',
106
+ label: '访问设置',
107
+ description: '设置私聊准入模式与允许群聊的渠道',
108
+ },
81
109
  ],
82
110
  });
83
111
  }
@@ -89,6 +117,8 @@ async function handleQuery(req, self, context) {
89
117
  return queryContacts(args, self);
90
118
  if (view === 'contact')
91
119
  return queryContact(req, self);
120
+ if (view === 'access')
121
+ return queryAccess(self);
92
122
  // Groups and role assignments are delegated to role-menu (authoritative source).
93
123
  // Role *definitions* are out of scope here — use `menu name=role` for those.
94
124
  if (view === 'groups') {
@@ -124,9 +154,19 @@ async function handleQuery(req, self, context) {
124
154
  dataSource: { ...data.dataSource, kind: 'relation-config' },
125
155
  });
126
156
  }
127
- return fail('INVALID_ARGUMENT', `Unknown view: ${String(view)}`);
157
+ return fail('NOT_SUPPORTED', `Unknown view: ${String(view)}`);
128
158
  }
129
- function queryContacts(args, self) {
159
+ function queryAccess(self) {
160
+ const { policy, accessRevision } = readAccessPolicySnapshot(self);
161
+ return ok({
162
+ self,
163
+ view: 'access',
164
+ policy,
165
+ accessRevision,
166
+ });
167
+ }
168
+ async function queryContacts(args, self) {
169
+ await expirePendingContactRequests(self);
130
170
  const snapshot = getContactSnapshot(self);
131
171
  const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
132
172
  const items = page.items.map(primaryId => contactItem(self, primaryId));
@@ -135,11 +175,13 @@ function queryContacts(args, self) {
135
175
  view: 'contacts',
136
176
  items,
137
177
  contactRevision: snapshot.contactRevision,
178
+ pendingCount: [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length,
138
179
  nextCursor: page.nextCursor,
139
180
  dataSource: { kind: 'contact-book', authoritative: true, complete: true },
140
181
  });
141
182
  }
142
- function queryContact(req, self) {
183
+ async function queryContact(req, self) {
184
+ await expirePendingContactRequests(self);
143
185
  const primaryId = String(req.args?.primaryId ?? '').trim();
144
186
  if (!primaryId)
145
187
  return fail('INVALID_ARGUMENT', 'primaryId is required');
@@ -150,6 +192,7 @@ function queryContact(req, self) {
150
192
  }
151
193
  function contactItem(self, primaryId) {
152
194
  const view = resolveContactView(self, primaryId);
195
+ const pendingRequest = getContactSnapshot(self).contacts.get(primaryId)?.pendingRequest;
153
196
  return {
154
197
  primaryId,
155
198
  displayName: view.displayName ?? null,
@@ -157,6 +200,13 @@ function contactItem(self, primaryId) {
157
200
  blocked: view.blocked,
158
201
  isOwner: view.isOwner,
159
202
  aliases: view.aliases,
203
+ ...(pendingRequest ? {
204
+ pendingRequest: {
205
+ id: pendingRequest.id,
206
+ submittedAt: pendingRequest.submittedAt,
207
+ expiresAt: pendingRequest.expiresAt,
208
+ },
209
+ } : {}),
160
210
  };
161
211
  }
162
212
  function normalizeAssignmentItem(item) {
@@ -181,12 +231,15 @@ function normalizeAssignmentItem(item) {
181
231
  return item;
182
232
  }
183
233
  }
184
- // ── Update (delegated to role-menu) ──
234
+ // ── Update ──
185
235
  async function handleUpdate(req, context) {
186
236
  const args = req.args ?? {};
187
- const resource = args.resource;
237
+ const resource = String(args.resource ?? '');
238
+ if (isAccessUpdateResource(resource)) {
239
+ return updateAccessResource(req, context, resource);
240
+ }
188
241
  if (resource !== 'assignment' && resource !== 'admin') {
189
- return fail('INVALID_ARGUMENT', `Unknown resource: ${String(resource)}`);
242
+ return fail('NOT_SUPPORTED', `Unknown resource: ${String(resource)}`);
190
243
  }
191
244
  if (req.value === undefined || !req.value.trim()) {
192
245
  return fail('INVALID_ARGUMENT', 'value is required');
@@ -194,9 +247,9 @@ async function handleUpdate(req, context) {
194
247
  validateWritableTarget(args.target);
195
248
  const value = roleMenuUpdateValue(resource, req.value);
196
249
  const data = await roleMenuUpdate(context, args, value);
197
- return ok(data);
250
+ return ok(resource === 'assignment' ? normalizeAssignmentUpdate(data) : data);
198
251
  }
199
- // ── Action (Contact Book v2) ──
252
+ // ── Action (Contact Book v3) ──
200
253
  async function handleAction(req, self, context) {
201
254
  const action = req.action;
202
255
  if (action === 'add')
@@ -205,9 +258,111 @@ async function handleAction(req, self, context) {
205
258
  return actionSetStatus(req, self, context, true);
206
259
  if (action === 'unblock')
207
260
  return actionSetStatus(req, self, context, false);
208
- return fail('INVALID_ARGUMENT', `Unknown action: ${String(action)}`);
261
+ if (action === 'approve-request')
262
+ return actionReviewRequest(req, self, context, 'approve');
263
+ if (action === 'reject-request')
264
+ return actionReviewRequest(req, self, context, 'reject');
265
+ if (action === 'block-request')
266
+ return actionReviewRequest(req, self, context, 'block');
267
+ return fail('NOT_SUPPORTED', `Unknown action: ${String(action)}`);
268
+ }
269
+ function isAccessUpdateResource(resource) {
270
+ return resource === 'policyMode'
271
+ || resource === 'allowedGroupChannels'
272
+ || resource === 'requestLimit';
273
+ }
274
+ function updateAccessResource(req, context, resource) {
275
+ const args = req.args ?? {};
276
+ if (resource === 'allowedGroupChannels')
277
+ requireAdminContext(context);
278
+ else
279
+ requireOwnerContext(context);
280
+ if (req.value === undefined || !req.value.trim()) {
281
+ return fail('INVALID_ARGUMENT', 'value is required');
282
+ }
283
+ const self = context.self;
284
+ const expectedAccessRevision = requireAccessRevision(args);
285
+ const decoded = decodeAccessUpdateValue(req.value);
286
+ if (resource === 'policyMode') {
287
+ if (typeof decoded !== 'string' || !['open', 'contacts', 'owners-only'].includes(decoded)) {
288
+ return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners-only');
289
+ }
290
+ }
291
+ else if (resource === 'allowedGroupChannels') {
292
+ if (!Array.isArray(decoded)
293
+ || !decoded.every(value => typeof value === 'string' && value.length > 0)
294
+ || new Set(decoded).size !== decoded.length) {
295
+ return fail('INVALID_ARGUMENT', 'allowedGroupChannels must be a JSON string array');
296
+ }
297
+ }
298
+ else {
299
+ const requestLimit = decoded;
300
+ if (!requestLimit || typeof requestLimit !== 'object' || Array.isArray(requestLimit)
301
+ || Object.keys(requestLimit).some(key => key !== 'maxTimes' && key !== 'coolDown')
302
+ || !Number.isInteger(requestLimit.maxTimes)
303
+ || Number(requestLimit.maxTimes) < 1
304
+ || Number(requestLimit.maxTimes) > 100_000
305
+ || !Number.isInteger(requestLimit.coolDown)
306
+ || Number(requestLimit.coolDown) < 1
307
+ || Number(requestLimit.coolDown) > 86_400) {
308
+ return fail('INVALID_ARGUMENT', 'requestLimit must be a JSON object with integer maxTimes and coolDown');
309
+ }
310
+ }
311
+ const result = mutateAccessPolicy({
312
+ selfAid: self,
313
+ actorAid: String(context.actorAid || ''),
314
+ actorRole: context.actorRole,
315
+ resource: `access.${resource}`,
316
+ expectedAccessRevision,
317
+ update: current => accessPolicyWithUpdate(current, resource, decoded),
318
+ });
319
+ return ok({
320
+ self,
321
+ resource,
322
+ policy: result.policy,
323
+ accessRevision: result.accessRevision,
324
+ changed: result.changed,
325
+ });
326
+ }
327
+ async function actionReviewRequest(req, self, context, decision) {
328
+ const args = req.args ?? {};
329
+ const primaryId = String(args.primaryId ?? '').trim();
330
+ const requestId = String(args.requestId ?? '').trim();
331
+ if (!primaryId)
332
+ return fail('INVALID_ARGUMENT', 'primaryId is required');
333
+ if (!requestId)
334
+ return fail('INVALID_ARGUMENT', 'requestId is required');
335
+ requireAdminContext(context);
336
+ const expectedContactRevision = requireContactRevision(args);
337
+ const actorId = String(context.actorAid || '').trim();
338
+ const reviewed = await reviewContactRequest({
339
+ selfAid: self,
340
+ primaryId,
341
+ requestId,
342
+ expectedContactRevision,
343
+ approverId: actorId,
344
+ actorId,
345
+ reviewerRole: context.actorRole,
346
+ decision,
347
+ });
348
+ if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
349
+ return ok({
350
+ self,
351
+ action: `${decision}-request`,
352
+ primaryId,
353
+ requestId,
354
+ result: reviewed.code,
355
+ contactRevision: reviewed.contactRevision,
356
+ });
357
+ }
358
+ const code = reviewed.code === 'forbidden' ? 'PERMISSION_DENIED'
359
+ : reviewed.code === 'revision-conflict' ? 'CONFLICT'
360
+ : reviewed.code === 'request-expired' ? 'EXPIRED'
361
+ : 'NOT_FOUND';
362
+ return fail(code, reviewed.code.replace(/-/g, ' '), { contactRevision: reviewed.contactRevision });
209
363
  }
210
364
  async function actionAdd(req, self, context) {
365
+ requireAdminContext(context);
211
366
  const args = req.args ?? {};
212
367
  const primaryId = String(args.primaryId ?? '').trim();
213
368
  if (!primaryId)
@@ -219,11 +374,13 @@ async function actionAdd(req, self, context) {
219
374
  }
220
375
  const current = getContactSnapshot(self).contacts.get(primaryId);
221
376
  const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
222
- const result = await mutateContactBook({
377
+ const result = await mutateContactWithOperation({
223
378
  selfAid: self,
224
- actor: context.actorAid ?? 'connect-menu',
379
+ actorId: context.actorAid ?? 'connect-menu',
380
+ operation: 'manual-add',
225
381
  expectedContactRevision,
226
382
  mutation: { type: 'set-display-name', primaryId, displayName },
383
+ primaryId,
227
384
  });
228
385
  const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
229
386
  return ok({
@@ -236,16 +393,22 @@ async function actionAdd(req, self, context) {
236
393
  });
237
394
  }
238
395
  async function actionSetStatus(req, self, context, blocked) {
396
+ requireAdminContext(context);
239
397
  const args = req.args ?? {};
240
398
  const primaryId = String(args.primaryId ?? '').trim();
241
399
  if (!primaryId)
242
400
  return fail('INVALID_ARGUMENT', 'primaryId is required');
243
401
  const expectedContactRevision = requireContactRevision(args);
244
- const result = await mutateContactBook({
402
+ if (!getContactSnapshot(self).contacts.has(primaryId)) {
403
+ return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
404
+ }
405
+ const result = await mutateContactWithOperation({
245
406
  selfAid: self,
246
- actor: context.actorAid ?? 'connect-menu',
407
+ actorId: context.actorAid ?? 'connect-menu',
408
+ operation: blocked ? 'block' : 'unblock',
247
409
  expectedContactRevision,
248
410
  mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
411
+ primaryId,
249
412
  });
250
413
  return ok({
251
414
  self,
@@ -259,6 +422,16 @@ async function actionSetStatus(req, self, context, blocked) {
259
422
  function ok(data) {
260
423
  return { type: 'menu.response', id: '', name: MENU_NAME, data };
261
424
  }
425
+ function requireOwnerContext(context) {
426
+ if (context.actorRole !== 'owner') {
427
+ throw connectError('PERMISSION_DENIED', 'Only an Agent Owner can modify this access setting');
428
+ }
429
+ }
430
+ function requireAdminContext(context) {
431
+ if (context.actorRole !== 'owner' && context.actorRole !== 'admin') {
432
+ throw connectError('PERMISSION_DENIED', 'Only an Agent Owner or Admin can perform this operation');
433
+ }
434
+ }
262
435
  function fail(code, message, data) {
263
436
  return {
264
437
  type: 'menu.response',
@@ -277,6 +450,20 @@ function errorCode(error) {
277
450
  return 'CONFLICT';
278
451
  if (code === 'INVALID_CONTACT_BOOK')
279
452
  return 'INTERNAL_ERROR';
453
+ if (code === 'access_policy_owner_required')
454
+ return 'PERMISSION_DENIED';
455
+ if (code === 'access_policy_manager_required')
456
+ return 'PERMISSION_DENIED';
457
+ if (code === 'access_policy_revision_conflict')
458
+ return 'CONFLICT';
459
+ if (code === 'access_policy_temporarily_unavailable')
460
+ return 'TEMPORARILY_UNAVAILABLE';
461
+ if (code === 'restricted_mode_without_owner')
462
+ return 'NOT_ALLOWED';
463
+ if (code === 'unknown_group_access_channel')
464
+ return 'INVALID_ARGUMENT';
465
+ if (code === 'invalid_access_policy')
466
+ return 'INTERNAL_ERROR';
280
467
  return typeof code === 'string' ? code : 'INTERNAL_ERROR';
281
468
  }
282
469
  function errorMessage(error) {
@@ -297,6 +484,15 @@ function requireContactRevision(args) {
297
484
  }
298
485
  return args.expectedContactRevision;
299
486
  }
487
+ function requireAccessRevision(args) {
488
+ if (!Object.prototype.hasOwnProperty.call(args, 'expectedAccessRevision')) {
489
+ throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision is required');
490
+ }
491
+ if (typeof args.expectedAccessRevision !== 'string' || !args.expectedAccessRevision) {
492
+ throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision must be a non-empty string');
493
+ }
494
+ return args.expectedAccessRevision;
495
+ }
300
496
  function decodeUpdateValue(value) {
301
497
  const text = String(value ?? '').trim();
302
498
  if (!text)
@@ -309,6 +505,15 @@ function decodeUpdateValue(value) {
309
505
  catch { }
310
506
  return text;
311
507
  }
508
+ function decodeAccessUpdateValue(value) {
509
+ const text = value.trim();
510
+ try {
511
+ return JSON.parse(text);
512
+ }
513
+ catch {
514
+ return text;
515
+ }
516
+ }
312
517
  function roleMenuUpdateValue(resource, value) {
313
518
  const decoded = decodeUpdateValue(value);
314
519
  if (resource === 'assignment') {
@@ -317,11 +522,31 @@ function roleMenuUpdateValue(resource, value) {
317
522
  }
318
523
  return JSON.stringify(decoded);
319
524
  }
320
- if (![true, false, null, 'grant', 'revoke'].includes(decoded)) {
525
+ if (decoded !== 'grant' && decoded !== 'revoke') {
321
526
  throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
322
527
  }
323
528
  return JSON.stringify(decoded);
324
529
  }
530
+ function normalizeAssignmentUpdate(data) {
531
+ const explicitRole = data?.assignment?.explicitRole;
532
+ return {
533
+ ...data,
534
+ roleId: typeof explicitRole === 'string' ? explicitRole : null,
535
+ };
536
+ }
537
+ function accessPolicyWithUpdate(current, resource, decoded) {
538
+ if (resource === 'policyMode') {
539
+ return { ...current, policyMode: decoded };
540
+ }
541
+ if (resource === 'allowedGroupChannels') {
542
+ return { ...current, allowedGroupChannels: [...decoded] };
543
+ }
544
+ const requestLimit = decoded;
545
+ return {
546
+ ...current,
547
+ requestLimit: { maxTimes: requestLimit.maxTimes, coolDown: requestLimit.coolDown },
548
+ };
549
+ }
325
550
  function validateWritableTarget(target) {
326
551
  if (!target || typeof target !== 'object' || Array.isArray(target))
327
552
  return;
@@ -0,0 +1,38 @@
1
+ import { loadDaemonConfig } from '../../config-store.js';
2
+ import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
3
+ import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
4
+ export function evaluateEvolMenuVersionGate(input) {
5
+ const minimum = loadDaemonConfig().aun?.minEvolVersion;
6
+ if (!minimum)
7
+ return null;
8
+ const minimumVersion = parseStableSemver(minimum);
9
+ if (!minimumVersion) {
10
+ throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
11
+ }
12
+ const received = input.protectedHeaders?.evol_version;
13
+ const receivedVersion = input.encrypted ? parseStableSemver(received) : null;
14
+ if (!receivedVersion || compareStableSemver(receivedVersion, minimumVersion) < 0) {
15
+ return {
16
+ code: 'UPGRADE_REQUIRED',
17
+ message: `Evol ${minimum} or later is required`,
18
+ data: {
19
+ minimum,
20
+ received: receivedVersion ? received : null,
21
+ },
22
+ };
23
+ }
24
+ return null;
25
+ }
26
+ export function isEvolMenuVersionGateEnabled() {
27
+ return loadDaemonConfig().aun?.minEvolVersion !== undefined;
28
+ }
29
+ /** Token may be issued while disabled, but only this flag makes it an access requirement. */
30
+ export function isAunMenuTokenRequired() {
31
+ return loadDaemonConfig().aun?.menuTokenRequired === true;
32
+ }
33
+ export function evolMenuResponseTransportMetadata(_requestEncrypted) {
34
+ return {
35
+ encrypted: true,
36
+ protectedHeaders: { ec_version: readInstalledEvolcoreVersion() },
37
+ };
38
+ }