evolcore 0.0.3 → 0.0.5

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 (148) hide show
  1. package/CHANGELOG.md +83 -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/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
@@ -18,6 +18,8 @@ export const USER_PLANE_CAPABILITY_CEILING = {
18
18
  'chatmode.update',
19
19
  'mentionmode.current',
20
20
  'mentionmode.update',
21
+ 'group.mentionmode.current',
22
+ 'group.rulespolicy.current',
21
23
  'session.list',
22
24
  'session.create',
23
25
  'session.rename',
@@ -415,6 +417,12 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
415
417
  if (!resolvedConfigOp.route || (resolvedConfigOp.fieldRule.class !== 'safe-scalar' && resolvedConfigOp.fieldRule.class !== 'safe-readonly-object')) {
416
418
  return { ok: false, reason: `Config field ${field} is not available to user roles` };
417
419
  }
420
+ if (policy === 'role-overridable-write'
421
+ && resolvedConfigOp.fieldRule.access === 'owner-only'
422
+ && ctx.role !== 'owner'
423
+ && !ctx.isDaemonOwner) {
424
+ return { ok: false, reason: `Config field ${field} can only be changed by an owner` };
425
+ }
418
426
  const roleDef = getRoleDefinition(ctx.role, ctx.selfAid);
419
427
  const fieldPermission = roleDef
420
428
  ? resolveRoleFieldPermission(roleDef.permissions || {}, field)
@@ -133,6 +133,46 @@ const OPERATIONS = [
133
133
  description: 'Update group mention mode',
134
134
  sources: ['slash', 'menu'],
135
135
  },
136
+ {
137
+ id: 'group.list',
138
+ category: 'read',
139
+ dangerous: false,
140
+ defaultScopes: ['agent'],
141
+ description: 'List joined AUN groups',
142
+ sources: ['menu'],
143
+ },
144
+ {
145
+ id: 'group.mentionmode.current',
146
+ category: 'read',
147
+ dangerous: false,
148
+ defaultScopes: ['relation', 'agent'],
149
+ description: 'Read a group mention mode',
150
+ sources: ['menu'],
151
+ },
152
+ {
153
+ id: 'group.mentionmode.update',
154
+ category: 'write-own',
155
+ dangerous: false,
156
+ defaultScopes: ['relation', 'agent'],
157
+ description: 'Update a group mention mode',
158
+ sources: ['menu'],
159
+ },
160
+ {
161
+ id: 'group.rulespolicy.current',
162
+ category: 'read',
163
+ dangerous: false,
164
+ defaultScopes: ['relation', 'agent'],
165
+ description: 'Read a group rules loading policy',
166
+ sources: ['menu'],
167
+ },
168
+ {
169
+ id: 'group.rulespolicy.update',
170
+ category: 'write-own',
171
+ dangerous: false,
172
+ defaultScopes: ['relation', 'agent'],
173
+ description: 'Update a group rules loading policy',
174
+ sources: ['menu'],
175
+ },
136
176
  {
137
177
  id: 'session.list',
138
178
  category: 'read',
@@ -180,7 +220,7 @@ const OPERATIONS = [
180
220
  dangerous: false,
181
221
  defaultScopes: ['filesystem'],
182
222
  description: '获取文件内容(需路径沙箱检查)',
183
- sources: ['menu'],
223
+ sources: ['slash', 'menu'],
184
224
  },
185
225
  // ── Trigger Operations ──
186
226
  {
@@ -317,7 +357,7 @@ const OPERATIONS = [
317
357
  category: 'write-agent',
318
358
  dangerous: false,
319
359
  defaultScopes: ['agent'],
320
- description: 'Block or unblock an AUN contact',
360
+ description: 'Block or unblock a canonical contact across bound private channels',
321
361
  sources: ['agent-tool'],
322
362
  },
323
363
  {
@@ -357,7 +397,15 @@ const OPERATIONS = [
357
397
  category: 'write-agent',
358
398
  dangerous: false,
359
399
  defaultScopes: ['agent'],
360
- description: 'Add contacts or modify contact status (block/unblock)',
400
+ description: 'Manage contacts and contact requests',
401
+ sources: ['menu', 'ecweb', 'control'],
402
+ },
403
+ {
404
+ id: 'connect.access.write',
405
+ category: 'write-agent',
406
+ dangerous: false,
407
+ defaultScopes: ['agent'],
408
+ description: 'Manage owner-only Agent access policy and request limit settings',
361
409
  sources: ['menu', 'ecweb', 'control'],
362
410
  },
363
411
  // Agent Operations
@@ -0,0 +1,15 @@
1
+ const CROSS_AGENT_READ_OPERATIONS = new Set([
2
+ 'trigger.list',
3
+ 'trigger.show',
4
+ 'trigger.history',
5
+ ]);
6
+ export function isCrossAgentTriggerOperationAllowed(input) {
7
+ if (input.control || input.taskAgentAid === input.targetAgentAid)
8
+ return true;
9
+ if (!input.taskAgentAid || !input.targetManagement)
10
+ return false;
11
+ return CROSS_AGENT_READ_OPERATIONS.has(input.operation);
12
+ }
13
+ export function isCrossAgentTriggerReadOperation(operation) {
14
+ return CROSS_AGENT_READ_OPERATIONS.has(operation);
15
+ }
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { kitsTemplatesDir, agentMdPath } from '../paths.js';
5
+ import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
5
6
  import { logger } from '../utils/logger.js';
6
7
  import { loadAgent, saveAgent } from '../config-store.js';
7
8
  import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
@@ -130,15 +131,7 @@ export class BootstrapService {
130
131
  return renderTemplate(template, vars).trim();
131
132
  }
132
133
  resolveAgentDisplayName(aid) {
133
- try {
134
- const content = fs.readFileSync(agentMdPath(aid), 'utf-8');
135
- const fm = content.match(/^---\n([\s\S]*?)\n---/)?.[1] || '';
136
- const name = fm.match(/^name:\s*["']?(.+?)["']?\s*$/m)?.[1]?.trim();
137
- if (name)
138
- return name;
139
- }
140
- catch { }
141
- return aid.split('.')[0];
134
+ return resolveAgentDisplayName(aid) || aid.split('.')[0];
142
135
  }
143
136
  resolveBaseagent(agent, aid) {
144
137
  try {
@@ -26,7 +26,11 @@ export function showActivitiesPolicy(mode, chatType, _identity) {
26
26
  }
27
27
  // ── ChannelLoader ──────────────────────────────────────────────────────────
28
28
  export class ChannelLoader {
29
+ processDebug;
29
30
  plugins = new Map();
31
+ constructor(processDebug) {
32
+ this.processDebug = processDebug;
33
+ }
30
34
  register(plugin) {
31
35
  if (this.plugins.has(plugin.name)) {
32
36
  throw new Error(`Channel plugin '${plugin.name}' already registered`);
@@ -46,7 +50,7 @@ export class ChannelLoader {
46
50
  const ctx = {
47
51
  agentName: agent.aid,
48
52
  defaultProjectPath: agent.config.projects?.defaultPath ?? process.cwd(),
49
- debug: agent.config.debug,
53
+ debug: this.processDebug,
50
54
  };
51
55
  // Build the full list of config instances to create.
52
56
  // AUN is synthesised from agent.aid; any explicit aun entry in channels[] is skipped.
@@ -252,7 +256,7 @@ export function buildReloadHooks(deps) {
252
256
  const ctx = {
253
257
  agentName: agent.aid ?? agent.config?.aid,
254
258
  defaultProjectPath: agent.config?.projects?.defaultPath ?? process.cwd(),
255
- debug: agent.config?.debug,
259
+ debug: channelLoader.processDebug,
256
260
  };
257
261
  const plugin = channelLoader.getPlugin(cfgInst.type);
258
262
  if (!plugin)
@@ -814,12 +814,9 @@ export class CommandHandler {
814
814
  item.lastActive = s.updatedAt;
815
815
  return item;
816
816
  }
817
- /**
818
- * 返回结构化命令菜单(供 menu.query 使用)
819
- * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),visitor/member 仅看到用户级命令
820
- */
821
- getMenuItems(role, chatType = 'private', scope = 'agent') {
822
- return menuGetMenuItems.call(this, role, chatType, scope);
817
+ /** 返回结构化命令菜单,已迁移的命令按当前授权主体过滤。 */
818
+ getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
819
+ return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
823
820
  }
824
821
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
825
822
  async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
@@ -843,8 +840,8 @@ export class CommandHandler {
843
840
  return await menuExecMenuUpdate.call(this, cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel, args, authSubject, source);
844
841
  }
845
842
  /** menu.action — 触发动词。 */
846
- async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu') {
847
- return await menuExecMenuAction.call(this, cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel, authSubject, source);
843
+ async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext, deferSideEffect) {
844
+ return await menuExecMenuAction.call(this, cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel, authSubject, source, transportReplyContext, deferSideEffect);
848
845
  }
849
846
  /** ECWeb 专用入口:身份只取可信 IPC 信封。 */
850
847
  async execMenuForEcweb(payload, auth) {
@@ -1168,7 +1165,7 @@ export class CommandHandler {
1168
1165
  /trigger list [--all] — 查看所有触发器
1169
1166
  /trigger create <参数> — 创建触发器(set 为兼容别名)
1170
1167
  /trigger update <名称|ID> <参数> — 修改触发器
1171
- 常用参数:--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1168
+ 常用参数:--exec script|trigger-session|target-session(create 必填)、--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1172
1169
  /trigger enable <名称|ID> — 启用触发器
1173
1170
  /trigger disable <名称|ID> — 暂停触发器
1174
1171
  /trigger show <名称|ID> — 查看触发器详情
@@ -1451,11 +1448,11 @@ export class CommandHandler {
1451
1448
  * Agent managed CLI config entrypoint. Identity and relation are resolved
1452
1449
  * from the daemon-owned session rather than caller-provided flags.
1453
1450
  */
1454
- async handleConfigOperation(argv, sessionId, delegationToken) {
1451
+ async handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1455
1452
  if (!this.agentDelegationRegistry) {
1456
1453
  return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1457
1454
  }
1458
- const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
1455
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1459
1456
  if (!delegation.ok)
1460
1457
  return { ok: false, code: delegation.code, error: delegation.reason };
1461
1458
  const session = await this.sessionManager.getSessionById(sessionId);
@@ -1513,11 +1510,11 @@ export class CommandHandler {
1513
1510
  : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
1514
1511
  }
1515
1512
  /** Agent-managed contact CLI entrypoint. The daemon owns actor resolution and authorization. */
1516
- async handleContactOperation(argv, sessionId, delegationToken) {
1513
+ async handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1517
1514
  if (!this.agentDelegationRegistry) {
1518
1515
  return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1519
1516
  }
1520
- const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
1517
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1521
1518
  if (!delegation.ok)
1522
1519
  return { ok: false, code: delegation.code, error: delegation.reason };
1523
1520
  const session = await this.sessionManager.getSessionById(sessionId);
@@ -1,6 +1,7 @@
1
- import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
1
+ import { roleMenuQuery, roleMenuGroupCount, 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 { 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 / 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,9 @@ 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 === 'policyMode' || resource === 'requestLimit') {
35
+ return 'connect.access.write';
36
+ }
30
37
  if (resource === 'admin')
31
38
  return 'connect.admin';
32
39
  const decoded = decodeUpdateValue(value);
@@ -41,14 +48,12 @@ export async function handleConnectMenu(req, context) {
41
48
  const { self } = context;
42
49
  if (!self)
43
50
  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');
51
+ if (req.args?.self !== undefined && req.args.self !== self) {
52
+ return fail('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
48
53
  }
49
54
  try {
50
55
  if (req.subtype === 'options')
51
- return await handleOptions(self);
56
+ return await handleOptions(self, req.args, context);
52
57
  if (req.subtype === 'query')
53
58
  return await handleQuery(req, self, context);
54
59
  if (req.subtype === 'update')
@@ -62,21 +67,40 @@ export async function handleConnectMenu(req, context) {
62
67
  return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
63
68
  }
64
69
  // ── Options ──
65
- async function handleOptions(self) {
70
+ async function handleOptions(self, args, context) {
71
+ const option = String(args?.option ?? '').trim();
72
+ if (option) {
73
+ return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
74
+ }
75
+ await expirePendingContactRequests(self);
66
76
  const snapshot = getContactSnapshot(self);
67
77
  const contactCount = snapshot.contacts.size;
68
78
  const blockedCount = snapshot.blockedIndex.size;
79
+ const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
80
+ let groupsDescription = '查看所在群组(数量暂不可用)';
81
+ try {
82
+ const groupCount = await roleMenuGroupCount(context);
83
+ groupsDescription = `查看所在群组(${groupCount} 个)`;
84
+ }
85
+ catch {
86
+ // Keep the root menu available when AUN group directory is disabled or unavailable.
87
+ }
69
88
  return ok({
70
89
  options: [
71
90
  {
72
91
  option: 'contacts',
73
92
  label: '联系人列表',
74
- description: `查看所有联系人(${contactCount} 人,${blockedCount} 人已拉黑)`,
93
+ description: `查看所有状态(${contactCount} 人,${pendingCount} 个待审核,${blockedCount} 人已拉黑)`,
75
94
  },
76
95
  {
77
96
  option: 'groups',
78
97
  label: '群组列表',
79
- description: '查看所在群组',
98
+ description: groupsDescription,
99
+ },
100
+ {
101
+ option: 'access',
102
+ label: '访问设置',
103
+ description: '设置消息准入模式和准入控制消息限额',
80
104
  },
81
105
  ],
82
106
  });
@@ -89,6 +113,8 @@ async function handleQuery(req, self, context) {
89
113
  return queryContacts(args, self);
90
114
  if (view === 'contact')
91
115
  return queryContact(req, self);
116
+ if (view === 'access')
117
+ return queryAccess(self);
92
118
  // Groups and role assignments are delegated to role-menu (authoritative source).
93
119
  // Role *definitions* are out of scope here — use `menu name=role` for those.
94
120
  if (view === 'groups') {
@@ -124,9 +150,19 @@ async function handleQuery(req, self, context) {
124
150
  dataSource: { ...data.dataSource, kind: 'relation-config' },
125
151
  });
126
152
  }
127
- return fail('INVALID_ARGUMENT', `Unknown view: ${String(view)}`);
153
+ return fail('NOT_SUPPORTED', `Unknown view: ${String(view)}`);
154
+ }
155
+ function queryAccess(self) {
156
+ const { policy, accessRevision } = readAccessPolicySnapshot(self);
157
+ return ok({
158
+ self,
159
+ view: 'access',
160
+ policy,
161
+ accessRevision,
162
+ });
128
163
  }
129
- function queryContacts(args, self) {
164
+ async function queryContacts(args, self) {
165
+ await expirePendingContactRequests(self);
130
166
  const snapshot = getContactSnapshot(self);
131
167
  const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
132
168
  const items = page.items.map(primaryId => contactItem(self, primaryId));
@@ -135,11 +171,13 @@ function queryContacts(args, self) {
135
171
  view: 'contacts',
136
172
  items,
137
173
  contactRevision: snapshot.contactRevision,
174
+ pendingCount: [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length,
138
175
  nextCursor: page.nextCursor,
139
176
  dataSource: { kind: 'contact-book', authoritative: true, complete: true },
140
177
  });
141
178
  }
142
- function queryContact(req, self) {
179
+ async function queryContact(req, self) {
180
+ await expirePendingContactRequests(self);
143
181
  const primaryId = String(req.args?.primaryId ?? '').trim();
144
182
  if (!primaryId)
145
183
  return fail('INVALID_ARGUMENT', 'primaryId is required');
@@ -150,6 +188,7 @@ function queryContact(req, self) {
150
188
  }
151
189
  function contactItem(self, primaryId) {
152
190
  const view = resolveContactView(self, primaryId);
191
+ const pendingRequest = getContactSnapshot(self).contacts.get(primaryId)?.pendingRequest;
153
192
  return {
154
193
  primaryId,
155
194
  displayName: view.displayName ?? null,
@@ -157,6 +196,13 @@ function contactItem(self, primaryId) {
157
196
  blocked: view.blocked,
158
197
  isOwner: view.isOwner,
159
198
  aliases: view.aliases,
199
+ ...(pendingRequest ? {
200
+ pendingRequest: {
201
+ id: pendingRequest.id,
202
+ submittedAt: pendingRequest.submittedAt,
203
+ expiresAt: pendingRequest.expiresAt,
204
+ },
205
+ } : {}),
160
206
  };
161
207
  }
162
208
  function normalizeAssignmentItem(item) {
@@ -181,12 +227,15 @@ function normalizeAssignmentItem(item) {
181
227
  return item;
182
228
  }
183
229
  }
184
- // ── Update (delegated to role-menu) ──
230
+ // ── Update ──
185
231
  async function handleUpdate(req, context) {
186
232
  const args = req.args ?? {};
187
- const resource = args.resource;
233
+ const resource = String(args.resource ?? '');
234
+ if (isAccessUpdateResource(resource)) {
235
+ return updateAccessResource(req, context, resource);
236
+ }
188
237
  if (resource !== 'assignment' && resource !== 'admin') {
189
- return fail('INVALID_ARGUMENT', `Unknown resource: ${String(resource)}`);
238
+ return fail('NOT_SUPPORTED', `Unknown resource: ${String(resource)}`);
190
239
  }
191
240
  if (req.value === undefined || !req.value.trim()) {
192
241
  return fail('INVALID_ARGUMENT', 'value is required');
@@ -194,9 +243,9 @@ async function handleUpdate(req, context) {
194
243
  validateWritableTarget(args.target);
195
244
  const value = roleMenuUpdateValue(resource, req.value);
196
245
  const data = await roleMenuUpdate(context, args, value);
197
- return ok(data);
246
+ return ok(resource === 'assignment' ? normalizeAssignmentUpdate(data) : data);
198
247
  }
199
- // ── Action (Contact Book v2) ──
248
+ // ── Action (Contact Book v3) ──
200
249
  async function handleAction(req, self, context) {
201
250
  const action = req.action;
202
251
  if (action === 'add')
@@ -205,9 +254,100 @@ async function handleAction(req, self, context) {
205
254
  return actionSetStatus(req, self, context, true);
206
255
  if (action === 'unblock')
207
256
  return actionSetStatus(req, self, context, false);
208
- return fail('INVALID_ARGUMENT', `Unknown action: ${String(action)}`);
257
+ if (action === 'approve-request')
258
+ return actionReviewRequest(req, self, context, 'approve');
259
+ if (action === 'reject-request')
260
+ return actionReviewRequest(req, self, context, 'reject');
261
+ if (action === 'block-request')
262
+ return actionReviewRequest(req, self, context, 'block');
263
+ return fail('NOT_SUPPORTED', `Unknown action: ${String(action)}`);
264
+ }
265
+ function isAccessUpdateResource(resource) {
266
+ return resource === 'policyMode'
267
+ || resource === 'requestLimit';
268
+ }
269
+ function updateAccessResource(req, context, resource) {
270
+ const args = req.args ?? {};
271
+ requireOwnerContext(context);
272
+ if (req.value === undefined || !req.value.trim()) {
273
+ return fail('INVALID_ARGUMENT', 'value is required');
274
+ }
275
+ const self = context.self;
276
+ const expectedAccessRevision = requireAccessRevision(args);
277
+ const decoded = decodeAccessUpdateValue(req.value);
278
+ if (resource === 'policyMode') {
279
+ if (typeof decoded !== 'string' || !['open', 'contacts', 'owners'].includes(decoded)) {
280
+ return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners');
281
+ }
282
+ }
283
+ else {
284
+ const requestLimit = decoded;
285
+ if (!requestLimit || typeof requestLimit !== 'object' || Array.isArray(requestLimit)
286
+ || Object.keys(requestLimit).some(key => key !== 'maxTimes' && key !== 'coolDown')
287
+ || !Number.isInteger(requestLimit.maxTimes)
288
+ || Number(requestLimit.maxTimes) < 1
289
+ || Number(requestLimit.maxTimes) > 100_000
290
+ || !Number.isInteger(requestLimit.coolDown)
291
+ || Number(requestLimit.coolDown) < 1
292
+ || Number(requestLimit.coolDown) > 86_400) {
293
+ return fail('INVALID_ARGUMENT', 'requestLimit must be a JSON object with integer maxTimes and coolDown');
294
+ }
295
+ }
296
+ const result = mutateAccessPolicy({
297
+ selfAid: self,
298
+ actorAid: String(context.actorAid || ''),
299
+ actorRole: context.actorRole,
300
+ resource: `access.${resource}`,
301
+ expectedAccessRevision,
302
+ update: current => accessPolicyWithUpdate(current, resource, decoded),
303
+ });
304
+ return ok({
305
+ self,
306
+ resource,
307
+ policy: result.policy,
308
+ accessRevision: result.accessRevision,
309
+ changed: result.changed,
310
+ });
311
+ }
312
+ async function actionReviewRequest(req, self, context, decision) {
313
+ const args = req.args ?? {};
314
+ const primaryId = String(args.primaryId ?? '').trim();
315
+ const requestId = String(args.requestId ?? '').trim();
316
+ if (!primaryId)
317
+ return fail('INVALID_ARGUMENT', 'primaryId is required');
318
+ if (!requestId)
319
+ return fail('INVALID_ARGUMENT', 'requestId is required');
320
+ requireAdminContext(context);
321
+ const expectedContactRevision = requireContactRevision(args);
322
+ const actorId = String(context.actorAid || '').trim();
323
+ const reviewed = await reviewContactRequest({
324
+ selfAid: self,
325
+ primaryId,
326
+ requestId,
327
+ expectedContactRevision,
328
+ approverId: actorId,
329
+ actorId,
330
+ reviewerRole: context.actorRole,
331
+ decision,
332
+ });
333
+ if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
334
+ return ok({
335
+ self,
336
+ action: `${decision}-request`,
337
+ primaryId,
338
+ requestId,
339
+ result: reviewed.code,
340
+ contactRevision: reviewed.contactRevision,
341
+ });
342
+ }
343
+ const code = reviewed.code === 'forbidden' ? 'PERMISSION_DENIED'
344
+ : reviewed.code === 'revision-conflict' ? 'CONFLICT'
345
+ : reviewed.code === 'request-expired' ? 'EXPIRED'
346
+ : 'NOT_FOUND';
347
+ return fail(code, reviewed.code.replace(/-/g, ' '), { contactRevision: reviewed.contactRevision });
209
348
  }
210
349
  async function actionAdd(req, self, context) {
350
+ requireAdminContext(context);
211
351
  const args = req.args ?? {};
212
352
  const primaryId = String(args.primaryId ?? '').trim();
213
353
  if (!primaryId)
@@ -219,11 +359,13 @@ async function actionAdd(req, self, context) {
219
359
  }
220
360
  const current = getContactSnapshot(self).contacts.get(primaryId);
221
361
  const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
222
- const result = await mutateContactBook({
362
+ const result = await mutateContactWithOperation({
223
363
  selfAid: self,
224
- actor: context.actorAid ?? 'connect-menu',
364
+ actorId: context.actorAid ?? 'connect-menu',
365
+ operation: 'manual-add',
225
366
  expectedContactRevision,
226
367
  mutation: { type: 'set-display-name', primaryId, displayName },
368
+ primaryId,
227
369
  });
228
370
  const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
229
371
  return ok({
@@ -236,16 +378,22 @@ async function actionAdd(req, self, context) {
236
378
  });
237
379
  }
238
380
  async function actionSetStatus(req, self, context, blocked) {
381
+ requireAdminContext(context);
239
382
  const args = req.args ?? {};
240
383
  const primaryId = String(args.primaryId ?? '').trim();
241
384
  if (!primaryId)
242
385
  return fail('INVALID_ARGUMENT', 'primaryId is required');
243
386
  const expectedContactRevision = requireContactRevision(args);
244
- const result = await mutateContactBook({
387
+ if (!getContactSnapshot(self).contacts.has(primaryId)) {
388
+ return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
389
+ }
390
+ const result = await mutateContactWithOperation({
245
391
  selfAid: self,
246
- actor: context.actorAid ?? 'connect-menu',
392
+ actorId: context.actorAid ?? 'connect-menu',
393
+ operation: blocked ? 'block' : 'unblock',
247
394
  expectedContactRevision,
248
395
  mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
396
+ primaryId,
249
397
  });
250
398
  return ok({
251
399
  self,
@@ -259,6 +407,16 @@ async function actionSetStatus(req, self, context, blocked) {
259
407
  function ok(data) {
260
408
  return { type: 'menu.response', id: '', name: MENU_NAME, data };
261
409
  }
410
+ function requireOwnerContext(context) {
411
+ if (context.actorRole !== 'owner') {
412
+ throw connectError('PERMISSION_DENIED', 'Only an Agent Owner can modify this access setting');
413
+ }
414
+ }
415
+ function requireAdminContext(context) {
416
+ if (context.actorRole !== 'owner' && context.actorRole !== 'admin') {
417
+ throw connectError('PERMISSION_DENIED', 'Only an Agent Owner or Admin can perform this operation');
418
+ }
419
+ }
262
420
  function fail(code, message, data) {
263
421
  return {
264
422
  type: 'menu.response',
@@ -277,6 +435,16 @@ function errorCode(error) {
277
435
  return 'CONFLICT';
278
436
  if (code === 'INVALID_CONTACT_BOOK')
279
437
  return 'INTERNAL_ERROR';
438
+ if (code === 'access_policy_owner_required')
439
+ return 'PERMISSION_DENIED';
440
+ if (code === 'access_policy_revision_conflict')
441
+ return 'CONFLICT';
442
+ if (code === 'access_policy_temporarily_unavailable')
443
+ return 'TEMPORARILY_UNAVAILABLE';
444
+ if (code === 'restricted_mode_without_owner')
445
+ return 'NOT_ALLOWED';
446
+ if (code === 'invalid_access_policy')
447
+ return 'INTERNAL_ERROR';
280
448
  return typeof code === 'string' ? code : 'INTERNAL_ERROR';
281
449
  }
282
450
  function errorMessage(error) {
@@ -297,6 +465,15 @@ function requireContactRevision(args) {
297
465
  }
298
466
  return args.expectedContactRevision;
299
467
  }
468
+ function requireAccessRevision(args) {
469
+ if (!Object.prototype.hasOwnProperty.call(args, 'expectedAccessRevision')) {
470
+ throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision is required');
471
+ }
472
+ if (typeof args.expectedAccessRevision !== 'string' || !args.expectedAccessRevision) {
473
+ throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision must be a non-empty string');
474
+ }
475
+ return args.expectedAccessRevision;
476
+ }
300
477
  function decodeUpdateValue(value) {
301
478
  const text = String(value ?? '').trim();
302
479
  if (!text)
@@ -309,6 +486,15 @@ function decodeUpdateValue(value) {
309
486
  catch { }
310
487
  return text;
311
488
  }
489
+ function decodeAccessUpdateValue(value) {
490
+ const text = value.trim();
491
+ try {
492
+ return JSON.parse(text);
493
+ }
494
+ catch {
495
+ return text;
496
+ }
497
+ }
312
498
  function roleMenuUpdateValue(resource, value) {
313
499
  const decoded = decodeUpdateValue(value);
314
500
  if (resource === 'assignment') {
@@ -317,11 +503,28 @@ function roleMenuUpdateValue(resource, value) {
317
503
  }
318
504
  return JSON.stringify(decoded);
319
505
  }
320
- if (![true, false, null, 'grant', 'revoke'].includes(decoded)) {
506
+ if (decoded !== 'grant' && decoded !== 'revoke') {
321
507
  throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
322
508
  }
323
509
  return JSON.stringify(decoded);
324
510
  }
511
+ function normalizeAssignmentUpdate(data) {
512
+ const explicitRole = data?.assignment?.explicitRole;
513
+ return {
514
+ ...data,
515
+ roleId: typeof explicitRole === 'string' ? explicitRole : null,
516
+ };
517
+ }
518
+ function accessPolicyWithUpdate(current, resource, decoded) {
519
+ if (resource === 'policyMode') {
520
+ return { ...current, policyMode: decoded };
521
+ }
522
+ const requestLimit = decoded;
523
+ return {
524
+ ...current,
525
+ requestLimit: { maxTimes: requestLimit.maxTimes, coolDown: requestLimit.coolDown },
526
+ };
527
+ }
325
528
  function validateWritableTarget(target) {
326
529
  if (!target || typeof target !== 'object' || Array.isArray(target))
327
530
  return;