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
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,16 +38,16 @@ 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';
48
- import { MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
49
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata } from './core/command/evol-menu-version-gate.js';
45
+ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
50
46
  import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
51
47
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
52
48
  import { HandoffRuntime } from './core/handoff/runtime.js';
53
49
  import { BootstrapService, completeBootstrapWithWelcome } from './core/bootstrap-service.js';
50
+ import { postBootstrapWelcomeOperationId } from './core/bootstrap-messages.js';
54
51
  import { MessageCache } from './core/message/message-cache.js';
55
52
  import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
56
53
  import { EventBus } from './core/event-bus.js';
@@ -58,10 +55,10 @@ 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';
61
+ import { parsePeerKey } from './core/relation/peer-identity.js';
65
62
  import { isHClassPath } from './core/protected-paths.js';
66
63
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
67
64
  import { AgentLoader } from './core/baseagent-loader.js';
@@ -71,7 +68,8 @@ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reloa
71
68
  import { buildReloadHooks } from './core/channel-loader.js';
72
69
  import { IpcServer } from './ipc.js';
73
70
  import { logger, setLogLevel } from './utils/logger.js';
74
- import { ecwebVersionRequirementError, fetchEcwebPairCode } from './utils/ecweb-utils.js';
71
+ import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
72
+ import { createEcwebSupervisor } from './utils/ecweb-supervisor.js';
75
73
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
76
74
  import { detectDuplicates } from './core/evolagent-registry.js';
77
75
  import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
@@ -90,12 +88,11 @@ import { validateModelSelectionForRole } from './core/model/model-permission.js'
90
88
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
91
89
  import { atomicWriteJson } from './core/session/session-fs-store.js';
92
90
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
91
+ import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
93
92
  import { MAIN_PACKAGE_NAME } from './product.js';
94
93
  import fs from 'fs';
95
94
  import crypto from 'crypto';
96
95
  import { fileURLToPath } from 'url';
97
- import { spawn } from 'child_process';
98
- import * as platform from './utils/cross-platform.js';
99
96
  const controlMenuDeduper = new MenuRequestDeduper();
100
97
  /** 出站 payload 摘要(用于 channel-out.log) */
101
98
  function summarizeOutboundPayload(payload) {
@@ -174,6 +171,61 @@ function normalizeMessageSource(value) {
174
171
  ? value
175
172
  : 'daemon';
176
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
+ }
177
229
  function daemonConversationWatchdogMs(settings) {
178
230
  const idleTimeoutSec = settings.idleMonitor?.timeout;
179
231
  const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
@@ -203,11 +255,15 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
203
255
  threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
204
256
  peerId,
205
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' },
206
262
  };
207
263
  }
208
264
  function seedUpgradeCheckTrigger(manager, owner) {
209
265
  const now = Date.now();
210
- const scriptName = 'upgrade-check.sh';
266
+ const scriptName = 'upgrade-check.cjs';
211
267
  const definition = normalizeTriggerDefinition({
212
268
  $schema_version: 3.1,
213
269
  id: '__upgrade-check',
@@ -234,7 +290,7 @@ function seedUpgradeCheckTrigger(manager, owner) {
234
290
  type: 'script',
235
291
  script: {
236
292
  path: scriptName,
237
- runtime: 'bash',
293
+ runtime: 'node',
238
294
  timeoutMs: 60_000,
239
295
  },
240
296
  permissionMode: 'bypass',
@@ -260,67 +316,107 @@ function seedUpgradeCheckTrigger(manager, owner) {
260
316
  const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
261
317
  const packageJsonPath = path.join(packageRoot, 'package.json');
262
318
  const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
263
- fs.writeFileSync(scriptPath, [
264
- '#!/usr/bin/env bash',
265
- 'set -euo pipefail',
266
- '',
267
- `CLI_PATH=${JSON.stringify(cliPath)}`,
268
- `PACKAGE_JSON=${JSON.stringify(packageJsonPath)}`,
269
- `DEV_MODE=${devMode ? '1' : '0'}`,
270
- '',
271
- 'json() {',
272
- ' local outcome="$1"',
273
- ' local text="$2"',
274
- ' node -e \'console.log(JSON.stringify({ outcome: process.argv[1], text: process.argv[2], files: [] }))\' "$outcome" "$text"',
275
- '}',
276
- '',
277
- 'if [ "$DEV_MODE" = "1" ]; then',
278
- ' json "noop" "upgrade check skipped in dev mode"',
279
- ' exit 0',
280
- 'fi',
281
- '',
282
- 'local_version="$(node -e \'try { console.log(require(process.argv[1]).version || "") } catch { process.exit(0) }\' "$PACKAGE_JSON")"',
283
- `remote_version="$(npm view ${MAIN_PACKAGE_NAME} version 2>/dev/null || true)"`,
284
- '',
285
- 'if [ -z "$remote_version" ]; then',
286
- ' json "error" "failed to check evolcore latest version"',
287
- ' exit 0',
288
- 'fi',
289
- '',
290
- 'if [ -z "$local_version" ]; then',
291
- ' json "error" "failed to read local ec version"',
292
- ' exit 0',
293
- 'fi',
294
- '',
295
- 'version_cmp="$(node -e \'',
296
- 'const [a, b] = process.argv.slice(1);',
297
- 'const pa = a.split("-")[0].split(".").map(Number);',
298
- 'const pb = b.split("-")[0].split(".").map(Number);',
299
- 'const n = Math.max(pa.length, pb.length);',
300
- 'let out = 0;',
301
- 'for (let i = 0; i < n; i++) {',
302
- ' const av = Number.isFinite(pa[i]) ? pa[i] : 0;',
303
- ' const bv = Number.isFinite(pb[i]) ? pb[i] : 0;',
304
- ' if (av < bv) { out = -1; break; }',
305
- ' if (av > bv) { out = 1; break; }',
306
- '}',
307
- 'console.log(out);',
308
- '\' "$local_version" "$remote_version")"',
309
- '',
310
- 'if [ "$version_cmp" != "-1" ]; then',
311
- ' json "noop" "evolcore is already up to date ($local_version; latest $remote_version)"',
312
- ' exit 0',
313
- 'fi',
314
- '',
315
- 'if [ ! -f "$CLI_PATH" ]; then',
316
- ' json "error" "restart-monitor entry not found: $CLI_PATH"',
317
- ' exit 0',
318
- 'fi',
319
- '',
320
- 'nohup node "$CLI_PATH" restart-monitor >/dev/null 2>&1 &',
321
- 'json "success" "evolcore upgrade available: ${local_version:-unknown} -> $remote_version; restart-monitor started"',
322
- '',
323
- ].join('\n'));
319
+ fs.writeFileSync(scriptPath, `#!/usr/bin/env node
320
+ const { existsSync, readFileSync } = require('node:fs');
321
+ const { spawn } = require('node:child_process');
322
+
323
+ const CLI_PATH = ${JSON.stringify(cliPath)};
324
+ const PACKAGE_JSON = ${JSON.stringify(packageJsonPath)};
325
+ const PACKAGE_NAME = ${JSON.stringify(MAIN_PACKAGE_NAME)};
326
+ const DEV_MODE = ${devMode ? 'true' : 'false'};
327
+
328
+ function emit(outcome, text) {
329
+ process.stdout.write(JSON.stringify({ outcome, text, files: [] }));
330
+ }
331
+
332
+ function compareVersions(a, b) {
333
+ const pa = a.split('-')[0].split('.').map(Number);
334
+ const pb = b.split('-')[0].split('.').map(Number);
335
+ const count = Math.max(pa.length, pb.length);
336
+ for (let i = 0; i < count; i += 1) {
337
+ const av = Number.isFinite(pa[i]) ? pa[i] : 0;
338
+ const bv = Number.isFinite(pb[i]) ? pb[i] : 0;
339
+ if (av < bv) return -1;
340
+ if (av > bv) return 1;
341
+ }
342
+ return 0;
343
+ }
344
+
345
+ async function latestVersion() {
346
+ const controller = new AbortController();
347
+ const timer = setTimeout(() => controller.abort(), 10_000);
348
+ try {
349
+ const response = await fetch('https://registry.npmjs.org/' + encodeURIComponent(PACKAGE_NAME) + '/latest', {
350
+ signal: controller.signal,
351
+ headers: { Accept: 'application/json' },
352
+ });
353
+ if (!response.ok) return null;
354
+ const data = await response.json();
355
+ return typeof data.version === 'string' ? data.version : null;
356
+ } finally {
357
+ clearTimeout(timer);
358
+ }
359
+ }
360
+
361
+ async function main() {
362
+ if (DEV_MODE) {
363
+ emit('noop', 'upgrade check skipped in dev mode');
364
+ return;
365
+ }
366
+
367
+ let localVersion;
368
+ try {
369
+ localVersion = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')).version;
370
+ } catch {
371
+ emit('error', 'failed to read local ec version');
372
+ return;
373
+ }
374
+ if (typeof localVersion !== 'string' || !localVersion) {
375
+ emit('error', 'failed to read local ec version');
376
+ return;
377
+ }
378
+
379
+ let remoteVersion;
380
+ try {
381
+ remoteVersion = await latestVersion();
382
+ } catch {
383
+ remoteVersion = null;
384
+ }
385
+ if (!remoteVersion) {
386
+ emit('error', 'failed to check evolcore latest version');
387
+ return;
388
+ }
389
+ if (compareVersions(localVersion, remoteVersion) >= 0) {
390
+ emit('noop', 'evolcore is already up to date (' + localVersion + '; latest ' + remoteVersion + ')');
391
+ return;
392
+ }
393
+ if (!existsSync(CLI_PATH)) {
394
+ emit('error', 'restart-monitor entry not found: ' + CLI_PATH);
395
+ return;
396
+ }
397
+
398
+ try {
399
+ const child = spawn(process.execPath, [CLI_PATH, 'restart-monitor'], {
400
+ detached: true,
401
+ stdio: 'ignore',
402
+ windowsHide: process.platform === 'win32',
403
+ env: { ...process.env },
404
+ });
405
+ child.unref();
406
+ emit('success', 'evolcore upgrade available: ' + localVersion + ' -> ' + remoteVersion + '; restart-monitor started');
407
+ } catch (error) {
408
+ emit('error', 'failed to start restart-monitor: ' + (error && error.message ? error.message : String(error)));
409
+ }
410
+ }
411
+
412
+ main().catch((error) => {
413
+ emit('error', 'upgrade check failed: ' + (error && error.message ? error.message : String(error)));
414
+ });
415
+ `);
416
+ // Remove the pre-Node script left by older installations so it cannot be
417
+ // selected by a stale trigger definition or run manually by accident.
418
+ fs.rmSync(path.join(dir, 'upgrade-check.sh'), { force: true });
419
+ fs.rmSync(path.join(dir, 'upgrade-check.js'), { force: true });
324
420
  fs.chmodSync(scriptPath, 0o755);
325
421
  atomicWriteJson(manager.definitionPath(definition.id), definition);
326
422
  fs.rmSync(manager.activePath(definition.id), { force: true });
@@ -338,7 +434,9 @@ function removeLegacyAgentUpgradeCheck(manager) {
338
434
  return;
339
435
  const isLegacySystemOrigin = existing.origin?.channelKey === '__system__' || existing.origin?.channelKey === 'daemon';
340
436
  const isUpgradeScript = existing.execution.type === 'script'
341
- && existing.execution.script?.path === 'upgrade-check.sh';
437
+ && (existing.execution.script?.path === 'upgrade-check.sh'
438
+ || existing.execution.script?.path === 'upgrade-check.js'
439
+ || existing.execution.script?.path === 'upgrade-check.cjs');
342
440
  if (!isLegacySystemOrigin || !isUpgradeScript)
343
441
  return;
344
442
  fs.rmSync(manager.triggerDir('__upgrade-check'), { recursive: true, force: true });
@@ -393,9 +491,16 @@ async function runBindBootstrapDaemon(daemonCfg) {
393
491
  }
394
492
  const response = parsed ? await bindService.handleRequest(parsed, opts.peerId) : null;
395
493
  if (response) {
396
- await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
494
+ await controlChannel.sendMessage(opts.channelId, JSON.stringify(response), controlReplyContext(opts));
397
495
  }
398
496
  });
497
+ const controlToken = crypto.randomBytes(32).toString('base64url');
498
+ fs.mkdirSync(path.dirname(resolvePaths().controlToken), { recursive: true });
499
+ fs.writeFileSync(resolvePaths().controlToken, controlToken, { mode: 0o600 });
500
+ try {
501
+ fs.chmodSync(resolvePaths().controlToken, 0o600);
502
+ }
503
+ catch { }
399
504
  const ipcServer = new IpcServer(resolvePaths().socket, () => ({
400
505
  pid: process.pid,
401
506
  uptime: Math.round(process.uptime() * 1000),
@@ -417,6 +522,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
417
522
  };
418
523
  })(),
419
524
  }));
525
+ ipcServer.setControlToken(controlToken);
420
526
  ipcServer.setBindExecutor({
421
527
  begin: (cmd) => bindService.begin(cmd),
422
528
  status: (taskId) => bindService.status(taskId),
@@ -737,6 +843,23 @@ async function main() {
737
843
  '如需远程管理,请在 daemon.json 配置 owners: [<你的 AID>]');
738
844
  }
739
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
+ }
740
863
  if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
741
864
  await runBindBootstrapDaemon(daemonCfg);
742
865
  return;
@@ -1039,7 +1162,7 @@ async function main() {
1039
1162
  if (!adapter)
1040
1163
  return;
1041
1164
  if (text) {
1042
- 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' });
1043
1166
  }
1044
1167
  };
1045
1168
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
@@ -1068,6 +1191,7 @@ async function main() {
1068
1191
  await processor.processMessage(message);
1069
1192
  }, {
1070
1193
  persistencePath: daemonQueuePath(),
1194
+ dedupPath: daemonMessageDedupPath(),
1071
1195
  });
1072
1196
  // 设置中断回调(精确中断正在处理的 agent)
1073
1197
  messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message') => {
@@ -1108,12 +1232,20 @@ async function main() {
1108
1232
  source: 'msg',
1109
1233
  handoffTrace: { version: 2, handoff_id: handoff.handoff_id },
1110
1234
  };
1111
- 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'
1112
1243
  ? typeof channel.sendDaemonGroupMsg === 'function'
1113
1244
  ? await channel.sendDaemonGroupMsg({
1114
- groupId: targetSession.channelId,
1245
+ groupId: delivery.groupId,
1115
1246
  payload: { ...handoff.request.payload, [AUN_HANDOFF_MARKER_FIELD]: handoff.handoff_id },
1116
1247
  encrypt: handoff.request.encrypt,
1248
+ causation: handoff.causation,
1117
1249
  log,
1118
1250
  })
1119
1251
  : { ok: false, error: 'AUN channel does not support daemon group sends' }
@@ -1129,6 +1261,7 @@ async function main() {
1129
1261
  return { ok: result.ok, message_id: result.message_id, error: result.error };
1130
1262
  }, undefined, { queueTtlMs: taskExecutionTtlMs });
1131
1263
  responseEngine.setHandoffRuntime(handoffRuntime);
1264
+ cmdHandler.setHandoffRuntime(handoffRuntime);
1132
1265
  // Trigger runtime: daemon-level script + feedback scheduler.
1133
1266
  const triggerSchedulers = new Map();
1134
1267
  const targetSessionLocks = new TargetSessionLockRegistry();
@@ -1226,6 +1359,14 @@ async function main() {
1226
1359
  if (target && !getTriggerChannel(definition.agentAid, target.channelKey)) {
1227
1360
  throw new Error(`agent ${definition.agentAid} has no configured channel ${target.channelKey}`);
1228
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
+ }
1229
1370
  };
1230
1371
  const validateTriggerDefinitionForActor = (definition, actor) => {
1231
1372
  if (definition.execution.type === 'script' && !actor.management) {
@@ -1467,9 +1608,13 @@ async function main() {
1467
1608
  && (candidate.adapter.channelKey === target.channelKey || candidate.adapter.channelName === target.channelKey)
1468
1609
  && tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === input.trigger.agentAid));
1469
1610
  const channel = inst?.channel;
1470
- 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)
1471
1617
  return null;
1472
- const targetChatType = channel.isGroupId(target.channelId) ? 'group' : 'private';
1473
1618
  if (targetChatType === 'group' && typeof channel.sendDaemonGroupMsg !== 'function')
1474
1619
  return null;
1475
1620
  if (targetChatType === 'private' && typeof channel.sendDaemonMsg !== 'function')
@@ -1695,8 +1840,8 @@ async function main() {
1695
1840
  });
1696
1841
  }
1697
1842
  // 5. 撤回消息 → 中断执行中任务
1698
- inst.channel.onRecall?.((messageId) => {
1699
- msgBridge.cancel(messageId);
1843
+ inst.channel.onRecall?.((messageId, selfAID) => {
1844
+ msgBridge.cancel(messageId, selfAID);
1700
1845
  });
1701
1846
  }
1702
1847
  // ── 注册所有渠道实例 ──
@@ -1805,6 +1950,7 @@ async function main() {
1805
1950
  channel: adapter.channelName,
1806
1951
  channelId: ownerAid,
1807
1952
  agentName,
1953
+ delivery: { chatType: 'private' },
1808
1954
  });
1809
1955
  sendSystemPayload(adapter, envelope, {
1810
1956
  kind: 'system.notice',
@@ -1843,16 +1989,31 @@ async function main() {
1843
1989
  logger.info(`[Restart] Pending notification waits for channel connection: ${pending.channel}`);
1844
1990
  return;
1845
1991
  }
1846
- const replyContext = pending.rootId
1847
- ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
1848
- : 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
+ };
1849
2009
  const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
1850
2010
  const envelope = buildEnvelope({
1851
2011
  taskId: `system-restart-${process.pid}`,
1852
2012
  channel: adapter.channelKey,
1853
2013
  channelId: pending.channelId,
1854
2014
  agentName: owningAgent?.aid || 'evolcore',
1855
- replyContext,
2015
+ ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
2016
+ replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
1856
2017
  });
1857
2018
  await sendSystemPayload(adapter, envelope, {
1858
2019
  kind: 'system.notice',
@@ -1993,7 +2154,7 @@ async function main() {
1993
2154
  const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1994
2155
  if (!delegation.ok)
1995
2156
  return { ok: false, code: delegation.code, error: delegation.reason };
1996
- return cmdHandler.handleCtl(cmd, sessionId);
2157
+ return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
1997
2158
  });
1998
2159
  // Register every IPC executor/provider before exposing the endpoint. The
1999
2160
  // function declaration is hoisted, while its invocation remains here so a
@@ -2062,52 +2223,6 @@ async function main() {
2062
2223
  catch (e) {
2063
2224
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2064
2225
  }
2065
- // ── ECWeb 自动启动 ──
2066
- // 如果 config.ecweb.enabled,自动拉起 ecweb 服务
2067
- if (daemonCfg.ecweb?.enabled) {
2068
- const ecwebPath = path.join(resolvePaths().root, 'ecweb');
2069
- const ecwebEntry = path.join(ecwebPath, 'dist', 'index.js');
2070
- if (fs.existsSync(ecwebEntry)) {
2071
- let ecwebVersion;
2072
- try {
2073
- const pkg = JSON.parse(fs.readFileSync(path.join(ecwebPath, 'package.json'), 'utf-8'));
2074
- ecwebVersion = pkg.version;
2075
- }
2076
- catch { }
2077
- const versionError = ecwebVersionRequirementError(ecwebVersion);
2078
- if (versionError) {
2079
- logger.error(`ECWeb 未启动: ${versionError}`);
2080
- }
2081
- else {
2082
- try {
2083
- const ecwebProc = spawn('node', [ecwebEntry], {
2084
- cwd: ecwebPath,
2085
- detached: true,
2086
- stdio: 'ignore',
2087
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2088
- });
2089
- ecwebProc.unref();
2090
- logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
2091
- onShutdown(() => {
2092
- try {
2093
- if (ecwebProc.pid)
2094
- platform.killProcess(ecwebProc.pid, true);
2095
- logger.info(`✓ ECWeb 已停止`);
2096
- }
2097
- catch { }
2098
- });
2099
- }
2100
- catch (e) {
2101
- logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
2102
- }
2103
- }
2104
- }
2105
- else {
2106
- // 正式安装时 ECWeb 是独立的全局 ec-web 包,由 ec start/restart 的
2107
- // 启动链路管理;runtime 目录没有内嵌构建产物是正常状态。
2108
- logger.debug(`ECWeb runtime entry absent (managed externally): ${ecwebEntry}`);
2109
- }
2110
- }
2111
2226
  // 控制 AID 接收 owner 指令:
2112
2227
  // 1. /pair — ECWeb 配对码(文本快路径)
2113
2228
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
@@ -2122,22 +2237,20 @@ async function main() {
2122
2237
  catch {
2123
2238
  parsed = null;
2124
2239
  }
2240
+ const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2125
2241
  if (bindService && parsed?.type === 'bind.request') {
2126
2242
  const response = await bindService.handleRequest(parsed, opts.peerId);
2127
2243
  if (response) {
2128
2244
  // 用 sendStructured 直发 typed payload(payload.type='bind.response'),
2129
2245
  // 不能用 sendMessage——它会把内容包成 {type:'text', text:...},App 无法识别。
2130
2246
  // encrypted 跟随入站请求:bind.response 与 bind.request 的加密/明文对称。
2131
- await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
2247
+ await controlChannel.sendStructured(opts.channelId, response, controlReplyContext(opts));
2132
2248
  }
2133
2249
  return;
2134
2250
  }
2135
2251
  const menuControl = parseMenuControl(text);
2136
2252
  const isRoleMenu = menuControl.isMenu
2137
2253
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
2138
- const menuResponseContext = () => ({
2139
- metadata: evolMenuResponseTransportMetadata(),
2140
- });
2141
2254
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2142
2255
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
2143
2256
  return;
@@ -2162,7 +2275,7 @@ async function main() {
2162
2275
  else {
2163
2276
  reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
2164
2277
  }
2165
- await controlChannel.sendMessage(opts.channelId, reply);
2278
+ await controlChannel.sendMessage(opts.channelId, reply, controlReplyContext(opts));
2166
2279
  return;
2167
2280
  }
2168
2281
  // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
@@ -2181,6 +2294,16 @@ async function main() {
2181
2294
  return;
2182
2295
  }
2183
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
+ }
2184
2307
  const response = 'conflict' in deduped
2185
2308
  ? menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'CONFLICT', message: 'Request ID was already used with a different payload' })
2186
2309
  : deduped.value;
@@ -2190,7 +2313,7 @@ async function main() {
2190
2313
  return;
2191
2314
  }
2192
2315
  // owner 发的其他内容:提示可用指令
2193
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
2316
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2194
2317
  }
2195
2318
  catch (e) {
2196
2319
  logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
@@ -2242,6 +2365,7 @@ async function main() {
2242
2365
  channel: other.adapter.channelKey,
2243
2366
  channelId: ownerId,
2244
2367
  agentName: owningAgent?.aid || 'evolcore',
2368
+ delivery: { chatType: 'private' },
2245
2369
  });
2246
2370
  sendSystemPayload(other.adapter, envelope, {
2247
2371
  kind: 'system.error',
@@ -2259,6 +2383,9 @@ async function main() {
2259
2383
  const recoveryAids = triggerStartupAgents.map(agent => agent.aid);
2260
2384
  for (const aid of recoveryAids) {
2261
2385
  try {
2386
+ const discarded = await handoffRuntime.discardExpired([aid]);
2387
+ if (discarded > 0)
2388
+ logger.info(`[Handoff] discarded ${discarded} expired handoff(s) for ${aid}`);
2262
2389
  const expired = handoffRuntime.expireQueued([aid]);
2263
2390
  if (expired > 0)
2264
2391
  logger.info(`[Handoff] expired ${expired} stale queued delivery item(s) for ${aid}`);
@@ -2328,11 +2455,13 @@ async function main() {
2328
2455
  channelType: session.channelType || parsedResumeKey?.type,
2329
2456
  selfAID: resumeSelfAID,
2330
2457
  channelId: session.channelId,
2458
+ groupId: session.chatType === 'group' ? (session.metadata?.groupId || session.channelId) : undefined,
2459
+ chatType: session.chatType === 'group' ? 'group' : 'private',
2331
2460
  content: '服务已重启,请继续之前未完成的任务。',
2332
2461
  timestamp: Date.now(),
2333
2462
  peerId: '',
2334
2463
  threadId: session.threadId || undefined,
2335
- replyContext: session.metadata?.replyContext,
2464
+ replyContext: replyContextFromSession(session),
2336
2465
  };
2337
2466
  // 清除状态后入队(processMessage 会重新标记)
2338
2467
  sessionManager.clearProcessing(session.id);
@@ -2344,7 +2473,27 @@ async function main() {
2344
2473
  function configureIpc() {
2345
2474
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2346
2475
  ipcServer.setAgentRegistry(agentRegistry);
2347
- ipcServer.setBootstrapCompleteExecutor(async (aid) => {
2476
+ ipcServer.setControlToken(triggerControlToken);
2477
+ ipcServer.setBootstrapCompleteExecutor(async (aid, auth) => {
2478
+ if (auth?.sessionId) {
2479
+ const authorization = await authorizeManagedAgentOperation({
2480
+ sessionId: auth.sessionId,
2481
+ delegationToken: auth.delegationToken,
2482
+ delegationCommandHash: auth.delegationCommandHash,
2483
+ targetAid: aid,
2484
+ }, 'agent.bootstrapComplete');
2485
+ if (!authorization.ok)
2486
+ return authorization;
2487
+ }
2488
+ else {
2489
+ const providedToken = typeof auth?.controlToken === 'string' ? Buffer.from(auth.controlToken) : undefined;
2490
+ const expectedToken = Buffer.from(triggerControlToken);
2491
+ if (!providedToken
2492
+ || providedToken.length !== expectedToken.length
2493
+ || !crypto.timingSafeEqual(providedToken, expectedToken)) {
2494
+ return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
2495
+ }
2496
+ }
2348
2497
  const aunInstance = channelInstances.find(candidate => {
2349
2498
  if (candidate.channelType !== 'aun')
2350
2499
  return false;
@@ -2353,7 +2502,30 @@ async function main() {
2353
2502
  return owner?.aid === aid;
2354
2503
  });
2355
2504
  const welcome = aunInstance?.channel;
2356
- return completeBootstrapWithWelcome(bootstrapService, aid, welcome);
2505
+ const ownerAid = getFirstStaticAgentOwner(aid);
2506
+ const owningAgent = agentRegistry.get(aid);
2507
+ const sessions = aunInstance && ownerAid && owningAgent
2508
+ ? {
2509
+ ensurePostBootstrapMainSession: async () => {
2510
+ const channel = aunInstance.adapter.channelName;
2511
+ const active = await sessionManager.getActiveSession(channel, ownerAid);
2512
+ const ensured = await sessionManager.ensureNewMainSessionForOperation({
2513
+ channel,
2514
+ channelId: ownerAid,
2515
+ projectPath: active?.projectPath || owningAgent.projectPath || defaultProjectPath,
2516
+ baseagent: owningAgent.baseagent || active?.baseagent || primaryBaseagent,
2517
+ operationId: postBootstrapWelcomeOperationId(aid),
2518
+ identityMetadata: {
2519
+ ...(active?.metadata ?? {}),
2520
+ channelKey: aunInstance.adapter.channelKey,
2521
+ peerId: ownerAid,
2522
+ },
2523
+ });
2524
+ return { sessionId: ensured.session.id, created: ensured.created };
2525
+ },
2526
+ }
2527
+ : undefined;
2528
+ return completeBootstrapWithWelcome(bootstrapService, aid, welcome, sessions);
2357
2529
  });
2358
2530
  ipcServer.setDingtalkContactBindExecutor({
2359
2531
  register: (cmd) => registerPendingDingtalkContactBind(cmd),
@@ -2378,6 +2550,10 @@ async function main() {
2378
2550
  ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2379
2551
  ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2380
2552
  ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2553
+ ipcServer.setAidOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash));
2554
+ ipcServer.setGroupOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash));
2555
+ ipcServer.setMsgOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash));
2556
+ ipcServer.setFsOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash));
2381
2557
  cmdHandler.setDaemonStatusProvider(() => {
2382
2558
  const aidState = controlChannel?.getAidState?.();
2383
2559
  return {
@@ -2446,30 +2622,82 @@ async function main() {
2446
2622
  aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2447
2623
  });
2448
2624
  ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2449
- ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2450
- ipcServer.setHandoffStatusExecutor(async (params) => {
2451
- if (!params.sessionId)
2452
- return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2625
+ ipcServer.setCodexPreToolUseExecutor(async (params) => {
2626
+ for (const runner of new Set(agentMap.values())) {
2627
+ if (typeof runner?.evaluatePreToolUse !== 'function')
2628
+ continue;
2629
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput);
2630
+ if (result.applicable)
2631
+ return result;
2632
+ }
2633
+ return {
2634
+ ok: false,
2635
+ applicable: true,
2636
+ decision: 'deny',
2637
+ reason: 'No active EvolCore Codex runner accepted this managed hook request',
2638
+ };
2639
+ });
2640
+ const resolveManagedHandoffScope = async (params, operation) => {
2641
+ const delegation = agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
2642
+ if (!delegation.ok)
2643
+ return { ok: false, code: delegation.code, error: delegation.reason };
2453
2644
  const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2454
2645
  const session = await sessionManager.getSessionById(params.sessionId);
2455
2646
  const selfAid = runtime?.selfAid || session?.selfAID;
2456
- if (!selfAid)
2457
- return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2458
- return handoffRuntime.status(selfAid, params.handoffId)
2459
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2460
- });
2461
- const resolveHandoffQueryAid = async (params) => {
2647
+ if (!selfAid || !session) {
2648
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2649
+ }
2650
+ if (delegation.grant.selfAid !== selfAid) {
2651
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation self agent does not match current session' };
2652
+ }
2653
+ if (params.agent && params.agent !== selfAid) {
2654
+ return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2655
+ }
2656
+ let conversationId;
2657
+ try {
2658
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
2659
+ }
2660
+ catch {
2661
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation contains an invalid relation key' };
2662
+ }
2663
+ const subject = buildAuthSubject({
2664
+ selfAid,
2665
+ actorId: delegation.grant.actorId,
2666
+ channel: delegation.grant.channel,
2667
+ channelType: delegation.grant.channelType,
2668
+ channelId: conversationId,
2669
+ chatType: delegation.grant.chatType,
2670
+ conversationId,
2671
+ processOwners: daemonCfg.owners ?? [],
2672
+ });
2673
+ const managementRole = isManagementRole(subject.role);
2674
+ const decision = authorizeOperation({
2675
+ source: 'agent-tool',
2676
+ subject,
2677
+ intent: {
2678
+ operation,
2679
+ scope: managementRole ? 'agent' : 'relation',
2680
+ source: 'agent-tool',
2681
+ args: {
2682
+ self: selfAid,
2683
+ peer: conversationId,
2684
+ peerKey: delegation.grant.peerKey,
2685
+ },
2686
+ },
2687
+ auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
2688
+ });
2689
+ if (!decision.allow) {
2690
+ return {
2691
+ ok: false,
2692
+ code: decision.command?.code ?? 'ROLE_ACCESS_DENIED',
2693
+ error: decision.reason,
2694
+ };
2695
+ }
2696
+ return { ok: true, aid: selfAid, currentSessionId: params.sessionId, managementRole };
2697
+ };
2698
+ const resolveHandoffQueryScope = async (params, operation) => {
2462
2699
  if (params.sessionId) {
2463
- const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2464
- const session = await sessionManager.getSessionById(params.sessionId);
2465
- const selfAid = runtime?.selfAid || session?.selfAID;
2466
- if (!selfAid) {
2467
- return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2468
- }
2469
- if (params.agent && params.agent !== selfAid) {
2470
- return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2471
- }
2472
- return { ok: true, aid: selfAid };
2700
+ return resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, operation);
2473
2701
  }
2474
2702
  if (!params.agent) {
2475
2703
  return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
@@ -2477,23 +2705,56 @@ async function main() {
2477
2705
  if (!agentRegistry.get(params.agent)) {
2478
2706
  return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2479
2707
  }
2480
- return { ok: true, aid: params.agent };
2708
+ return { ok: true, aid: params.agent, currentSessionId: undefined, managementRole: true };
2481
2709
  };
2710
+ ipcServer.setHandoffReturnExecutor(async (params) => {
2711
+ if (!params.sessionId)
2712
+ return responseEngine.returnHandoffResult(params);
2713
+ const scope = await resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, 'ec.handoff.return');
2714
+ if (!scope.ok)
2715
+ return scope;
2716
+ return responseEngine.returnAuthorizedHandoffResult({ sessionId: params.sessionId, handoffId: params.handoffId, content: params.content }, scope.aid);
2717
+ });
2718
+ ipcServer.setHandoffStatusExecutor(async (params) => {
2719
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.status');
2720
+ if (!scope.ok)
2721
+ return scope;
2722
+ const status = handoffRuntime.status(scope.aid, params.handoffId);
2723
+ if (!status)
2724
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2725
+ if (!scope.managementRole
2726
+ && status.origin_session_id !== scope.currentSessionId
2727
+ && status.target_session_id !== scope.currentSessionId) {
2728
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found in the current session' };
2729
+ }
2730
+ return status;
2731
+ });
2482
2732
  ipcServer.setHandoffListExecutor(async (params) => {
2483
- const scope = await resolveHandoffQueryAid(params);
2733
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.list');
2484
2734
  if (!scope.ok)
2485
2735
  return scope;
2736
+ if (!scope.managementRole && params.filterSessionId && params.filterSessionId !== scope.currentSessionId) {
2737
+ return { ok: false, code: 'HANDOFF_SESSION_SCOPE_MISMATCH', error: 'only the current session can be queried' };
2738
+ }
2486
2739
  return handoffRuntime.listHandoffs({
2487
2740
  selfAid: scope.aid,
2488
2741
  state: params.state,
2489
- sessionId: params.filterSessionId,
2742
+ sessionId: scope.managementRole ? params.filterSessionId : scope.currentSessionId,
2490
2743
  limit: params.limit,
2491
2744
  });
2492
2745
  });
2493
2746
  ipcServer.setHandoffTraceExecutor(async (params) => {
2494
- const scope = await resolveHandoffQueryAid(params);
2747
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.trace');
2495
2748
  if (!scope.ok)
2496
2749
  return scope;
2750
+ if (!scope.managementRole) {
2751
+ const status = handoffRuntime.status(scope.aid, params.handoffId);
2752
+ if (!status
2753
+ || (status.origin_session_id !== scope.currentSessionId
2754
+ && status.target_session_id !== scope.currentSessionId)) {
2755
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found in the current session' };
2756
+ }
2757
+ }
2497
2758
  return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2498
2759
  ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2499
2760
  });
@@ -2532,10 +2793,9 @@ async function main() {
2532
2793
  if (!ch) {
2533
2794
  return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2534
2795
  }
2535
- const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2536
- if ((params.scope === 'group') !== targetIsGroup) {
2537
- return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2538
- }
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.
2539
2799
  const encrypt = typeof params.encrypt === 'boolean'
2540
2800
  ? params.encrypt
2541
2801
  : (typeof ch.getDefaultEncrypt === 'function' ? ch.getDefaultEncrypt() : true);
@@ -2565,12 +2825,27 @@ async function main() {
2565
2825
  if (!payload) {
2566
2826
  return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2567
2827
  }
2828
+ try {
2829
+ if (Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
2830
+ payload = { ...payload, mentions: normalizeAunMentionEntries(payload.mentions) };
2831
+ }
2832
+ if (params.scope === 'group' && params.mentions !== undefined) {
2833
+ const normalizedMentions = normalizeAunMentionEntries(params.mentions);
2834
+ if (normalizedMentions.length > 0) {
2835
+ payload = { ...payload, mentions: normalizedMentions };
2836
+ }
2837
+ }
2838
+ }
2839
+ catch (error) {
2840
+ return {
2841
+ ok: false,
2842
+ error: error instanceof Error ? error.message : String(error),
2843
+ code: error?.code ?? 'INVALID_MENTION_SCHEMA',
2844
+ };
2845
+ }
2568
2846
  if (params.thread && typeof payload.thread_id !== 'string') {
2569
2847
  payload = { ...payload, thread_id: params.thread };
2570
2848
  }
2571
- if (params.scope === 'group' && params.mentions?.length) {
2572
- payload = { ...payload, mentions: params.mentions };
2573
- }
2574
2849
  let targetSessionId;
2575
2850
  if (params.originSessionId && params.originMessageId) {
2576
2851
  try {
@@ -2824,7 +3099,7 @@ async function main() {
2824
3099
  agent.status = 'running';
2825
3100
  // 连接
2826
3101
  await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2827
- await handoffRuntime.recover([aid]);
3102
+ await handoffRuntime.recover([aid], { discardExpired: false });
2828
3103
  await ensureTriggerSchedulerStarted(agent);
2829
3104
  handoffRuntime.resumeAgent(aid);
2830
3105
  resumed = true;
@@ -2976,7 +3251,7 @@ async function main() {
2976
3251
  const agentName = handle?.name;
2977
3252
  if (!agentName)
2978
3253
  return [];
2979
- return messageQueue.getQueueItemsByAgent(agentName);
3254
+ return messageQueue.getQueueItemsByAgent(agentName, { after: params.after, before: params.before });
2980
3255
  });
2981
3256
  ipcServer.setQueueActionExecutor(async (params) => {
2982
3257
  const handle = agentRegistry.get(params.agent);
@@ -3003,6 +3278,92 @@ async function main() {
3003
3278
  return { ok: false, error: `unknown action: ${params.action}` };
3004
3279
  }
3005
3280
  });
3281
+ async function authorizeManagedAgentOperation(params, operation) {
3282
+ const delegation = agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
3283
+ if (!delegation.ok)
3284
+ return { ok: false, code: delegation.code, error: delegation.reason };
3285
+ const session = await sessionManager.getSessionById(params.sessionId);
3286
+ if (!session || session.selfAID !== delegation.grant.selfAid) {
3287
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation does not match the current session' };
3288
+ }
3289
+ if (params.targetAid && params.targetAid !== delegation.grant.selfAid) {
3290
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'only the current Agent can be targeted' };
3291
+ }
3292
+ let conversationId;
3293
+ try {
3294
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
3295
+ }
3296
+ catch {
3297
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation contains an invalid relation key' };
3298
+ }
3299
+ const subject = buildAuthSubject({
3300
+ selfAid: delegation.grant.selfAid,
3301
+ actorId: delegation.grant.actorId,
3302
+ channel: delegation.grant.channel,
3303
+ channelType: delegation.grant.channelType,
3304
+ channelId: conversationId,
3305
+ chatType: delegation.grant.chatType,
3306
+ conversationId,
3307
+ processOwners: daemonCfg.owners ?? [],
3308
+ });
3309
+ if (!isManagementRole(subject.role)) {
3310
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${operation} requires Agent admin permission` };
3311
+ }
3312
+ const decision = authorizeOperation({
3313
+ source: 'agent-tool',
3314
+ subject,
3315
+ intent: {
3316
+ operation,
3317
+ scope: operation === 'system.status' ? 'process' : 'agent',
3318
+ source: 'agent-tool',
3319
+ args: { self: delegation.grant.selfAid, aid: params.targetAid ?? delegation.grant.selfAid },
3320
+ },
3321
+ auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
3322
+ });
3323
+ return decision.allow
3324
+ ? { ok: true, aid: delegation.grant.selfAid }
3325
+ : { ok: false, code: decision.code, error: decision.reason };
3326
+ }
3327
+ ipcServer.setQueueOperationExecutor(async (params) => {
3328
+ const operation = `ec.queue.${params.action ?? 'read'}`;
3329
+ const authorization = await authorizeManagedAgentOperation(params, operation);
3330
+ if (!authorization.ok)
3331
+ return authorization;
3332
+ const handle = agentRegistry.get(authorization.aid);
3333
+ if (!handle?.name)
3334
+ return { ok: false, code: 'AGENT_NOT_FOUND', error: 'current Agent not found' };
3335
+ if (!params.action) {
3336
+ return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name, {
3337
+ after: params.after,
3338
+ before: params.before,
3339
+ }) };
3340
+ }
3341
+ if (params.action === 'clear')
3342
+ return { ok: true, cleared: messageQueue.clearByAgent(handle.name) };
3343
+ if (params.action === 'cancel') {
3344
+ if (!params.messageId)
3345
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'missing messageId' };
3346
+ return { ok: true, cancelled: messageQueue.cancelMessageById(handle.name, params.messageId) };
3347
+ }
3348
+ if (!params.sessionKey)
3349
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'missing sessionKey' };
3350
+ const targetSessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
3351
+ if (!targetSessionId)
3352
+ return { ok: false, code: 'SESSION_NOT_FOUND', error: `session not found: ${params.sessionKey}` };
3353
+ const targetSession = await sessionManager.getSessionById(targetSessionId);
3354
+ if (!targetSession || targetSession.selfAID !== authorization.aid) {
3355
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'session does not belong to the current Agent' };
3356
+ }
3357
+ return { ok: true, interrupted: await messageQueue.interruptBySession(targetSessionId) };
3358
+ });
3359
+ ipcServer.setStatusAuthorizationExecutor(async (params) => {
3360
+ const authorization = await authorizeManagedAgentOperation(params, 'system.status');
3361
+ return authorization.ok ? { ok: true } : authorization;
3362
+ });
3363
+ ipcServer.setStorageAuthorizationExecutor(async (params) => {
3364
+ const authorization = await authorizeManagedAgentOperation({ ...params, targetAid: params.aid }, params.operation);
3365
+ return authorization.ok ? { ok: true } : authorization;
3366
+ });
3006
3367
  ipcServer.setTriggerExecutor(async (cmd) => {
3007
3368
  const schedulerFor = (agentAid) => {
3008
3369
  const scheduler = triggerSchedulers.get(agentAid);