evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -8,7 +8,7 @@ import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
8
8
  import { agentDir, resolvePaths } from '../../paths.js';
9
9
  import { listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, } from '../../config/peer-role-resolver.js';
10
10
  import { resolveContactView, resolvePeerDisplayLabel, resolvePrincipal } from '../../config/contact-book.js';
11
- import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, } from '../../config/contact-request-service.js';
11
+ import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, contactRequestSubmissionProtocolResult, queryOwnContactRequestStatus, } from '../../config/contact-request-service.js';
12
12
  import { isValidAid } from '../../aun/aid/validation.js';
13
13
  import { PeerIdentityCache } from '../relation/peer-identity.js';
14
14
  import { handlePendingDingtalkContactBindMessage, registerPendingDingtalkContactBind } from '../../channels/dingtalk.js';
@@ -17,8 +17,8 @@ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind }
17
17
  import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
18
18
  import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
19
19
  import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
20
- import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, } from '../command/menu-protocol.js';
21
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired } from '../command/evol-menu-version-gate.js';
20
+ import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, } from '../command/menu-protocol.js';
21
+ import { resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from '../command/menu-catalog.js';
22
22
  import { MenuTokenStore } from '../command/menu-token-store.js';
23
23
  import { SessionRenewService } from '../session/session-renew.js';
24
24
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
@@ -306,42 +306,171 @@ export class MessageBridge {
306
306
  return true;
307
307
  }
308
308
  }
309
+ return false;
310
+ }
311
+ async handleContactRequestProtocol(input) {
312
+ const { context, msg, content, owningAgent, adapter, sendReply } = input;
309
313
  const requestMatch = /^\s*\/request(?:\s+([\s\S]*?))?\s*$/i.exec(content);
310
- if (!requestMatch)
314
+ let protocolType;
315
+ let protocolPayload;
316
+ try {
317
+ const parsed = JSON.parse(content);
318
+ protocolPayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
319
+ ? parsed
320
+ : undefined;
321
+ protocolType = protocolPayload?.type === 'contact.request.query'
322
+ ? 'query'
323
+ : protocolPayload?.type === 'contact.request.submit' ? 'submit' : undefined;
324
+ }
325
+ catch { /* slash command */ }
326
+ if (!requestMatch && !protocolType)
311
327
  return false;
312
- if (context.channelType !== 'aun') {
313
- await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
328
+ // Slash commands keep their human-facing response semantics. Structured
329
+ // App requests use contact.request.response and never fall through to the
330
+ // normal command/session pipeline.
331
+ if (requestMatch) {
332
+ // Blacklist remains the first gate even though slash controls are
333
+ // recognized before ordinary admission. Let the normal blocked path
334
+ // decide whether an agent/bot/service gets a limited rejection.
335
+ if (context.contactStatus === 'blocked')
336
+ return false;
337
+ if (context.channelType !== 'aun') {
338
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
339
+ return true;
340
+ }
341
+ if (context.chatType !== 'private') {
342
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
343
+ return true;
344
+ }
345
+ if (!context.primaryId || context.principal?.principalStatus !== 'native_aid') {
346
+ await this.sendAdmissionControlReply(context, msg, sendReply, '无法验证当前 AUN 身份,未提交联系人申请。');
347
+ return true;
348
+ }
349
+ try {
350
+ const submission = await submitContactRequest({
351
+ selfAid: context.selfAid,
352
+ applicantAid: context.primaryId,
353
+ note: requestMatch[1],
354
+ sourceChannelKey: context.channelKey,
355
+ });
356
+ if ((submission.code === 'submitted' || submission.code === 'resubmitted') && submission.requestId) {
357
+ void this.deliverContactReviewCard(context.selfAid, context.primaryId, submission, owningAgent)
358
+ .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: ${error instanceof Error ? error.message : String(error)}`));
359
+ }
360
+ await this.sendAdmissionControlReply(context, msg, sendReply, this.contactRequestReply(submission));
361
+ }
362
+ catch (error) {
363
+ logger.warn(`[MessageBridge] Contact request submission failed: ${error instanceof Error ? error.message : String(error)}`);
364
+ await this.sendAdmissionControlReply(context, msg, sendReply, '当前无法受理联系人申请,请稍后再试。');
365
+ }
314
366
  return true;
315
367
  }
316
- if (context.chatType !== 'private') {
317
- await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
368
+ // The structured protocol is AUN-only. For a blocked external alias,
369
+ // return false so the existing blacklist response/silence policy applies;
370
+ // never emit a custom payload through a non-AUN adapter.
371
+ if (context.contactStatus === 'blocked' && context.channelType !== 'aun')
372
+ return false;
373
+ if (context.channelType !== 'aun')
374
+ return true;
375
+ if (context.chatType !== 'private')
376
+ return true;
377
+ if (context.principal?.principalStatus !== 'native_aid' || !context.primaryId) {
378
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
379
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
380
+ }, owningAgent?.name);
318
381
  return true;
319
382
  }
320
- if (!context.primaryId) {
321
- await this.sendAdmissionControlReply(context, msg, sendReply, '无法验证当前 AUN 身份,未提交联系人申请。');
383
+ if (context.contactStatus === 'blocked') {
384
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
385
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
386
+ }, owningAgent?.name);
322
387
  return true;
323
388
  }
324
389
  let result;
325
- try {
326
- result = await submitContactRequest({
327
- selfAid: context.selfAid,
328
- applicantAid: context.primaryId,
329
- note: requestMatch[1],
330
- sourceChannelKey: context.channelKey,
331
- });
332
- }
333
- catch (error) {
334
- logger.warn(`[MessageBridge] Contact request submission failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`);
335
- await this.sendAdmissionControlReply(context, msg, sendReply, '当前无法受理联系人申请,请稍后再试。');
336
- return true;
390
+ if (protocolType === 'query') {
391
+ try {
392
+ result = await queryOwnContactRequestStatus({
393
+ selfAid: context.selfAid,
394
+ applicantAid: context.primaryId,
395
+ requestId: protocolPayload && Object.prototype.hasOwnProperty.call(protocolPayload, 'request_id')
396
+ ? protocolPayload.request_id
397
+ : undefined,
398
+ });
399
+ }
400
+ catch (error) {
401
+ logger.warn(`[MessageBridge] Contact request query failed: ${error instanceof Error ? error.message : String(error)}`);
402
+ result = { status: 'NOT_ALLOWED', requestId: null, expiresAt: null };
403
+ }
337
404
  }
338
- if ((result.code === 'submitted' || result.code === 'resubmitted') && result.requestId) {
339
- void this.deliverContactReviewCard(context.selfAid, context.primaryId, result, owningAgent)
340
- .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`));
405
+ else {
406
+ try {
407
+ if (protocolPayload && Object.prototype.hasOwnProperty.call(protocolPayload, 'note')
408
+ && typeof protocolPayload?.note !== 'string') {
409
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
410
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
411
+ }, owningAgent?.name);
412
+ return true;
413
+ }
414
+ const submission = await submitContactRequest({
415
+ selfAid: context.selfAid,
416
+ applicantAid: context.primaryId,
417
+ note: typeof protocolPayload?.note === 'string' ? protocolPayload.note : undefined,
418
+ sourceChannelKey: context.channelKey,
419
+ });
420
+ result = contactRequestSubmissionProtocolResult(submission);
421
+ if ((submission.code === 'submitted' || submission.code === 'resubmitted') && submission.requestId) {
422
+ void this.deliverContactReviewCard(context.selfAid, context.primaryId, submission, owningAgent)
423
+ .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: ${error instanceof Error ? error.message : String(error)}`));
424
+ }
425
+ }
426
+ catch (error) {
427
+ logger.warn(`[MessageBridge] Contact request submission failed: ${error instanceof Error ? error.message : String(error)}`);
428
+ result = { status: 'NOT_ALLOWED', requestId: null, expiresAt: null };
429
+ }
341
430
  }
342
- await this.sendAdmissionControlReply(context, msg, sendReply, this.contactRequestReply(result));
431
+ await this.sendContactRequestProtocolResponse(adapter, msg, result, owningAgent?.name);
343
432
  return true;
344
433
  }
434
+ contactRequestReply(result) {
435
+ switch (result.code) {
436
+ case 'submitted': return '联系人申请已提交,等待管理员审核。';
437
+ case 'resubmitted': return '联系人申请已更新,等待管理员审核。';
438
+ case 'already-contact': return '你已经是联系人。';
439
+ case 'owner': return '你已经是 Owner。';
440
+ case 'blocked': return CONTACT_REJECTION_NOTICE;
441
+ case 'rate-limited': return '联系人申请过于频繁,请稍后再试。';
442
+ case 'capacity': return '当前申请较多,请稍后再试。';
443
+ case 'no-owner': return '当前无法受理联系人申请。';
444
+ case 'invalid-applicant': return '无法验证当前 AUN 身份,未提交联系人申请。';
445
+ case 'invalid-note': return '申请说明不能超过 500 个字符。';
446
+ }
447
+ }
448
+ async sendContactRequestProtocolResponse(adapter, msg, result, agentName) {
449
+ if (!adapter?.send)
450
+ return;
451
+ await adapter.send(buildEnvelope({
452
+ taskId: `contact-request-${randomBytes(4).toString('hex')}`,
453
+ channel: adapter.channelName,
454
+ channelId: msg.channelId,
455
+ agentName: agentName ?? '<unknown>',
456
+ replyContext: {
457
+ ...(msg.replyContext ?? {}),
458
+ metadata: {
459
+ ...(msg.replyContext?.metadata ?? {}),
460
+ ...evolMenuResponseTransportMetadata(),
461
+ },
462
+ },
463
+ }), {
464
+ kind: 'custom',
465
+ channelType: 'aun',
466
+ payload: {
467
+ type: 'contact.request.response',
468
+ status: result.status,
469
+ request_id: result.requestId,
470
+ expires_at: result.expiresAt,
471
+ },
472
+ });
473
+ }
345
474
  async sendAdmissionControlReply(context, msg, sendReply, text) {
346
475
  if (!text || !consumeAdmissionReplyBudget(context))
347
476
  return false;
@@ -360,20 +489,6 @@ export class MessageBridge {
360
489
  }
361
490
  return '当前不接受陌生人消息。联系人申请仅支持 AUN 私聊。';
362
491
  }
363
- contactRequestReply(result) {
364
- switch (result.code) {
365
- case 'submitted': return '联系人申请已提交,等待管理员审核。';
366
- case 'resubmitted': return '联系人申请已更新,等待管理员审核。';
367
- case 'already-contact': return '你已经是联系人。';
368
- case 'owner': return '你已经是 Owner。';
369
- case 'blocked': return CONTACT_REJECTION_NOTICE;
370
- case 'rate-limited': return '联系人申请过于频繁,请稍后再试。';
371
- case 'capacity': return '当前申请较多,请稍后再试。';
372
- case 'no-owner': return '当前无法受理联系人申请。';
373
- case 'invalid-applicant': return '无法验证当前 AUN 身份,未提交联系人申请。';
374
- case 'invalid-note': return '申请说明不能超过 500 个字符。';
375
- }
376
- }
377
492
  async deliverContactReviewCard(selfAid, applicantAid, submission, owningAgent) {
378
493
  if (!this.interactionRouter || !submission.requestId || !submission.expiresAt || !submission.submittedAt) {
379
494
  return false;
@@ -463,7 +578,7 @@ export class MessageBridge {
463
578
  }
464
579
  if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
465
580
  try {
466
- await this.notifyContactApplicant(route.adapter, applicantAid, reviewed.code, owningAgent?.name);
581
+ await this.notifyContactApplicant(route.adapter, applicantAid, reviewed.code, submission.requestId, submission.expiresAt, owningAgent?.name);
467
582
  }
468
583
  catch (error) {
469
584
  logger.warn(`[MessageBridge] Contact applicant notification failed: request=${submission.requestId} error=${error instanceof Error ? error.message : String(error)}`);
@@ -485,7 +600,7 @@ export class MessageBridge {
485
600
  channelId: route.approverOperatorId,
486
601
  agentName: owningAgent?.name ?? selfAid,
487
602
  chatmode: 'interactive',
488
- replyContext: { metadata: { source: 'handoff', chatmode: 'interactive' } },
603
+ replyContext: { delivery: { chatType: 'private' }, metadata: { source: 'handoff', chatmode: 'interactive' } },
489
604
  });
490
605
  const sent = await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), envelope.replyContext);
491
606
  if (!sent)
@@ -500,13 +615,24 @@ export class MessageBridge {
500
615
  : code === 'request-stale' ? '申请已更新或已被处理。'
501
616
  : code === 'revision-conflict' ? '联系人列表已变化,请从联系人列表重新审核。'
502
617
  : '当前无审核权限。';
503
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-review' });
618
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>', delivery: { chatType: 'private' } }), { kind: 'system.notice', text, subtype: 'contact-review' });
504
619
  }
505
- async notifyContactApplicant(adapter, applicantAid, code, agentName) {
506
- const text = code === 'approved' ? '你的联系人申请已通过。'
507
- : code === 'rejected' ? '你的联系人申请未通过。'
508
- : '你的联系人申请未通过。';
509
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: applicantAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-request-result' });
620
+ async notifyContactApplicant(adapter, applicantAid, code, requestId, expiresAt, agentName) {
621
+ await adapter.send(buildEnvelope({
622
+ channel: adapter.channelName,
623
+ channelId: applicantAid,
624
+ agentName: agentName ?? '<unknown>',
625
+ replyContext: { delivery: { chatType: 'private' }, metadata: evolMenuResponseTransportMetadata() },
626
+ }), {
627
+ kind: 'custom',
628
+ channelType: 'aun',
629
+ payload: {
630
+ type: 'contact.request.response',
631
+ status: code === 'approved' ? 'APPROVED' : code === 'rejected' ? 'REJECTED' : 'NOT_ALLOWED',
632
+ request_id: requestId,
633
+ expires_at: expiresAt,
634
+ },
635
+ });
510
636
  }
511
637
  getDebouncer(channelName, channelType) {
512
638
  let d = this.debouncers.get(channelName);
@@ -561,11 +687,23 @@ export class MessageBridge {
561
687
  onMessage(async (msg) => {
562
688
  try {
563
689
  let content = msg.content.trim();
690
+ // Channels that expose adapter receive time (notably AUN) must keep
691
+ // that timestamp through session resolution and queue processing.
692
+ // The fallback is only for legacy adapters that do not provide it.
693
+ const receivedAt = msg.receivedAt ?? Date.now();
564
694
  const channelKey = adapter?.channelKey || channelName;
565
695
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
566
696
  ?? this.agentRegistry?.resolveByChannel(channelName);
567
697
  const parsedChannelKey = tryParseChannelKey(channelKey);
568
698
  const chatType = msg.chatType || 'private';
699
+ const delivery = chatType === 'group'
700
+ ? { chatType: 'group', groupId: msg.groupId || msg.channelId }
701
+ : { chatType: 'private' };
702
+ msg.groupId = delivery.chatType === 'group' ? delivery.groupId : undefined;
703
+ msg.replyContext = {
704
+ ...(msg.replyContext ?? {}),
705
+ delivery,
706
+ };
569
707
  const actorId = msg.peerId;
570
708
  const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
571
709
  const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
@@ -582,6 +720,16 @@ export class MessageBridge {
582
720
  actorId,
583
721
  trustedHandoffEcho,
584
722
  });
723
+ if (!trustedHandoffEcho && await this.handleContactRequestProtocol({
724
+ context: admissionPreflight.context,
725
+ msg,
726
+ content,
727
+ owningAgent,
728
+ adapter,
729
+ sendReply,
730
+ })) {
731
+ return;
732
+ }
585
733
  if (!admissionPreflight.allow) {
586
734
  const requestCommand = /^\s*\/request(?:\s|$)/i.test(content);
587
735
  if (admissionPreflight.reason === 'blocked' && admissionPreflight.context.primaryId) {
@@ -789,7 +937,9 @@ export class MessageBridge {
789
937
  content: msg.messageLogContent ?? content,
790
938
  replyTo: inboundReplyTo,
791
939
  permMode: null,
792
- timestamp: Date.now(),
940
+ timestamp: receivedAt,
941
+ receivedAt,
942
+ gatewaySeq: msg.gatewaySeq,
793
943
  encrypt: inboundEncrypt,
794
944
  chatmode: inboundChatmode,
795
945
  peerName: msg.peerName,
@@ -881,9 +1031,12 @@ export class MessageBridge {
881
1031
  channelType: msg.channelType || effectiveChannelType,
882
1032
  channelId: msg.channelId, content,
883
1033
  selfAID: msg.selfAID,
1034
+ groupId: msg.groupId,
884
1035
  baseagent: session.baseagent,
885
1036
  chatType,
886
- images: msg.images, timestamp: Date.now(),
1037
+ images: msg.images, timestamp: receivedAt,
1038
+ receivedAt,
1039
+ gatewaySeq: msg.gatewaySeq,
887
1040
  peerId: msg.peerId, peerName: msg.peerName,
888
1041
  peerType: msg.peerType,
889
1042
  sameDevice: msg.sameDevice,
@@ -900,7 +1053,7 @@ export class MessageBridge {
900
1053
  source: msg.source,
901
1054
  boundSessionId: session.id,
902
1055
  resolvedIdentity: identity,
903
- dispatchMode: msg.dispatchMode,
1056
+ mentionMode: msg.mentionMode,
904
1057
  causation: inboundCausation,
905
1058
  };
906
1059
  recordCausationSpan(inboundCausation, 'message.inbound', {
@@ -926,6 +1079,8 @@ export class MessageBridge {
926
1079
  replyTo: inboundReplyTo,
927
1080
  permMode: session.identity?.role ?? null,
928
1081
  timestamp: fullMessage.timestamp,
1082
+ receivedAt: msg.receivedAt,
1083
+ gatewaySeq: msg.gatewaySeq,
929
1084
  encrypt: inboundEncrypt,
930
1085
  chatmode: inboundChatmode,
931
1086
  peerName: msg.peerName,
@@ -1027,28 +1182,6 @@ export class MessageBridge {
1027
1182
  });
1028
1183
  }
1029
1184
  // ── Menu Protocol ──
1030
- static MENU_NAME_MAP = {
1031
- pwd: '/pwd',
1032
- session: '/session',
1033
- topic: '/topic',
1034
- baseagent: '/baseagent',
1035
- model: '/model',
1036
- effort: '/effort',
1037
- chatmode: '/chatmode',
1038
- mentionmode: '/mentionmode',
1039
- group: '/group',
1040
- permission: '/perm',
1041
- activity: '/activity',
1042
- observable: '/observable',
1043
- system: '/system',
1044
- cli: '/cli',
1045
- agent: '/agent',
1046
- trigger: '/trigger',
1047
- file: '/file',
1048
- capability: '/capability',
1049
- role: '/role',
1050
- connect: '/connect',
1051
- };
1052
1185
  extractTopicName(msg) {
1053
1186
  const raw = msg.topicName
1054
1187
  ?? msg.replyContext?.title
@@ -1077,7 +1210,7 @@ export class MessageBridge {
1077
1210
  resolveCmd(name, cmd) {
1078
1211
  if (cmd)
1079
1212
  return cmd;
1080
- const mapped = MessageBridge.MENU_NAME_MAP[name];
1213
+ const mapped = menuCommandForName(name);
1081
1214
  if (!mapped)
1082
1215
  throw { code: 'NOT_SUPPORTED', message: `Unsupported menu name: ${name}` };
1083
1216
  return mapped;
@@ -1086,6 +1219,31 @@ export class MessageBridge {
1086
1219
  if (!hasValidMenuId(parsed))
1087
1220
  return;
1088
1221
  const header = { id: parsed.id, ...(parsed.name?.trim() ? { name: parsed.name } : {}) };
1222
+ // Menu IDs are the protocol's stable correlation key. Bind it, together
1223
+ // with the active EvolCore session, before authorization so every
1224
+ // CommandAudit record and denial can be joined to the same request.
1225
+ let activeSession;
1226
+ if (msg.threadId && typeof this.sessionManager.getThreadSession === 'function') {
1227
+ try {
1228
+ activeSession = await this.sessionManager.getThreadSession(channel, msg.channelId, msg.threadId);
1229
+ }
1230
+ catch {
1231
+ activeSession = undefined;
1232
+ }
1233
+ }
1234
+ if (!activeSession && typeof this.sessionManager.getActiveSession === 'function') {
1235
+ try {
1236
+ activeSession = await this.sessionManager.getActiveSession(channel, msg.channelId);
1237
+ }
1238
+ catch {
1239
+ activeSession = undefined;
1240
+ }
1241
+ }
1242
+ const menuAuthSubject = {
1243
+ ...authSubject,
1244
+ requestId: parsed.id,
1245
+ ...(activeSession?.id ? { sessionId: activeSession.id } : {}),
1246
+ };
1089
1247
  const isTokenRequest = parsed.type === 'menu.token.request';
1090
1248
  if (isTokenRequest) {
1091
1249
  if (msg.chatType !== 'private') {
@@ -1104,7 +1262,7 @@ export class MessageBridge {
1104
1262
  }
1105
1263
  }
1106
1264
  if (!isTokenRequest) {
1107
- const access = authorizeAccess(authSubject);
1265
+ const access = authorizeAccess(menuAuthSubject);
1108
1266
  if (!access.allow) {
1109
1267
  this.logMenuDiagnostic('access-denied', channel, msg, parsed, access.code);
1110
1268
  await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
@@ -1119,7 +1277,7 @@ export class MessageBridge {
1119
1277
  let afterResponse;
1120
1278
  const deduped = await this.menuDeduper.execute(dedupKey, menuPayloadFingerprint(parsed.raw), () => isTokenRequest
1121
1279
  ? this.issueMenuTokenResponse(header, channel, msg)
1122
- : this.dispatchMenuRequest(parsed.request, header, channel, msg, authSubject, effect => { afterResponse = effect; }));
1280
+ : this.dispatchMenuRequest(parsed.request, header, channel, msg, menuAuthSubject, effect => { afterResponse = effect; }));
1123
1281
  if ('conflict' in deduped) {
1124
1282
  this.logMenuDiagnostic('request-conflict', channel, msg, parsed, 'CONFLICT');
1125
1283
  await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
@@ -1130,6 +1288,8 @@ export class MessageBridge {
1130
1288
  }
1131
1289
  if (deduped.replayed)
1132
1290
  this.logMenuDiagnostic('request-replay', channel, msg, parsed);
1291
+ if (deduped.retry)
1292
+ this.logMenuDiagnostic('request-retry', channel, msg, parsed);
1133
1293
  const response = responseWarning && 'data' in deduped.value
1134
1294
  ? { ...deduped.value, warning: responseWarning }
1135
1295
  : deduped.value;
@@ -1152,29 +1312,73 @@ export class MessageBridge {
1152
1312
  const args = request.args;
1153
1313
  switch (request.type) {
1154
1314
  case 'menu.list':
1315
+ if (args?.view === 'catalog') {
1316
+ validateMenuCatalogArgs(args);
1317
+ const data = await this.cmdHandler.getMenuCatalog(channel, msg.channelId, authSubject, args.context, msg.isControlChannel ? 'control' : 'agent');
1318
+ return menuSuccess(header, data);
1319
+ }
1320
+ if (args?.view !== undefined) {
1321
+ return menuFailure(header, { code: 'NOT_SUPPORTED', message: `Unsupported menu.list view: ${String(args.view)}` });
1322
+ }
1155
1323
  return menuSuccess(header, this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent', authSubject));
1156
1324
  case 'menu.query': {
1157
- const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, args, msg.chatType, msg.isControlChannel ?? false, identity, authSubject);
1325
+ if (name === 'menu' && args?.view === 'values') {
1326
+ validateMenuValueBatchArgs(args);
1327
+ const data = await this.cmdHandler.execMenuValueBatch(channel, msg.channelId, authSubject, args, args.context, msg.isControlChannel ? 'control' : 'agent');
1328
+ return menuSuccess(header, data);
1329
+ }
1330
+ if (name === 'menu') {
1331
+ return menuFailure(header, { code: 'NOT_SUPPORTED', message: `Unsupported menu.query view: ${String(args?.view)}` });
1332
+ }
1333
+ const execution = await resolveExplicitMenuExecutionContext({
1334
+ subject: authSubject,
1335
+ channelType: authSubject.channelType,
1336
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1337
+ channelId: msg.channelId,
1338
+ args,
1339
+ });
1340
+ const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject.identity, execution.subject);
1158
1341
  if ('error' in result)
1159
1342
  throw result;
1160
1343
  return menuSuccess(header, result.data);
1161
1344
  }
1162
1345
  case 'menu.options': {
1163
- let optionArgs = args;
1346
+ const execution = await resolveExplicitMenuExecutionContext({
1347
+ subject: authSubject,
1348
+ channelType: authSubject.channelType,
1349
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1350
+ channelId: msg.channelId,
1351
+ args,
1352
+ });
1353
+ let optionArgs = execution.args;
1164
1354
  if ((name === 'topic' || cmd === '/topic') && args?.mode === 'fork-turns' && msg.threadId && !args?.sourceThreadId) {
1165
- optionArgs = { ...(args ?? {}), sourceThreadId: msg.threadId };
1355
+ optionArgs = { ...(execution.args ?? {}), sourceThreadId: msg.threadId };
1166
1356
  }
1167
- const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, optionArgs, identity, msg.chatType, msg.isControlChannel ?? false, authSubject) ?? [];
1357
+ const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, optionArgs, execution.subject.identity, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject) ?? [];
1168
1358
  return menuSuccess(header, data);
1169
1359
  }
1170
1360
  case 'menu.update': {
1171
- const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, msg.channelId, msg.peerId, identity, msg.isControlChannel ?? false, args, authSubject);
1361
+ const execution = await resolveExplicitMenuExecutionContext({
1362
+ subject: authSubject,
1363
+ channelType: authSubject.channelType,
1364
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1365
+ channelId: msg.channelId,
1366
+ args,
1367
+ });
1368
+ const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.fromControlChannel, execution.args, execution.subject);
1172
1369
  if ('error' in result)
1173
1370
  throw result;
1174
1371
  return menuSuccess(header, result.data);
1175
1372
  }
1176
1373
  case 'menu.action': {
1177
- let actionArgs = args;
1374
+ const execution = await resolveExplicitMenuExecutionContext({
1375
+ subject: authSubject,
1376
+ channelType: authSubject.channelType,
1377
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1378
+ channelId: msg.channelId,
1379
+ args,
1380
+ });
1381
+ let actionArgs = execution.args;
1178
1382
  if ((name === 'topic' || cmd === '/topic') && request.action === 'fork' && msg.threadId) {
1179
1383
  if (typeof args?.targetThreadId === 'string' && args.targetThreadId !== msg.threadId) {
1180
1384
  return menuFailure(header, {
@@ -1182,11 +1386,11 @@ export class MessageBridge {
1182
1386
  message: 'args.targetThreadId 必须与消息 thread_id 一致',
1183
1387
  });
1184
1388
  }
1185
- actionArgs = { ...(args ?? {}), targetThreadId: msg.threadId };
1389
+ actionArgs = { ...(execution.args ?? {}), targetThreadId: msg.threadId };
1186
1390
  }
1187
1391
  const resolvedCmd = this.resolveCmd(name, cmd);
1188
1392
  const isFileFetch = resolvedCmd.trim().split(' ')[0] === '/file' && request.action === 'fetch';
1189
- const result = await this.cmdHandler.execMenuAction(resolvedCmd, request.action, actionArgs, channel, msg.channelId, msg.peerId, identity, msg.chatType, header.id, msg.isControlChannel ?? false, authSubject, 'menu', msg.replyContext, isFileFetch ? registerAfterResponse : undefined);
1393
+ const result = await this.cmdHandler.execMenuAction(resolvedCmd, request.action, actionArgs, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.chatType, header.id, execution.subject.fromControlChannel, execution.subject, 'menu', msg.replyContext, isFileFetch ? registerAfterResponse : undefined);
1190
1394
  if ('error' in result)
1191
1395
  throw result;
1192
1396
  return menuSuccess(header, result.data);
@@ -1222,6 +1426,10 @@ export class MessageBridge {
1222
1426
  ...evolMenuResponseTransportMetadata(),
1223
1427
  },
1224
1428
  };
1429
+ if (msg.channelType === 'aun' && !replyContext.delivery) {
1430
+ logger.warn(`[MessageBridge] dropping AUN menu response without trusted delivery route: channel=${channel} channelId=${msg.channelId}`);
1431
+ return;
1432
+ }
1225
1433
  const envelope = buildEnvelope({
1226
1434
  taskId: `menu-${randomBytes(4).toString('hex')}`,
1227
1435
  channel,
@@ -1280,6 +1488,8 @@ export class MessageBridge {
1280
1488
  logger.warn(`[MenuControl] category=${category} request=${parsed.id || '<missing>'}`
1281
1489
  + ` type=${parsed.type} name=${parsed.name || '<none>'} action=${parsed.action || '<none>'}`
1282
1490
  + ` transport=${channel} scope=${this.shortHash(scope)} sender=${this.shortHash(msg.peerId)}`
1491
+ + (category === 'request-replay' ? ' replay=true' : '')
1492
+ + (category === 'request-retry' ? ' retry=true' : '')
1283
1493
  + `${code ? ` code=${code}` : ''}${decision.suppressed ? ` suppressed=${decision.suppressed}` : ''}`);
1284
1494
  }
1285
1495
  shortHash(value) {
@@ -1398,17 +1608,17 @@ export class MessageBridge {
1398
1608
  * 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
1399
1609
  * @returns true 如果找到并取消/中断
1400
1610
  */
1401
- cancel(messageId) {
1611
+ cancel(messageId, selfAID) {
1402
1612
  // 阶段 1: debounce 窗口(尚未入队)
1403
1613
  for (const d of this.debouncers.values()) {
1404
- if (d.cancel(messageId))
1614
+ if (d.cancel(messageId, selfAID))
1405
1615
  return true;
1406
1616
  }
1407
1617
  // 阶段 2: 已入队但未处理(合并后 messageId 可能是逗号分隔的多个 id)
1408
- if (this.messageQueue.cancel(messageId))
1618
+ if (this.messageQueue.cancel(messageId, selfAID))
1409
1619
  return true;
1410
1620
  // 阶段 3: 正在执行的任务 → 触发 interrupt
1411
- return this.messageQueue.cancelActive(messageId);
1621
+ return this.messageQueue.cancelActive(messageId, selfAID);
1412
1622
  }
1413
1623
  /** 清理资源 */
1414
1624
  dispose() {
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { appendJsonl, chatDirPath } from '../session/session-fs-store.js';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  const TRANSIENT_PROTOCOL_TYPE_PREFIXES = [
6
+ 'contact.request.',
6
7
  'menu.',
7
8
  'status.',
8
9
  'event.',
@@ -288,6 +289,8 @@ export function buildInboundEntry(opts) {
288
289
  const isCommand = isCommandCapable && opts.content.startsWith('/');
289
290
  return {
290
291
  ts,
292
+ ...(opts.receivedAt !== undefined ? { receivedAt: opts.receivedAt } : {}),
293
+ ...(opts.gatewaySeq !== undefined ? { gatewaySeq: opts.gatewaySeq } : {}),
291
294
  time: formatTimestampMs(ts),
292
295
  sessionId: opts.sessionId,
293
296
  threadId: opts.threadId ?? null,