evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -828,6 +828,7 @@ export class SessionManager {
828
828
  }
829
829
  // Create new session
830
830
  const sessionMetadata = { ...(metadata || {}) };
831
+ sessionMetadata.__sessionCreatedAt = Date.now();
831
832
  const newIdentity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
832
833
  const resolvedBaseagent = baseagent || 'claude';
833
834
  const session = {
@@ -1111,6 +1112,33 @@ export class SessionManager {
1111
1112
  }
1112
1113
  return 'legacy_updated';
1113
1114
  }
1115
+ /**
1116
+ * Clear a main-session backend only when the durable latest snapshot still
1117
+ * points at the caller's expected ID. This is the recovery counterpart to
1118
+ * provider-side "session not found" errors and must never rotate a topic.
1119
+ */
1120
+ async clearMainSessionBackendIfMatches(sessionId, expectedAgentSessionId, metadataKeys = []) {
1121
+ const loaded = this.loadSessionForUpdate(sessionId);
1122
+ if (!loaded)
1123
+ return 'missing';
1124
+ const { current } = loaded;
1125
+ if (current.threadId)
1126
+ return 'topic';
1127
+ const currentId = normalizeBackendSessionId(current.agentSessionId);
1128
+ if (!currentId)
1129
+ return 'already_clear';
1130
+ if (currentId !== normalizeBackendSessionId(expectedAgentSessionId))
1131
+ return 'mismatch';
1132
+ current.agentSessionId = undefined;
1133
+ if (metadataKeys.length > 0 && current.metadata) {
1134
+ const metadata = { ...current.metadata };
1135
+ for (const key of metadataKeys)
1136
+ delete metadata[key];
1137
+ current.metadata = metadata;
1138
+ }
1139
+ this.persistSession(current, 'sync');
1140
+ return 'cleared';
1141
+ }
1114
1142
  async switchAgent(channel, channelId, projectPath, newBaseagent) {
1115
1143
  const inheritedChatType = this.getActiveChatType(channel, channelId);
1116
1144
  const identity = this.deriveChannelIdentity(channel, channelId);
@@ -1424,6 +1452,7 @@ export class SessionManager {
1424
1452
  throw new Error(`[SessionManager] createNewSession: baseagent is empty for channel=${channel} channelId=${channelId}`);
1425
1453
  }
1426
1454
  const metadata = newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata);
1455
+ metadata.__sessionCreatedAt = Date.now();
1427
1456
  if (identityMetadata?.sessionBoundaryOperationId) {
1428
1457
  metadata.sessionBoundaryOperationId = identityMetadata.sessionBoundaryOperationId;
1429
1458
  }
@@ -1586,6 +1615,7 @@ export class SessionManager {
1586
1615
  delete metadata.agentSessions;
1587
1616
  delete metadata.resumeAt;
1588
1617
  delete metadata.replyContext;
1618
+ metadata.__sessionRenewedAt = Date.now();
1589
1619
  const session = {
1590
1620
  id: generateSessionId(),
1591
1621
  channel: sourceSession.channel,
@@ -13,6 +13,9 @@ const MAX_CONTEXT_CHARS = 16_000;
13
13
  const MAX_MESSAGE_CHARS = 2_000;
14
14
  const MODEL_TIMEOUT_MS = 10_000;
15
15
  const RECENT_DECISION_TTL_MS = 30_000;
16
+ const FRESH_EMPTY_SESSION_TTL_MS = 30_000;
17
+ const RENEWED_AT_METADATA_KEY = '__sessionRenewedAt';
18
+ const CREATED_AT_METADATA_KEY = '__sessionCreatedAt';
16
19
  const NEW_CONFIDENCE_THRESHOLD = 0.85;
17
20
  const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
18
21
  const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
@@ -91,7 +94,9 @@ export class SessionRenewService {
91
94
  const routeKey = this.routeKey(request);
92
95
  return await this.withLock(routeKey, async () => {
93
96
  const cached = this.recentDecisions.get(routeKey);
94
- if (cached && cached.candidateSessionId === request.session.id && cached.expiresAt > this.now()) {
97
+ if (cached
98
+ && cached.expiresAt > this.now()
99
+ && (cached.candidateSessionId === request.session.id || cached.selectedSessionId === request.session.id)) {
95
100
  const selected = cached.selectedSessionId === request.session.id
96
101
  ? request.session
97
102
  : await this.sessionManager.getSessionById(cached.selectedSessionId);
@@ -121,6 +126,18 @@ export class SessionRenewService {
121
126
  if (!last) {
122
127
  if (request.isNewSession)
123
128
  return { session: request.session, renewed: false };
129
+ // A renewed session is intentionally created before its first inbound
130
+ // message is appended. A concurrent message can therefore observe an
131
+ // empty, freshly-created session. Treat that state as already renewed;
132
+ // do not create a second logical session for the same route.
133
+ const metadata = request.session.metadata;
134
+ const renewedAt = Number(metadata?.[RENEWED_AT_METADATA_KEY] ?? metadata?.[CREATED_AT_METADATA_KEY]);
135
+ const sessionAgeMs = Number.isFinite(renewedAt)
136
+ ? Math.max(0, this.now() - renewedAt)
137
+ : Number.POSITIVE_INFINITY;
138
+ if (Number.isFinite(renewedAt) && renewedAt <= this.now() && sessionAgeMs <= FRESH_EMPTY_SESSION_TTL_MS) {
139
+ return { session: request.session, renewed: false, decision: 'continue', source: 'missing_history' };
140
+ }
124
141
  const idleMs = this.now() - request.session.updatedAt;
125
142
  return await this.finishDecision(routeKey, request, 'new', 'missing_history', idleMs, evaluationStartedAt);
126
143
  }
@@ -48,10 +48,14 @@ export class SessionTurnCoordinator {
48
48
  && state.lastTerminal.taskId === lease.taskId
49
49
  && state.lastTerminal.generation === lease.generation;
50
50
  }
51
- invalidate(sessionId, reason) {
51
+ invalidate(sessionId, reason, expected) {
52
52
  const state = this.states.get(sessionId);
53
53
  if (!state)
54
54
  return undefined;
55
+ if (expected?.taskId && state.active?.taskId !== expected.taskId)
56
+ return undefined;
57
+ if (expected?.generation !== undefined && state.active?.generation !== expected.generation)
58
+ return undefined;
55
59
  if (state.active?.status === 'interrupting' && state.active.cancelReason === reason) {
56
60
  return cloneTurnMetadata(state);
57
61
  }
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ import { ResponseEngine } from './core/message/response-engine.js';
45
45
  import { MessageQueue } from './core/message/message-queue.js';
46
46
  import { MessageBridge } from './core/message/message-bridge.js';
47
47
  import { evaluateOutboundContactAdmission } from './core/message/message-admission.js';
48
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
48
+ import { enforceMenuResponseSize, evaluateMenuClientCompatibility, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
49
49
  import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
50
50
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
51
51
  import { HandoffRuntime } from './core/handoff/runtime.js';
@@ -74,7 +74,7 @@ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reloa
74
74
  import { buildReloadHooks } from './core/channel-loader.js';
75
75
  import { IpcServer } from './ipc.js';
76
76
  import { logger, setLogLevel } from './utils/logger.js';
77
- import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
77
+ import { remoteAccessService } from './core/auth/remote-access-store.js';
78
78
  import { createEcwebSupervisor } from './utils/ecweb-supervisor.js';
79
79
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
80
80
  import { detectDuplicates } from './core/evolagent-registry.js';
@@ -88,12 +88,12 @@ import { TriggerFeedbackDispatcher } from './trigger/feedback.js';
88
88
  import { TriggerRuntimeScheduler } from './trigger/scheduler.js';
89
89
  import { TargetSessionLockRegistry } from './trigger/session-lock.js';
90
90
  import { DaemonChannel } from './channels/daemon.js';
91
- import { definitionRevision, normalizeTriggerDefinition } from './trigger/validation.js';
91
+ import { definitionRevision, normalizeTriggerDefinition, splitScriptCommand } from './trigger/validation.js';
92
92
  import { applyTriggerPatch } from './trigger/patch.js';
93
93
  import { validateModelSelectionForRole } from './core/model/model-permission.js';
94
94
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
95
95
  import { atomicWriteJson } from './core/session/session-fs-store.js';
96
- import { ensureProcessManagedTempDir } from './cli/task-context.js';
96
+ import { DAEMON_RUNTIME_EPOCH_ENV, ensureProcessManagedTempDir } from './cli/task-context.js';
97
97
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
98
98
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
99
99
  import { MAIN_PACKAGE_NAME } from './product.js';
@@ -551,6 +551,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
551
551
  })(),
552
552
  }));
553
553
  ipcServer.setControlToken(controlToken);
554
+ ipcServer.setRemoteAccessService(remoteAccessService);
554
555
  ipcServer.setShutdownExecutor(async (reason) => {
555
556
  if (!shutdown) {
556
557
  pendingShutdownReason = reason;
@@ -1211,6 +1212,9 @@ async function main() {
1211
1212
  : null;
1212
1213
  bindService?.startCleanup();
1213
1214
  const agentDelegationRegistry = new AgentDelegationRegistry();
1215
+ // Replace any inherited marker from a parent/previous daemon before child
1216
+ // runners or in-process IPC clients are created.
1217
+ process.env[DAEMON_RUNTIME_EPOCH_ENV] = agentDelegationRegistry.getRuntimeEpoch();
1214
1218
  // 创建命令处理器
1215
1219
  const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
1216
1220
  let agentApplicationService;
@@ -1351,6 +1355,7 @@ async function main() {
1351
1355
  baseagent: primaryAgent?.baseagent || 'codex',
1352
1356
  projectPath: primaryAgent?.projectPath || process.cwd(),
1353
1357
  };
1358
+ cmdHandler.setDaemonTriggerOwner(daemonTriggerOwner.aid, daemonChannel.channelName);
1354
1359
  const triggerBaseagentsForAgent = (agentAid) => {
1355
1360
  const ownerName = agentRegistry.get(agentAid)?.name
1356
1361
  ?? (agentAid === daemonTriggerOwner.aid ? primaryAgent?.name : undefined);
@@ -2305,6 +2310,8 @@ async function main() {
2305
2310
  return { ok: false, code: delegation.code, error: delegation.reason };
2306
2311
  return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
2307
2312
  });
2313
+ ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
2314
+ cmdHandler.setDaemonMonitorProvider(() => ipcServer.getMonitorSnapshot().snapshot);
2308
2315
  // Register every IPC executor/provider before exposing the endpoint. The
2309
2316
  // function declaration is hoisted, while its invocation remains here so a
2310
2317
  // failed bind still aborts before any AUN connection is attempted.
@@ -2374,10 +2381,10 @@ async function main() {
2374
2381
  catch (e) {
2375
2382
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2376
2383
  }
2377
- // 控制 AID 接收 owner 指令:
2378
- // 1. /pair — ECWeb 配对码(文本快路径)
2384
+ // 控制 AID 接收 daemon-owner 或有效远程访问 token 授权的指令:
2385
+ // 1. /pair — daemon 远程控制配对码(文本快路径)
2379
2386
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
2380
- // 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
2387
+ // 发送方身份由 AUN X.509 证书链验证;非 Menu 文本仍只接受 daemon-owner。
2381
2388
  controlChannel.onMessage(async (opts) => {
2382
2389
  let activeMenuRequest;
2383
2390
  let menuCompletionLogged = false;
@@ -2385,6 +2392,11 @@ async function main() {
2385
2392
  source: 'control-aun',
2386
2393
  selfAid: daemonCfg.aid,
2387
2394
  messageId: opts.messageId,
2395
+ requesterAid: opts.peerId,
2396
+ authMethod: opts.protectedHeaders?.auth_method === 'remote-access-token' || opts.protectedHeaders?.auth_method === 'ecweb-token'
2397
+ ? opts.protectedHeaders.auth_method
2398
+ : 'daemon-owner',
2399
+ clientName: opts.protectedHeaders?.client_name ?? 'evol',
2388
2400
  };
2389
2401
  try {
2390
2402
  const menuTiming = normalizeMenuResponseTiming(opts);
@@ -2398,7 +2410,7 @@ async function main() {
2398
2410
  }
2399
2411
  const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2400
2412
  const sendControlMenuResponse = async (response) => {
2401
- const timedResponse = withMenuProcessingTime(response, menuTiming);
2413
+ const timedResponse = withMenuProcessingTime(enforceMenuResponseSize(response, 128 * 1024 - 1024), menuTiming);
2402
2414
  const request = activeMenuRequest ?? {
2403
2415
  type: 'menu.unknown',
2404
2416
  id: response.id,
@@ -2446,7 +2458,21 @@ async function main() {
2446
2458
  menuCompletionLogged = true;
2447
2459
  return;
2448
2460
  }
2449
- if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2461
+ const menuClientCompatibilityError = menuControl.isMenu
2462
+ ? evaluateMenuClientCompatibility({ encrypted: opts.encrypted, protectedHeaders: opts.protectedHeaders })
2463
+ : null;
2464
+ if (menuControl.isMenu && menuClientCompatibilityError) {
2465
+ await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, menuClientCompatibilityError));
2466
+ return;
2467
+ }
2468
+ const protectedMenuHeaders = opts.protectedHeaders;
2469
+ const hasRemoteAccessToken = protectedMenuHeaders?.auth_method === 'remote-access-token'
2470
+ || protectedMenuHeaders?.auth_method === 'ecweb-token';
2471
+ if (text.toLowerCase() === '/pair' && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2472
+ logger.debug(`控制 AID 收到非 owner 配对请求,忽略: from=${opts.peerId}`);
2473
+ return;
2474
+ }
2475
+ if (menuControl.isMenu && !isRoleMenu && !hasRemoteAccessToken && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2450
2476
  const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, {
2451
2477
  code: 'NOT_ALLOWED',
2452
2478
  message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
@@ -2455,40 +2481,28 @@ async function main() {
2455
2481
  await sendControlMenuResponse(response);
2456
2482
  return;
2457
2483
  }
2458
- if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2484
+ if (!isRoleMenu && !hasRemoteAccessToken && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2459
2485
  logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
2460
2486
  return;
2461
2487
  }
2462
2488
  if (text.toLowerCase() === '/pair') {
2463
- const port = ecwebService(loadDaemonConfig().services)?.port ?? DEFAULT_ECWEB_PORT;
2464
- const pair = await fetchEcwebPairCode(port, opts.peerId);
2465
- let reply;
2466
- if (pair) {
2467
- const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2468
- reply = `ECWeb 配对码:${pair.code}(约 ${mins} 分钟内有效)\n在浏览器打开 ECWeb 后输入此码登录`;
2469
- }
2470
- else {
2471
- reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
2472
- }
2489
+ const pair = remoteAccessService.getPairingCode(opts.peerId);
2490
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2491
+ const reply = `远程控制配对码:${pair.code}(约 ${mins} 分钟内有效)`;
2473
2492
  await controlChannel.sendMessage(opts.channelId, reply, controlReplyContext(opts));
2474
2493
  return;
2475
2494
  }
2476
- // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
2495
+ // menu.* JSON 路由:准入来源在上方分流,最终 token/owner 校验由 execMenuForControl 完成。
2477
2496
  if (menuControl.isMenu) {
2478
- const versionError = evaluateEvolMenuVersionGate({
2479
- encrypted: opts.encrypted,
2480
- protectedHeaders: opts.protectedHeaders,
2481
- });
2482
- if (versionError) {
2483
- await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, versionError));
2484
- return;
2485
- }
2486
2497
  const validationError = validateMenuRequest(menuControl.request);
2487
2498
  if (validationError) {
2488
2499
  await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError));
2489
2500
  return;
2490
2501
  }
2491
- const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
2502
+ const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId, {
2503
+ encrypted: opts.encrypted,
2504
+ protectedHeaders: opts.protectedHeaders,
2505
+ }));
2492
2506
  if ('retry' in deduped) {
2493
2507
  if (deduped.replayed) {
2494
2508
  logger.warn(`[MenuControl] category=request-replay request=${menuControl.id}`
@@ -2508,7 +2522,7 @@ async function main() {
2508
2522
  return;
2509
2523
  }
2510
2524
  // owner 发的其他内容:提示可用指令
2511
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2525
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取远程控制配对码)', controlReplyContext(opts));
2512
2526
  }
2513
2527
  catch (e) {
2514
2528
  if (activeMenuRequest && !menuCompletionLogged) {
@@ -2684,6 +2698,7 @@ async function main() {
2684
2698
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2685
2699
  ipcServer.setAgentRegistry(agentRegistry);
2686
2700
  ipcServer.setControlToken(triggerControlToken);
2701
+ ipcServer.setRemoteAccessService(remoteAccessService);
2687
2702
  ipcServer.setBootstrapCompleteExecutor(async (aid, auth) => {
2688
2703
  if (auth?.sessionId) {
2689
2704
  const authorization = await authorizeManagedAgentOperation({
@@ -2836,7 +2851,7 @@ async function main() {
2836
2851
  for (const runner of new Set(agentMap.values())) {
2837
2852
  if (typeof runner?.evaluatePreToolUse !== 'function')
2838
2853
  continue;
2839
- const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal);
2854
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId);
2840
2855
  if (result.applicable)
2841
2856
  return result;
2842
2857
  }
@@ -4003,17 +4018,59 @@ async function main() {
4003
4018
  const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
4004
4019
  if (!existing)
4005
4020
  throw new Error(`trigger not found: ${cmd.triggerId}`);
4021
+ const rawPatch = cmd.patch && typeof cmd.patch === 'object' && !Array.isArray(cmd.patch)
4022
+ ? cmd.patch
4023
+ : {};
4024
+ const rawScriptFile = rawPatch.scriptFile;
4025
+ if (rawScriptFile !== undefined && rawScriptFile !== true) {
4026
+ throw new Error('scriptFile must be true');
4027
+ }
4028
+ const patchScriptFile = rawScriptFile === true;
4029
+ const { scriptFile: _scriptFile, ...definitionPatch } = rawPatch;
4030
+ const scriptFileBase64 = cmd.scriptFileBase64;
4031
+ if (patchScriptFile && scriptFileBase64 === undefined) {
4032
+ throw new Error('--script-file requires scriptFileBase64');
4033
+ }
4034
+ if (!patchScriptFile && scriptFileBase64 !== undefined) {
4035
+ throw new Error('scriptFileBase64 requires --script-file');
4036
+ }
4037
+ if (scriptFileBase64 !== undefined) {
4038
+ if (typeof scriptFileBase64 !== 'string') {
4039
+ throw new Error('scriptFileBase64 must be a base64 string');
4040
+ }
4041
+ if (!actor.daemonPrivileged) {
4042
+ throw new Error('updating a script Trigger requires DaemonOwner');
4043
+ }
4044
+ if (existing.execution.type !== 'script' || !existing.execution.script) {
4045
+ throw new Error('--script-file only applies to script Triggers');
4046
+ }
4047
+ if (scriptFileBase64.length > 6 * 1024 * 1024
4048
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(scriptFileBase64)) {
4049
+ throw new Error('scriptFileBase64 is invalid or exceeds the 4 MiB limit');
4050
+ }
4051
+ const scriptBytes = Buffer.from(scriptFileBase64, 'base64');
4052
+ if (scriptBytes.length > 4 * 1024 * 1024)
4053
+ throw new Error('script file exceeds the 4 MiB limit');
4054
+ }
4006
4055
  const currentRevision = definitionRevision(existing);
4007
4056
  if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
4008
4057
  throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
4009
4058
  }
4010
- let definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
4059
+ let definition = Object.keys(definitionPatch).length > 0
4060
+ ? applyTriggerPatch(existing, definitionPatch, { fromPromptFile: cmd.promptFile === true })
4061
+ : existing;
4011
4062
  rejectUnapprovedFullAccessConfiguration(definition, actor);
4012
4063
  definition = withFullAccessConfigurationProvenance(definition, actor);
4013
4064
  validateTriggerDefinitionForActor(definition, actor);
4014
4065
  validateTriggerFeedbackChannels(definition);
4015
4066
  const scheduler = schedulerFor(agentAid);
4016
- const trigger = scheduler.update(cmd.triggerId, definition);
4067
+ const files = scriptFileBase64 !== undefined && existing.execution.script
4068
+ ? [{
4069
+ relativePath: splitScriptCommand(existing.execution.script.path).path,
4070
+ contentBase64: scriptFileBase64,
4071
+ }]
4072
+ : [];
4073
+ const trigger = scheduler.update(cmd.triggerId, definition, files);
4017
4074
  if (trigger.execution.permissionMode === 'fullaccess') {
4018
4075
  auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, existing.execution.permissionMode === 'fullaccess'
4019
4076
  ? 'fullaccess Trigger updated through IPC'
@@ -4152,6 +4209,19 @@ async function main() {
4152
4209
  await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.suspendForDaemonExit()));
4153
4210
  });
4154
4211
  await shutdownStep('interrupt active message queue tasks', async () => { await queueShutdown; });
4212
+ await shutdownStep('dispose agent runners', async () => {
4213
+ const currentInstances = [...agentMap.entries()].map(([key, agent]) => {
4214
+ const split = key.lastIndexOf('::');
4215
+ return {
4216
+ evolagentName: split > 0 ? key.slice(0, split) : '<unknown>',
4217
+ baseagent: split > 0 ? key.slice(split + 2) : 'unknown',
4218
+ agent,
4219
+ };
4220
+ });
4221
+ await disposeAgentInstances(currentInstances, (instance, error) => {
4222
+ logger.warn(`[Shutdown] Failed to dispose runner ${instance.evolagentName}::${instance.baseagent}: ${error instanceof Error ? error.message : String(error)}`);
4223
+ });
4224
+ });
4155
4225
  eventBus.publish({
4156
4226
  type: 'system:shutdown',
4157
4227
  timestamp: Date.now()