evolcore 0.0.12 → 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 (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
@@ -1,6 +1,23 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { BASEAGENT_CAPS } from './baseagent-caps.js';
3
+ export const BASEAGENT_CAPS = {
4
+ 'claude-code': {
5
+ autoLoadsRules: true,
6
+ supportsSystemPrompt: true,
7
+ },
8
+ 'claude': {
9
+ autoLoadsRules: true,
10
+ supportsSystemPrompt: true,
11
+ },
12
+ 'codex': {
13
+ autoLoadsRules: false,
14
+ supportsSystemPrompt: true,
15
+ },
16
+ 'gemini': {
17
+ autoLoadsRules: false,
18
+ supportsSystemPrompt: true,
19
+ },
20
+ };
4
21
  const MAX_DEPTH = 5;
5
22
  export function resolveEckInjection(agentConfig, projectPath, kitsRulesPath) {
6
23
  const caps = BASEAGENT_CAPS[agentConfig.baseAgent]
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { createHash } from 'crypto';
4
3
  import { TextDecoder } from 'util';
5
4
  import { getAidStore, isValidAid, loadClient, SLOT } from '../aun/aid/index.js';
5
+ import { downloadGroupFsBytes, GroupFsDownloadError } from '../aun/group-fs-download.js';
6
6
  import { checkGroupIndex as checkGroupIndexRpc, getGroupIndex as getGroupIndexRpc } from '../aun/msg/group-index.js';
7
7
  import { agentRelationsDir } from '../paths.js';
8
8
  import { formatPeerKey } from '../core/relation/peer-identity.js';
@@ -220,7 +220,9 @@ function lazyGroupClient(selfAid) {
220
220
  client: async () => {
221
221
  if (client)
222
222
  return client;
223
- store = await getAidStore({ slotId: SLOT.daemon });
223
+ // This short-lived RPC client must never share the daemon's gateway
224
+ // cursor. A short connection can perform a background pull on connect.
225
+ store = await getAidStore({ slotId: SLOT.groupRules });
224
226
  client = await loadClient(store, selfAid);
225
227
  await client.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
226
228
  return client;
@@ -242,39 +244,15 @@ function lazyGroupClient(selfAid) {
242
244
  };
243
245
  }
244
246
  async function downloadGroupFileBytes(client, remoteRef) {
245
- const ticket = await client.call('group.fs.create_download_ticket', { path: remoteRef });
246
- if (!isRecord(ticket)) {
247
- throw statusError('unreadable', 'group.fs.create_download_ticket returned invalid response');
248
- }
249
- const downloadUrl = stringField(ticket, ['download_url', 'url']);
250
- if (!downloadUrl) {
251
- throw statusError('unreadable', 'group.fs.create_download_ticket did not return download_url');
252
- }
253
- const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
254
- const expectedSha = stringField(ticket, ['sha256']);
255
- if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
256
- throw statusError('unreadable', 'download hash verification failed');
247
+ try {
248
+ return (await downloadGroupFsBytes(client, remoteRef)).bytes;
257
249
  }
258
- return bytes;
259
- }
260
- function sha256Hex(bytes) {
261
- return createHash('sha256').update(bytes).digest('hex');
262
- }
263
- function bearerHeaders(client) {
264
- const token = accessTokenFromClient(client);
265
- return token ? { Authorization: `Bearer ${token}` } : undefined;
266
- }
267
- function accessTokenFromClient(client) {
268
- const anyClient = client;
269
- if (typeof anyClient.getAccessToken === 'function') {
270
- const token = stringValue(anyClient.getAccessToken());
271
- if (token)
272
- return token;
250
+ catch (error) {
251
+ if (error instanceof GroupFsDownloadError) {
252
+ throw statusError('unreadable', error.message);
253
+ }
254
+ throw error;
273
255
  }
274
- return stringValue(anyClient.accessToken)
275
- || stringValue(anyClient.access_token)
276
- || stringValue(anyClient._identity?.access_token)
277
- || stringValue(anyClient._sessionParams?.access_token);
278
256
  }
279
257
  function assertUtf8(bytes) {
280
258
  new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -302,6 +280,10 @@ function classifyError(e) {
302
280
  if (e instanceof StatusError)
303
281
  return e.status;
304
282
  const msg = errorMessage(e).toLowerCase();
283
+ // A peer cache miss is a signer/identity lookup failure, not a missing
284
+ // rules file.
285
+ if (msg.includes('peer not found in cache'))
286
+ return 'unreadable';
305
287
  if (msg.includes('forbidden') || msg.includes('unauthorized') || msg.includes('permission'))
306
288
  return 'forbidden';
307
289
  if (msg.includes('not found') || msg.includes('does not exist'))
@@ -314,17 +296,6 @@ function errorMessage(e) {
314
296
  function isRecord(x) {
315
297
  return typeof x === 'object' && x !== null && !Array.isArray(x);
316
298
  }
317
- function stringValue(x) {
318
- return typeof x === 'string' ? x : '';
319
- }
320
- function stringField(obj, keys) {
321
- for (const k of keys) {
322
- const v = obj[k];
323
- if (typeof v === 'string')
324
- return v;
325
- }
326
- return undefined;
327
- }
328
299
  function numberField(obj, keys) {
329
300
  for (const k of keys) {
330
301
  const v = obj[k];
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // 【重要】最先加载环境变量,确保后续模块初始化时可用
2
2
  import dotenv from 'dotenv';
3
3
  import path from 'path';
4
- import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath, eckDebugDir, daemonControlDir, daemonQueuePath } from './paths.js';
4
+ import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath, eckDebugDir, daemonControlDir, daemonQueuePath, daemonMessageDedupPath } from './paths.js';
5
5
  // 立即加载 .env 文件(在其他模块导入之前)
6
6
  try {
7
7
  dotenv.config({ path: path.join(resolvePaths().root, '.env') });
@@ -14,16 +14,13 @@ import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-f
14
14
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
15
15
  import { EcagentSessionFileAdapter } from './core/session/adapters/ecagent-session-file-adapter.js';
16
16
  import { loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig } from './config-store.js';
17
- import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig } from './config/config-manager.js';
18
- import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
19
- import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
17
+ import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig, shouldFailFastForMissingOwners } from './config/config-manager.js';
18
+ import { ensureRoleConfigV4OnStartup, ensureRoleConfigV5OnStartup } from './config/role-migration-startup.js';
20
19
  import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
21
20
  import { resolvePeerDisplayLabel } from './config/contact-book.js';
22
21
  import { expirePendingContactRequests } from './config/contact-request-service.js';
23
- import { shouldFailFastForMissingOwners } from './config/owner-policy.js';
24
22
  import { isManagementRole } from './config/builtin-roles.js';
25
23
  import { checkRoleAccess, getFirstStaticAgentOwner, resolvePeerRoleDetail, roleToSessionIdentity } from './config/peer-role-resolver.js';
26
- import { isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation } from './core/auth/trigger-authorization.js';
27
24
  import { ensureStartupConfigSnapshot, isConfigSnapshotsEnabled, retentionCleanup, readCurrent, readWVersion, diffWorkingVsVersion, paramDiff, incrementSuccessCount } from './config/snapshot.js';
28
25
  import { appendBootLog, selfDiagnose } from './config/boot-log.js';
29
26
  import { CONFIG_SCHEMA_VERSION } from './types.js';
@@ -41,7 +38,7 @@ import { BindService } from './utils/aid-bind.js';
41
38
  import { DingtalkChannelPlugin, registerPendingDingtalkContactBind } from './channels/dingtalk.js';
42
39
  import { QQBotChannelPlugin, registerPendingQQBotContactBind } from './channels/qqbot.js';
43
40
  import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/wecom.js';
44
- import { buildEnvelope } from './core/message/message-utils.js';
41
+ import { buildEnvelope, isDeliveryTargetForChannel, replyContextFromSession } from './core/message/message-utils.js';
45
42
  import { ResponseEngine } from './core/message/response-engine.js';
46
43
  import { MessageQueue } from './core/message/message-queue.js';
47
44
  import { MessageBridge } from './core/message/message-bridge.js';
@@ -58,10 +55,9 @@ import { getEventCatalog } from './core/event-catalog.js';
58
55
  import { StatsCollector } from './utils/stats.js';
59
56
  import { AidStatsCollector } from './utils/stats.js';
60
57
  import { PermissionGateway } from './core/permission/approval-gateway.js';
61
- import { InteractionRouter } from './core/interaction-router.js';
62
- import { registerAdapterInteractions } from './core/interaction-registration.js';
58
+ import { InteractionRouter, registerAdapterInteractions } from './core/interaction-router.js';
63
59
  import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend } from './core/auth/agent-delegation.js';
64
- import { authorizeOperation, buildAuthSubject } from './core/auth/auth-gateway.js';
60
+ import { authorizeOperation, buildAuthSubject, isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation, } from './core/auth/auth-gateway.js';
65
61
  import { parsePeerKey } from './core/relation/peer-identity.js';
66
62
  import { isHClassPath } from './core/protected-paths.js';
67
63
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
@@ -72,7 +68,8 @@ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reloa
72
68
  import { buildReloadHooks } from './core/channel-loader.js';
73
69
  import { IpcServer } from './ipc.js';
74
70
  import { logger, setLogLevel } from './utils/logger.js';
75
- import { ecwebVersionRequirementError, fetchEcwebPairCode } from './utils/ecweb-utils.js';
71
+ import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
72
+ import { createEcwebSupervisor } from './utils/ecweb-supervisor.js';
76
73
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
77
74
  import { detectDuplicates } from './core/evolagent-registry.js';
78
75
  import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
@@ -96,8 +93,6 @@ import { MAIN_PACKAGE_NAME } from './product.js';
96
93
  import fs from 'fs';
97
94
  import crypto from 'crypto';
98
95
  import { fileURLToPath } from 'url';
99
- import { spawn } from 'child_process';
100
- import * as platform from './utils/cross-platform.js';
101
96
  const controlMenuDeduper = new MenuRequestDeduper();
102
97
  /** 出站 payload 摘要(用于 channel-out.log) */
103
98
  function summarizeOutboundPayload(payload) {
@@ -176,6 +171,61 @@ function normalizeMessageSource(value) {
176
171
  ? value
177
172
  : 'daemon';
178
173
  }
174
+ /**
175
+ * Preserve the authenticated AUN inbound route for control-plane replies.
176
+ * Control callbacks must not reconstruct group/private semantics from an AID.
177
+ */
178
+ function controlReplyContext(opts, metadata) {
179
+ const supplied = opts.replyContext?.delivery;
180
+ if (supplied) {
181
+ const matchesInbound = opts.chatType === 'group'
182
+ ? supplied.chatType === 'group' && supplied.groupId === opts.groupId
183
+ : supplied.chatType === 'private';
184
+ if (!matchesInbound) {
185
+ throw Object.assign(new Error('AUN control reply route does not match its authenticated inbound context'), {
186
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
187
+ });
188
+ }
189
+ }
190
+ const delivery = supplied
191
+ ?? (opts.chatType === 'group'
192
+ ? (opts.groupId ? { chatType: 'group', groupId: opts.groupId } : undefined)
193
+ : { chatType: 'private' });
194
+ if (!delivery)
195
+ throw Object.assign(new Error('AUN control reply is missing a trusted groupId'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
196
+ return {
197
+ ...(opts.replyContext ?? {}),
198
+ delivery,
199
+ metadata: {
200
+ ...(opts.replyContext?.metadata ?? {}),
201
+ ...(opts.encrypted === undefined ? {} : { encrypted: opts.encrypted }),
202
+ ...(opts.protectedHeaders ? { protectedHeaders: opts.protectedHeaders } : {}),
203
+ ...(metadata ?? {}),
204
+ },
205
+ };
206
+ }
207
+ function isValidAunDeliveryForChannel(delivery, channelId) {
208
+ return isDeliveryTargetForChannel(delivery, channelId);
209
+ }
210
+ function restartChannelType(channel) {
211
+ return channel?.split('#', 1)[0].toLowerCase();
212
+ }
213
+ /**
214
+ * Restore a persisted restart route without inferring AUN group semantics from
215
+ * an address. Legacy non-AUN entries may remain route-less; AUN entries do not.
216
+ */
217
+ export function resolvePersistedRestartDelivery(opts) {
218
+ const requiresAunRoute = restartChannelType(opts.channel) === 'aun'
219
+ || restartChannelType(opts.adapterChannel) === 'aun';
220
+ if (!requiresAunRoute) {
221
+ return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
222
+ ? { valid: true, delivery: opts.delivery }
223
+ : { valid: true };
224
+ }
225
+ return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
226
+ ? { valid: true, delivery: opts.delivery }
227
+ : { valid: false };
228
+ }
179
229
  function daemonConversationWatchdogMs(settings) {
180
230
  const idleTimeoutSec = settings.idleMonitor?.timeout;
181
231
  const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
@@ -205,6 +255,10 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
205
255
  threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
206
256
  peerId,
207
257
  sessionKey: sameConversation && !isGroup ? session.sessionKey : undefined,
258
+ // A group-created trigger intentionally feeds back to the authenticated
259
+ // actor's private conversation (the origin channelId is peerId), so the
260
+ // route remains explicit even though the source session was a group.
261
+ delivery: { chatType: 'private' },
208
262
  };
209
263
  }
210
264
  function seedUpgradeCheckTrigger(manager, owner) {
@@ -437,7 +491,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
437
491
  }
438
492
  const response = parsed ? await bindService.handleRequest(parsed, opts.peerId) : null;
439
493
  if (response) {
440
- await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
494
+ await controlChannel.sendMessage(opts.channelId, JSON.stringify(response), controlReplyContext(opts));
441
495
  }
442
496
  });
443
497
  const controlToken = crypto.randomBytes(32).toString('base64url');
@@ -789,6 +843,23 @@ async function main() {
789
843
  '如需远程管理,请在 daemon.json 配置 owners: [<你的 AID>]');
790
844
  }
791
845
  const paths = resolvePaths();
846
+ // ECWeb has one lifecycle owner: the daemon. The CLI may have started an
847
+ // instance just before spawning us; the supervisor adopts it when healthy,
848
+ // otherwise starts one and keeps it alive for the daemon's lifetime.
849
+ let ecwebSupervisor = null;
850
+ if (daemonCfg.ecweb?.enabled) {
851
+ ecwebSupervisor = createEcwebSupervisor({
852
+ port: daemonCfg.ecweb.port ?? 42705,
853
+ logger,
854
+ });
855
+ try {
856
+ await ecwebSupervisor.start();
857
+ onShutdown(() => ecwebSupervisor?.stop());
858
+ }
859
+ catch (error) {
860
+ logger.warn(`ECWeb supervisor 启动失败(不影响 daemon 主流程): ${error?.message || error}`);
861
+ }
862
+ }
792
863
  if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
793
864
  await runBindBootstrapDaemon(daemonCfg);
794
865
  return;
@@ -1091,7 +1162,7 @@ async function main() {
1091
1162
  if (!adapter)
1092
1163
  return;
1093
1164
  if (text) {
1094
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
1165
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, delivery: opts?.delivery, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
1095
1166
  }
1096
1167
  };
1097
1168
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
@@ -1120,6 +1191,7 @@ async function main() {
1120
1191
  await processor.processMessage(message);
1121
1192
  }, {
1122
1193
  persistencePath: daemonQueuePath(),
1194
+ dedupPath: daemonMessageDedupPath(),
1123
1195
  });
1124
1196
  // 设置中断回调(精确中断正在处理的 agent)
1125
1197
  messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message') => {
@@ -1160,12 +1232,20 @@ async function main() {
1160
1232
  source: 'msg',
1161
1233
  handoffTrace: { version: 2, handoff_id: handoff.handoff_id },
1162
1234
  };
1163
- const result = targetSession.chatType === 'group'
1235
+ const delivery = handoff.request.delivery;
1236
+ if (!isValidAunDeliveryForChannel(delivery, targetSession.channelId)) {
1237
+ return {
1238
+ ok: false,
1239
+ error: 'handoff is missing a valid immutable AUN delivery route',
1240
+ };
1241
+ }
1242
+ const result = delivery.chatType === 'group'
1164
1243
  ? typeof channel.sendDaemonGroupMsg === 'function'
1165
1244
  ? await channel.sendDaemonGroupMsg({
1166
- groupId: targetSession.channelId,
1245
+ groupId: delivery.groupId,
1167
1246
  payload: { ...handoff.request.payload, [AUN_HANDOFF_MARKER_FIELD]: handoff.handoff_id },
1168
1247
  encrypt: handoff.request.encrypt,
1248
+ causation: handoff.causation,
1169
1249
  log,
1170
1250
  })
1171
1251
  : { ok: false, error: 'AUN channel does not support daemon group sends' }
@@ -1279,6 +1359,14 @@ async function main() {
1279
1359
  if (target && !getTriggerChannel(definition.agentAid, target.channelKey)) {
1280
1360
  throw new Error(`agent ${definition.agentAid} has no configured channel ${target.channelKey}`);
1281
1361
  }
1362
+ const channelType = tryParseChannelKey(target?.channelKey ?? '')?.type
1363
+ ?? (target?.channelKey?.split('#', 1)[0] || undefined);
1364
+ const validAunRoute = definition.feedback.strategy === 'target'
1365
+ ? definition.feedback.target?.chatType === 'private' || definition.feedback.target?.chatType === 'group'
1366
+ : !!definition.origin && isValidAunDeliveryForChannel(definition.origin.delivery, definition.origin.channelId);
1367
+ if (target && channelType === 'aun' && !validAunRoute) {
1368
+ throw Object.assign(new Error('AUN trigger feedback target requires a valid explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
1369
+ }
1282
1370
  };
1283
1371
  const validateTriggerDefinitionForActor = (definition, actor) => {
1284
1372
  if (definition.execution.type === 'script' && !actor.management) {
@@ -1520,9 +1608,13 @@ async function main() {
1520
1608
  && (candidate.adapter.channelKey === target.channelKey || candidate.adapter.channelName === target.channelKey)
1521
1609
  && tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === input.trigger.agentAid));
1522
1610
  const channel = inst?.channel;
1523
- if (!channel || typeof channel.isGroupId !== 'function')
1611
+ if (!channel)
1612
+ return null;
1613
+ // Trigger targets carry their transport classification explicitly. A
1614
+ // channelId is only an address and is never parsed to choose the RPC.
1615
+ const targetChatType = target.chatType;
1616
+ if (!targetChatType)
1524
1617
  return null;
1525
- const targetChatType = channel.isGroupId(target.channelId) ? 'group' : 'private';
1526
1618
  if (targetChatType === 'group' && typeof channel.sendDaemonGroupMsg !== 'function')
1527
1619
  return null;
1528
1620
  if (targetChatType === 'private' && typeof channel.sendDaemonMsg !== 'function')
@@ -1748,8 +1840,8 @@ async function main() {
1748
1840
  });
1749
1841
  }
1750
1842
  // 5. 撤回消息 → 中断执行中任务
1751
- inst.channel.onRecall?.((messageId) => {
1752
- msgBridge.cancel(messageId);
1843
+ inst.channel.onRecall?.((messageId, selfAID) => {
1844
+ msgBridge.cancel(messageId, selfAID);
1753
1845
  });
1754
1846
  }
1755
1847
  // ── 注册所有渠道实例 ──
@@ -1858,6 +1950,7 @@ async function main() {
1858
1950
  channel: adapter.channelName,
1859
1951
  channelId: ownerAid,
1860
1952
  agentName,
1953
+ delivery: { chatType: 'private' },
1861
1954
  });
1862
1955
  sendSystemPayload(adapter, envelope, {
1863
1956
  kind: 'system.notice',
@@ -1896,16 +1989,31 @@ async function main() {
1896
1989
  logger.info(`[Restart] Pending notification waits for channel connection: ${pending.channel}`);
1897
1990
  return;
1898
1991
  }
1899
- const replyContext = pending.rootId
1900
- ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
1901
- : undefined;
1992
+ const pendingDelivery = pending.delivery;
1993
+ const restartDelivery = resolvePersistedRestartDelivery({
1994
+ channel: pending.channel,
1995
+ adapterChannel: adapter.channelName,
1996
+ channelId: pending.channelId,
1997
+ delivery: pendingDelivery,
1998
+ });
1999
+ if (!restartDelivery.valid) {
2000
+ logger.warn(`[Restart] Discarding pending notification without a valid persisted delivery route: channel=${pending.channel} channelId=${pending.channelId}`);
2001
+ fs.rmSync(pendingFile, { force: true });
2002
+ return;
2003
+ }
2004
+ const pendingRouteContext = restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {};
2005
+ const replyContext = {
2006
+ ...(pendingRouteContext ?? {}),
2007
+ ...(pending.rootId ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId } : {}),
2008
+ };
1902
2009
  const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
1903
2010
  const envelope = buildEnvelope({
1904
2011
  taskId: `system-restart-${process.pid}`,
1905
2012
  channel: adapter.channelKey,
1906
2013
  channelId: pending.channelId,
1907
2014
  agentName: owningAgent?.aid || 'evolcore',
1908
- replyContext,
2015
+ ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
2016
+ replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
1909
2017
  });
1910
2018
  await sendSystemPayload(adapter, envelope, {
1911
2019
  kind: 'system.notice',
@@ -2115,53 +2223,6 @@ async function main() {
2115
2223
  catch (e) {
2116
2224
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2117
2225
  }
2118
- // ── ECWeb 自动启动 ──
2119
- // 如果 config.ecweb.enabled,自动拉起 ecweb 服务
2120
- if (daemonCfg.ecweb?.enabled) {
2121
- const ecwebPath = path.join(resolvePaths().root, 'ecweb');
2122
- const ecwebEntry = path.join(ecwebPath, 'dist', 'index.js');
2123
- if (fs.existsSync(ecwebEntry)) {
2124
- let ecwebVersion;
2125
- try {
2126
- const pkg = JSON.parse(fs.readFileSync(path.join(ecwebPath, 'package.json'), 'utf-8'));
2127
- ecwebVersion = pkg.version;
2128
- }
2129
- catch { }
2130
- const versionError = ecwebVersionRequirementError(ecwebVersion);
2131
- if (versionError) {
2132
- logger.error(`ECWeb 未启动: ${versionError}`);
2133
- }
2134
- else {
2135
- try {
2136
- const ecwebProc = spawn(process.execPath, [ecwebEntry], {
2137
- cwd: ecwebPath,
2138
- detached: true,
2139
- stdio: 'ignore',
2140
- windowsHide: platform.isWindows,
2141
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2142
- });
2143
- ecwebProc.unref();
2144
- logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
2145
- onShutdown(() => {
2146
- try {
2147
- if (ecwebProc.pid)
2148
- platform.killProcess(ecwebProc.pid, true);
2149
- logger.info(`✓ ECWeb 已停止`);
2150
- }
2151
- catch { }
2152
- });
2153
- }
2154
- catch (e) {
2155
- logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
2156
- }
2157
- }
2158
- }
2159
- else {
2160
- // 正式安装时 ECWeb 是独立的全局 ec-web 包,由 ec start/restart 的
2161
- // 启动链路管理;runtime 目录没有内嵌构建产物是正常状态。
2162
- logger.debug(`ECWeb runtime entry absent (managed externally): ${ecwebEntry}`);
2163
- }
2164
- }
2165
2226
  // 控制 AID 接收 owner 指令:
2166
2227
  // 1. /pair — ECWeb 配对码(文本快路径)
2167
2228
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
@@ -2176,22 +2237,20 @@ async function main() {
2176
2237
  catch {
2177
2238
  parsed = null;
2178
2239
  }
2240
+ const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2179
2241
  if (bindService && parsed?.type === 'bind.request') {
2180
2242
  const response = await bindService.handleRequest(parsed, opts.peerId);
2181
2243
  if (response) {
2182
2244
  // 用 sendStructured 直发 typed payload(payload.type='bind.response'),
2183
2245
  // 不能用 sendMessage——它会把内容包成 {type:'text', text:...},App 无法识别。
2184
2246
  // encrypted 跟随入站请求:bind.response 与 bind.request 的加密/明文对称。
2185
- await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
2247
+ await controlChannel.sendStructured(opts.channelId, response, controlReplyContext(opts));
2186
2248
  }
2187
2249
  return;
2188
2250
  }
2189
2251
  const menuControl = parseMenuControl(text);
2190
2252
  const isRoleMenu = menuControl.isMenu
2191
2253
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
2192
- const menuResponseContext = () => ({
2193
- metadata: evolMenuResponseTransportMetadata(),
2194
- });
2195
2254
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2196
2255
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
2197
2256
  return;
@@ -2216,7 +2275,7 @@ async function main() {
2216
2275
  else {
2217
2276
  reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
2218
2277
  }
2219
- await controlChannel.sendMessage(opts.channelId, reply);
2278
+ await controlChannel.sendMessage(opts.channelId, reply, controlReplyContext(opts));
2220
2279
  return;
2221
2280
  }
2222
2281
  // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
@@ -2235,6 +2294,16 @@ async function main() {
2235
2294
  return;
2236
2295
  }
2237
2296
  const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
2297
+ if ('retry' in deduped) {
2298
+ if (deduped.replayed) {
2299
+ logger.warn(`[MenuControl] category=request-replay request=${menuControl.id}`
2300
+ + ` sender=${opts.peerId} replay=true`);
2301
+ }
2302
+ if (deduped.retry) {
2303
+ logger.warn(`[MenuControl] category=request-retry request=${menuControl.id}`
2304
+ + ` sender=${opts.peerId} retry=true`);
2305
+ }
2306
+ }
2238
2307
  const response = 'conflict' in deduped
2239
2308
  ? menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'CONFLICT', message: 'Request ID was already used with a different payload' })
2240
2309
  : deduped.value;
@@ -2244,7 +2313,7 @@ async function main() {
2244
2313
  return;
2245
2314
  }
2246
2315
  // owner 发的其他内容:提示可用指令
2247
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
2316
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2248
2317
  }
2249
2318
  catch (e) {
2250
2319
  logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
@@ -2296,6 +2365,7 @@ async function main() {
2296
2365
  channel: other.adapter.channelKey,
2297
2366
  channelId: ownerId,
2298
2367
  agentName: owningAgent?.aid || 'evolcore',
2368
+ delivery: { chatType: 'private' },
2299
2369
  });
2300
2370
  sendSystemPayload(other.adapter, envelope, {
2301
2371
  kind: 'system.error',
@@ -2385,11 +2455,13 @@ async function main() {
2385
2455
  channelType: session.channelType || parsedResumeKey?.type,
2386
2456
  selfAID: resumeSelfAID,
2387
2457
  channelId: session.channelId,
2458
+ groupId: session.chatType === 'group' ? (session.metadata?.groupId || session.channelId) : undefined,
2459
+ chatType: session.chatType === 'group' ? 'group' : 'private',
2388
2460
  content: '服务已重启,请继续之前未完成的任务。',
2389
2461
  timestamp: Date.now(),
2390
2462
  peerId: '',
2391
2463
  threadId: session.threadId || undefined,
2392
- replyContext: session.metadata?.replyContext,
2464
+ replyContext: replyContextFromSession(session),
2393
2465
  };
2394
2466
  // 清除状态后入队(processMessage 会重新标记)
2395
2467
  sessionManager.clearProcessing(session.id);
@@ -2721,10 +2793,9 @@ async function main() {
2721
2793
  if (!ch) {
2722
2794
  return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2723
2795
  }
2724
- const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2725
- if ((params.scope === 'group') !== targetIsGroup) {
2726
- return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2727
- }
2796
+ // The IPC command already carries the caller-selected transport scope.
2797
+ // Never reclassify the target by parsing its AID string: a private AID may
2798
+ // look group-like and a group identifier may use a future format.
2728
2799
  const encrypt = typeof params.encrypt === 'boolean'
2729
2800
  ? params.encrypt
2730
2801
  : (typeof ch.getDefaultEncrypt === 'function' ? ch.getDefaultEncrypt() : true);
@@ -3180,7 +3251,7 @@ async function main() {
3180
3251
  const agentName = handle?.name;
3181
3252
  if (!agentName)
3182
3253
  return [];
3183
- return messageQueue.getQueueItemsByAgent(agentName);
3254
+ return messageQueue.getQueueItemsByAgent(agentName, { after: params.after, before: params.before });
3184
3255
  });
3185
3256
  ipcServer.setQueueActionExecutor(async (params) => {
3186
3257
  const handle = agentRegistry.get(params.agent);
@@ -3261,8 +3332,12 @@ async function main() {
3261
3332
  const handle = agentRegistry.get(authorization.aid);
3262
3333
  if (!handle?.name)
3263
3334
  return { ok: false, code: 'AGENT_NOT_FOUND', error: 'current Agent not found' };
3264
- if (!params.action)
3265
- return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name) };
3335
+ if (!params.action) {
3336
+ return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name, {
3337
+ after: params.after,
3338
+ before: params.before,
3339
+ }) };
3340
+ }
3266
3341
  if (params.action === 'clear')
3267
3342
  return { ok: true, cleared: messageQueue.clearByAgent(handle.name) };
3268
3343
  if (params.action === 'cancel') {
package/dist/ipc.js CHANGED
@@ -15,6 +15,7 @@ import { hashCurrentDelegatedCommand } from './core/auth/agent-delegation.js';
15
15
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
16
16
  const isWindows = process.platform === 'win32';
17
17
  const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
18
+ const finiteTimestamp = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
18
19
  const LOCAL_CONTROL_COMMANDS = new Set([
19
20
  'bind.begin',
20
21
  'bind.status',
@@ -32,6 +33,12 @@ const LOCAL_CONTROL_COMMANDS = new Set([
32
33
  'menu.exec',
33
34
  'monitor-snapshot',
34
35
  ]);
36
+ function requiresLocalControlToken(cmd) {
37
+ // Managed bootstrap completion is authenticated by the session delegation
38
+ // ticket. A standalone CLI invocation still needs the daemon control token.
39
+ return LOCAL_CONTROL_COMMANDS.has(String(cmd.type ?? ''))
40
+ || (cmd.type === 'evolagent.bootstrapComplete' && typeof cmd.sessionId !== 'string');
41
+ }
35
42
  export function summarizeMonitorAgents(agents, aids) {
36
43
  const connectedAgents = new Set(aids.filter(aid => aid.status === 'connected').map(aid => aid.agentName || aid.aid));
37
44
  const enabledAgents = agents.filter(agent => agent.status !== 'disabled');
@@ -379,7 +386,7 @@ export class IpcServer {
379
386
  }
380
387
  }
381
388
  async handleCommand(cmd) {
382
- if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && !this.hasValidControlToken(cmd.controlToken)) {
389
+ if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
383
390
  return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
384
391
  }
385
392
  switch (cmd.type) {
@@ -520,7 +527,11 @@ export class IpcServer {
520
527
  // 纯查询
521
528
  if (!this.queueSnapshotProvider)
522
529
  return { ok: false, error: 'queue-snapshot not configured' };
523
- return { ok: true, items: this.queueSnapshotProvider({ agent: cmd.agent }) };
530
+ return { ok: true, items: this.queueSnapshotProvider({
531
+ agent: cmd.agent,
532
+ after: finiteTimestamp(cmd.after),
533
+ before: finiteTimestamp(cmd.before),
534
+ }) };
524
535
  }
525
536
  // 操作:clear / cancel / interrupt
526
537
  if (!this.queueActionExecutor)
@@ -546,6 +557,8 @@ export class IpcServer {
546
557
  action: cmd.action,
547
558
  messageId: typeof cmd.messageId === 'string' ? cmd.messageId : undefined,
548
559
  sessionKey: typeof cmd.sessionKey === 'string' ? cmd.sessionKey : undefined,
560
+ after: finiteTimestamp(cmd.after),
561
+ before: finiteTimestamp(cmd.before),
549
562
  sessionId: cmd.sessionId,
550
563
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
551
564
  delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
@@ -1037,7 +1050,7 @@ export class IpcServer {
1037
1050
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1038
1051
  let authenticatedCmd = cmd;
1039
1052
  if (!process.env.EVOLCORE_SESSION_ID
1040
- && LOCAL_CONTROL_COMMANDS.has(cmd.type)
1053
+ && requiresLocalControlToken(cmd)
1041
1054
  && typeof cmd.controlToken !== 'string') {
1042
1055
  try {
1043
1056
  const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();