evolcore 0.0.22 → 0.0.24

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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  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 +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
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';
@@ -100,6 +100,7 @@ import { MAIN_PACKAGE_NAME } from './product.js';
100
100
  import fs from 'fs';
101
101
  import crypto from 'crypto';
102
102
  import { fileURLToPath } from 'url';
103
+ import * as platform from './utils/cross-platform.js';
103
104
  const controlMenuDeduper = new MenuRequestDeduper();
104
105
  /** 出站 payload 摘要(用于 channel-out.log) */
105
106
  function summarizeOutboundPayload(payload) {
@@ -134,6 +135,7 @@ function summarizeOutboundPayload(payload) {
134
135
  break;
135
136
  case 'status.started':
136
137
  case 'status.progress':
138
+ case 'status.notice':
137
139
  case 'status.requires_action':
138
140
  case 'status.queued':
139
141
  case 'status.completed':
@@ -326,11 +328,21 @@ function seedUpgradeCheckTrigger(manager, owner) {
326
328
  const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
327
329
  const packageJsonPath = path.join(packageRoot, 'package.json');
328
330
  const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
331
+ // Resolve the launch argv at generation time: in a checkout this points the
332
+ // monitor at src/ through tsx, so a `npm run build` that wipes dist/ cannot
333
+ // take the watchdog down with it.
334
+ const monitorEntry = platform.resolveCliEntry(packageRoot);
335
+ const stageParent = path.join(resolvePaths().root, 'data', 'monitor-stage');
329
336
  fs.writeFileSync(scriptPath, `#!/usr/bin/env node
330
- const { existsSync, readFileSync } = require('node:fs');
337
+ const fs = require('node:fs');
338
+ const path = require('node:path');
339
+ const { existsSync, readFileSync } = fs;
331
340
  const { spawn } = require('node:child_process');
332
341
 
333
- const CLI_PATH = ${JSON.stringify(cliPath)};
342
+ const CLI_PATH = ${JSON.stringify(monitorEntry.entry)};
343
+ const CLI_ARGS = ${JSON.stringify(monitorEntry.args)};
344
+ const PACKAGE_ROOT = ${JSON.stringify(packageRoot)};
345
+ const STAGE_PARENT = ${JSON.stringify(stageParent)};
334
346
  const PACKAGE_JSON = ${JSON.stringify(packageJsonPath)};
335
347
  const PACKAGE_NAME = ${JSON.stringify(MAIN_PACKAGE_NAME)};
336
348
  const DEV_MODE = ${devMode ? 'true' : 'false'};
@@ -405,14 +417,60 @@ async function main() {
405
417
  return;
406
418
  }
407
419
 
420
+ // Installed mode: copy the monitor out of the package before it upgrades that
421
+ // same package. Mirrors prepareMonitorStage(); kept inline because this file
422
+ // is generated standalone CJS. Any failure degrades to launching in place —
423
+ // losing upgrade isolation beats losing the upgrade.
424
+ let launchArgs = CLI_ARGS;
425
+ const launchEnv = { ...process.env };
426
+ if (!DEV_MODE && STAGE_PARENT) {
427
+ try {
428
+ const stageDir = path.join(STAGE_PARENT, 'upgrade-' + process.pid + '-' + Date.now());
429
+ fs.mkdirSync(stageDir, { recursive: true });
430
+ fs.cpSync(path.join(PACKAGE_ROOT, 'dist'), path.join(stageDir, 'dist'), { recursive: true });
431
+ if (existsSync(PACKAGE_JSON)) fs.copyFileSync(PACKAGE_JSON, path.join(stageDir, 'package.json'));
432
+ const modulesSource = path.join(PACKAGE_ROOT, 'node_modules');
433
+ if (existsSync(modulesSource)) {
434
+ const staged = path.join(stageDir, 'node_modules');
435
+ try {
436
+ // junction works unelevated on Windows; dir symlink elsewhere.
437
+ fs.symlinkSync(modulesSource, staged, process.platform === 'win32' ? 'junction' : 'dir');
438
+ fs.readdirSync(staged);
439
+ } catch (linkError) {
440
+ fs.rmSync(staged, { recursive: true, force: true });
441
+ fs.cpSync(modulesSource, staged, { recursive: true });
442
+ }
443
+ }
444
+ const stagedEntry = path.join(stageDir, 'dist', 'cli', 'index.js');
445
+ if (existsSync(stagedEntry)) {
446
+ launchArgs = [stagedEntry];
447
+ launchEnv.EVOLCORE_TARGET_PACKAGE_ROOT = PACKAGE_ROOT;
448
+ launchEnv.EVOLCORE_MONITOR_STAGE = stageDir;
449
+ }
450
+ } catch (stageError) {
451
+ // Fall through with the in-place entry.
452
+ }
453
+ }
454
+
408
455
  try {
409
- const child = spawn(process.execPath, [CLI_PATH, 'restart-monitor'], {
456
+ const child = spawn(process.execPath, [...launchArgs, 'restart-monitor'], {
410
457
  detached: true,
411
458
  stdio: 'ignore',
412
459
  windowsHide: process.platform === 'win32',
413
- env: { ...process.env },
460
+ env: launchEnv,
414
461
  });
415
462
  child.unref();
463
+ const pid = child.pid;
464
+ // The daemon is still running here — the monitor is what stops it. So a
465
+ // monitor that dies on load costs the upgrade, not the service; still,
466
+ // report it accurately instead of claiming success.
467
+ await new Promise(resolve => setTimeout(resolve, 3000));
468
+ let alive = false;
469
+ try { process.kill(pid, 0); alive = true; } catch (e) { alive = false; }
470
+ if (!alive) {
471
+ emit('error', 'restart-monitor exited during startup; upgrade to ' + remoteVersion + ' not applied');
472
+ return;
473
+ }
416
474
  emit('success', 'evolcore upgrade available: ' + localVersion + ' -> ' + remoteVersion + '; restart-monitor started');
417
475
  } catch (error) {
418
476
  emit('error', 'failed to start restart-monitor: ' + (error && error.message ? error.message : String(error)));
@@ -551,6 +609,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
551
609
  })(),
552
610
  }));
553
611
  ipcServer.setControlToken(controlToken);
612
+ ipcServer.setRemoteAccessService(remoteAccessService);
554
613
  ipcServer.setShutdownExecutor(async (reason) => {
555
614
  if (!shutdown) {
556
615
  pendingShutdownReason = reason;
@@ -1065,7 +1124,7 @@ async function main() {
1065
1124
  agentLoader.register(new GeminiAgentPlugin());
1066
1125
  agentLoader.register(new EcagentAgentPlugin());
1067
1126
  const creationErrors = [];
1068
- const agentInstances = agentLoader.createAll(agentRegistry, {
1127
+ const agentInstances = await agentLoader.createAll(agentRegistry, {
1069
1128
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
1070
1129
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
1071
1130
  },
@@ -1354,6 +1413,7 @@ async function main() {
1354
1413
  baseagent: primaryAgent?.baseagent || 'codex',
1355
1414
  projectPath: primaryAgent?.projectPath || process.cwd(),
1356
1415
  };
1416
+ cmdHandler.setDaemonTriggerOwner(daemonTriggerOwner.aid, daemonChannel.channelName);
1357
1417
  const triggerBaseagentsForAgent = (agentAid) => {
1358
1418
  const ownerName = agentRegistry.get(agentAid)?.name
1359
1419
  ?? (agentAid === daemonTriggerOwner.aid ? primaryAgent?.name : undefined);
@@ -1491,8 +1551,9 @@ async function main() {
1491
1551
  }
1492
1552
  if (definition.execution.effort && !actor.daemonPrivileged) {
1493
1553
  const baseagent = definition.execution.baseagent
1494
- ?? agentRegistry.get(definition.agentAid)?.baseagent
1495
- ?? 'claude';
1554
+ ?? agentRegistry.get(definition.agentAid)?.baseagent;
1555
+ if (!baseagent)
1556
+ throw new Error('cannot determine Trigger baseagent');
1496
1557
  const effortDecision = validateRuntimeStringFieldOverride({
1497
1558
  selfAid: definition.agentAid,
1498
1559
  role: actor.role,
@@ -2152,6 +2213,7 @@ async function main() {
2152
2213
  const pendingRouteContext = restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {};
2153
2214
  const replyContext = {
2154
2215
  ...(pendingRouteContext ?? {}),
2216
+ ...(pending.threadId ? { threadId: pending.threadId } : {}),
2155
2217
  ...(pending.rootId ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId } : {}),
2156
2218
  };
2157
2219
  const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
@@ -2163,10 +2225,23 @@ async function main() {
2163
2225
  ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
2164
2226
  replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
2165
2227
  });
2228
+ // restart-monitor 会在回滚/升级时写入 outcome:必须如实告诉发起方这次跑的是
2229
+ // 哪个版本,否则 agent 会以为自己的改动或升级已经生效。
2230
+ const restartNotice = pending.outcome === 'rolled-back'
2231
+ ? {
2232
+ text: `⚠️ 服务已重启,但运行的是上一个版本。${pending.outcomeReason ? `原因:${pending.outcomeReason}。` : ''}本次改动未生效,请检查后重试。`,
2233
+ subtype: 'restarted-rolled-back',
2234
+ }
2235
+ : pending.outcome === 'upgraded'
2236
+ ? {
2237
+ text: `✅ 服务重启成功!${pending.outcomeReason ? `${pending.outcomeReason}` : ''}`,
2238
+ subtype: 'restarted-upgraded',
2239
+ }
2240
+ : { text: '✅ 服务重启成功!', subtype: 'restarted' };
2166
2241
  await sendSystemPayload(adapter, envelope, {
2167
2242
  kind: 'system.notice',
2168
- text: '✅ 服务重启成功!',
2169
- subtype: 'restarted',
2243
+ text: restartNotice.text,
2244
+ subtype: restartNotice.subtype,
2170
2245
  });
2171
2246
  pendingRestartNoticeSent = true;
2172
2247
  fs.rmSync(pendingFile, { force: true });
@@ -2309,6 +2384,7 @@ async function main() {
2309
2384
  return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
2310
2385
  });
2311
2386
  ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
2387
+ cmdHandler.setDaemonMonitorProvider(() => ipcServer.getMonitorSnapshot().snapshot);
2312
2388
  // Register every IPC executor/provider before exposing the endpoint. The
2313
2389
  // function declaration is hoisted, while its invocation remains here so a
2314
2390
  // failed bind still aborts before any AUN connection is attempted.
@@ -2378,10 +2454,10 @@ async function main() {
2378
2454
  catch (e) {
2379
2455
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2380
2456
  }
2381
- // 控制 AID 接收 owner 指令:
2382
- // 1. /pair — ECWeb 配对码(文本快路径)
2457
+ // 控制 AID 接收 daemon-owner 或有效远程访问 token 授权的指令:
2458
+ // 1. /pair — daemon 远程控制配对码(文本快路径)
2383
2459
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
2384
- // 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
2460
+ // 发送方身份由 AUN X.509 证书链验证;非 Menu 文本仍只接受 daemon-owner。
2385
2461
  controlChannel.onMessage(async (opts) => {
2386
2462
  let activeMenuRequest;
2387
2463
  let menuCompletionLogged = false;
@@ -2389,6 +2465,11 @@ async function main() {
2389
2465
  source: 'control-aun',
2390
2466
  selfAid: daemonCfg.aid,
2391
2467
  messageId: opts.messageId,
2468
+ requesterAid: opts.peerId,
2469
+ authMethod: opts.protectedHeaders?.auth_method === 'remote-access-token' || opts.protectedHeaders?.auth_method === 'ecweb-token'
2470
+ ? opts.protectedHeaders.auth_method
2471
+ : 'daemon-owner',
2472
+ clientName: opts.protectedHeaders?.client_name ?? 'evol',
2392
2473
  };
2393
2474
  try {
2394
2475
  const menuTiming = normalizeMenuResponseTiming(opts);
@@ -2402,7 +2483,7 @@ async function main() {
2402
2483
  }
2403
2484
  const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2404
2485
  const sendControlMenuResponse = async (response) => {
2405
- const timedResponse = withMenuProcessingTime(response, menuTiming);
2486
+ const timedResponse = withMenuProcessingTime(enforceMenuResponseSize(response, 128 * 1024 - 1024), menuTiming);
2406
2487
  const request = activeMenuRequest ?? {
2407
2488
  type: 'menu.unknown',
2408
2489
  id: response.id,
@@ -2450,7 +2531,21 @@ async function main() {
2450
2531
  menuCompletionLogged = true;
2451
2532
  return;
2452
2533
  }
2453
- if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2534
+ const menuClientCompatibilityError = menuControl.isMenu
2535
+ ? evaluateMenuClientCompatibility({ encrypted: opts.encrypted, protectedHeaders: opts.protectedHeaders })
2536
+ : null;
2537
+ if (menuControl.isMenu && menuClientCompatibilityError) {
2538
+ await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, menuClientCompatibilityError));
2539
+ return;
2540
+ }
2541
+ const protectedMenuHeaders = opts.protectedHeaders;
2542
+ const hasRemoteAccessToken = protectedMenuHeaders?.auth_method === 'remote-access-token'
2543
+ || protectedMenuHeaders?.auth_method === 'ecweb-token';
2544
+ if (text.toLowerCase() === '/pair' && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2545
+ logger.debug(`控制 AID 收到非 owner 配对请求,忽略: from=${opts.peerId}`);
2546
+ return;
2547
+ }
2548
+ if (menuControl.isMenu && !isRoleMenu && !hasRemoteAccessToken && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2454
2549
  const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, {
2455
2550
  code: 'NOT_ALLOWED',
2456
2551
  message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
@@ -2459,40 +2554,28 @@ async function main() {
2459
2554
  await sendControlMenuResponse(response);
2460
2555
  return;
2461
2556
  }
2462
- if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2557
+ if (!isRoleMenu && !hasRemoteAccessToken && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2463
2558
  logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
2464
2559
  return;
2465
2560
  }
2466
2561
  if (text.toLowerCase() === '/pair') {
2467
- const port = ecwebService(loadDaemonConfig().services)?.port ?? DEFAULT_ECWEB_PORT;
2468
- const pair = await fetchEcwebPairCode(port, opts.peerId);
2469
- let reply;
2470
- if (pair) {
2471
- const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2472
- reply = `ECWeb 配对码:${pair.code}(约 ${mins} 分钟内有效)\n在浏览器打开 ECWeb 后输入此码登录`;
2473
- }
2474
- else {
2475
- reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
2476
- }
2562
+ const pair = remoteAccessService.getPairingCode(opts.peerId);
2563
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2564
+ const reply = `远程控制配对码:${pair.code}(约 ${mins} 分钟内有效)`;
2477
2565
  await controlChannel.sendMessage(opts.channelId, reply, controlReplyContext(opts));
2478
2566
  return;
2479
2567
  }
2480
- // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
2568
+ // menu.* JSON 路由:准入来源在上方分流,最终 token/owner 校验由 execMenuForControl 完成。
2481
2569
  if (menuControl.isMenu) {
2482
- const versionError = evaluateEvolMenuVersionGate({
2483
- encrypted: opts.encrypted,
2484
- protectedHeaders: opts.protectedHeaders,
2485
- });
2486
- if (versionError) {
2487
- await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, versionError));
2488
- return;
2489
- }
2490
2570
  const validationError = validateMenuRequest(menuControl.request);
2491
2571
  if (validationError) {
2492
2572
  await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError));
2493
2573
  return;
2494
2574
  }
2495
- const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
2575
+ const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId, {
2576
+ encrypted: opts.encrypted,
2577
+ protectedHeaders: opts.protectedHeaders,
2578
+ }));
2496
2579
  if ('retry' in deduped) {
2497
2580
  if (deduped.replayed) {
2498
2581
  logger.warn(`[MenuControl] category=request-replay request=${menuControl.id}`
@@ -2512,7 +2595,7 @@ async function main() {
2512
2595
  return;
2513
2596
  }
2514
2597
  // owner 发的其他内容:提示可用指令
2515
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2598
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取远程控制配对码)', controlReplyContext(opts));
2516
2599
  }
2517
2600
  catch (e) {
2518
2601
  if (activeMenuRequest && !menuCompletionLogged) {
@@ -2648,9 +2731,18 @@ async function main() {
2648
2731
  continue;
2649
2732
  }
2650
2733
  const evolName = owningAgent.aid;
2651
- const baseagentName = session.baseagent || primaryBaseagent;
2652
- const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
2734
+ const baseagentName = typeof session.baseagent === 'string' ? session.baseagent.trim() : '';
2735
+ if (!baseagentName) {
2736
+ logger.error(`[Resume] session ${session.id}: missing baseagent binding, refusing implicit runner fallback`);
2737
+ sessionManager.clearProcessing(session.id);
2738
+ continue;
2739
+ }
2740
+ // Recovery must resume on the exact Agent × baseagent runner recorded by
2741
+ // the Session. Never fall back to the primary runner: doing so can bind
2742
+ // an in-flight session to a different backend.
2743
+ const agent = agentMap.get(`${evolName}::${baseagentName}`);
2653
2744
  if (!agent) {
2745
+ logger.error(`[Resume] session ${session.id}: runner unavailable for ${evolName}::${baseagentName}; refusing fallback`);
2654
2746
  sessionManager.clearProcessing(session.id);
2655
2747
  continue;
2656
2748
  }
@@ -2688,6 +2780,7 @@ async function main() {
2688
2780
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2689
2781
  ipcServer.setAgentRegistry(agentRegistry);
2690
2782
  ipcServer.setControlToken(triggerControlToken);
2783
+ ipcServer.setRemoteAccessService(remoteAccessService);
2691
2784
  ipcServer.setBootstrapCompleteExecutor(async (aid, auth) => {
2692
2785
  if (auth?.sessionId) {
2693
2786
  const authorization = await authorizeManagedAgentOperation({
@@ -2716,11 +2809,12 @@ async function main() {
2716
2809
  return owner?.aid === aid;
2717
2810
  });
2718
2811
  const welcome = aunInstance?.channel;
2719
- const ownerAid = getFirstStaticAgentOwner(aid);
2720
2812
  const owningAgent = agentRegistry.get(aid);
2721
- const sessions = aunInstance && ownerAid && owningAgent
2813
+ const firstOwnerAid = getFirstStaticAgentOwner(aid);
2814
+ const sessions = aunInstance && firstOwnerAid && owningAgent
2722
2815
  ? {
2723
- ensurePostBootstrapMainSession: async () => {
2816
+ ensurePostBootstrapMainSession: async (owner) => {
2817
+ const ownerAid = owner || firstOwnerAid;
2724
2818
  const channel = aunInstance.adapter.channelName;
2725
2819
  const active = await sessionManager.getActiveSession(channel, ownerAid);
2726
2820
  const ensured = await sessionManager.ensureNewMainSessionForOperation({
@@ -2728,7 +2822,7 @@ async function main() {
2728
2822
  channelId: ownerAid,
2729
2823
  projectPath: active?.projectPath || owningAgent.projectPath || defaultProjectPath,
2730
2824
  baseagent: owningAgent.baseagent || active?.baseagent || primaryBaseagent,
2731
- operationId: postBootstrapWelcomeOperationId(aid),
2825
+ operationId: postBootstrapWelcomeOperationId(aid, ownerAid),
2732
2826
  identityMetadata: {
2733
2827
  ...(active?.metadata ?? {}),
2734
2828
  channelKey: aunInstance.adapter.channelKey,
@@ -2840,15 +2934,18 @@ async function main() {
2840
2934
  for (const runner of new Set(agentMap.values())) {
2841
2935
  if (typeof runner?.evaluatePreToolUse !== 'function')
2842
2936
  continue;
2843
- const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId);
2937
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId, params.sessionToken);
2844
2938
  if (result.applicable)
2845
2939
  return result;
2846
2940
  }
2941
+ // A credential that no runner issued is forged or belongs to a thread
2942
+ // whose rollout is gone. Never allow it; uncredentialed calls were
2943
+ // already allowed by the IPC layer before reaching this executor.
2847
2944
  return {
2848
2945
  ok: false,
2849
2946
  applicable: true,
2850
2947
  decision: 'deny',
2851
- reason: 'No active EvolCore Codex runner accepted this managed hook request',
2948
+ reason: 'No active EvolCore Codex runner accepted this managed hook credential',
2852
2949
  };
2853
2950
  });
2854
2951
  const resolveManagedHandoffScope = async (params, operation) => {
@@ -3181,7 +3278,7 @@ async function main() {
3181
3278
  const creationErrors = [];
3182
3279
  const staged = candidate.config.enabled === false
3183
3280
  ? []
3184
- : agentLoader.createForAgent(candidate, {
3281
+ : await agentLoader.createForAgent(candidate, {
3185
3282
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
3186
3283
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
3187
3284
  },
@@ -3281,7 +3378,7 @@ async function main() {
3281
3378
  throw new Error(agentRuntimeError);
3282
3379
  }
3283
3380
  const creationErrors = [];
3284
- const newAgentInstances = agentLoader.createForAgent(agent, {
3381
+ const newAgentInstances = await agentLoader.createForAgent(agent, {
3285
3382
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
3286
3383
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
3287
3384
  },
@@ -3472,7 +3569,7 @@ async function main() {
3472
3569
  hotLoad: coreHotLoadAgent,
3473
3570
  resync: coreResyncAgents,
3474
3571
  create: async (options) => {
3475
- const { agentCreateNonInteractive } = await import('./cli/agent.js');
3572
+ const { agentCreateNonInteractive } = await import('./cli/agent-create.js');
3476
3573
  return await agentCreateNonInteractive(options, { hotLoadAgent: coreHotLoadAgent });
3477
3574
  },
3478
3575
  aunAids: () => channelInstances.flatMap((inst) => {
@@ -3721,12 +3818,20 @@ async function main() {
3721
3818
  const aid = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
3722
3819
  if (!aid)
3723
3820
  return fail('INVALID_ARGS', 'ec agent new requires an AID');
3724
- const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan } = await import('./cli/agent.js');
3821
+ const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan, parseAgentOwnerArgs } = await import('./cli/agent-create.js');
3822
+ let ownerOptions;
3823
+ try {
3824
+ ownerOptions = parseAgentOwnerArgs(argv);
3825
+ }
3826
+ catch (error) {
3827
+ return fail('INVALID_ARGS', error instanceof Error ? error.message : String(error));
3828
+ }
3725
3829
  const plan = resolveAgentCreatePlan({
3726
3830
  aid,
3727
3831
  project: value('--project'),
3728
3832
  baseagent: value('--baseagent'),
3729
- owner: value('--owner'),
3833
+ ...ownerOptions,
3834
+ handoff: value('--handoff'),
3730
3835
  name: value('--name'),
3731
3836
  description: value('--description'),
3732
3837
  force: argv.includes('--force'),
@@ -3734,23 +3839,75 @@ async function main() {
3734
3839
  const validationError = validateAgentCreatePlan(plan);
3735
3840
  if (validationError)
3736
3841
  return fail('INVALID_ARGS', validationError);
3842
+ if (argv.includes('--handoff')) {
3843
+ if (!delegation.grant.messageId)
3844
+ return fail('INVALID_ARGS', '--handoff requires an originating message');
3845
+ if (!plan.handoff?.trim())
3846
+ return fail('INVALID_ARGS', '--handoff requires non-empty context');
3847
+ if ((plan.owners?.[0] ?? plan.owner) !== taskAid) {
3848
+ return fail('INVALID_ARGS', '--handoff requires the creating Agent to be the first Owner');
3849
+ }
3850
+ if (plan.configExists)
3851
+ return fail('INVALID_ARGS', '--handoff requires a new Agent');
3852
+ }
3737
3853
  if (argv.includes('--dry-run'))
3738
3854
  return { ok: true, result: { ok: true, dryRun: true, plan } };
3855
+ let handoffId;
3856
+ if (plan.handoff) {
3857
+ try {
3858
+ const created = await handoffRuntime.createOutbound({
3859
+ selfAid: taskAid,
3860
+ to: aid,
3861
+ originSessionId: sessionId,
3862
+ originMessageId: delegation.grant.messageId,
3863
+ payload: { type: 'text', text: plan.handoff },
3864
+ encrypt: true,
3865
+ targetChatType: 'private',
3866
+ explicitReturnPolicy: 'required',
3867
+ contextOnly: true,
3868
+ originAuthorization: {
3869
+ actorId: delegation.grant.actorId,
3870
+ channelKey: delegation.grant.channel,
3871
+ channelType: delegation.grant.channelType,
3872
+ chatType: delegation.grant.chatType,
3873
+ peerKey: delegation.grant.peerKey,
3874
+ },
3875
+ causation: responseEngine.getTaskRuntimeContext(sessionId)?.causation,
3876
+ });
3877
+ handoffId = created.handoff?.handoff_id;
3878
+ if (!handoffId)
3879
+ return fail('INVALID_ARGS', '--handoff requires a different target session');
3880
+ }
3881
+ catch (error) {
3882
+ return fail('HANDOFF_PREPARE_FAILED', error instanceof Error ? error.message : String(error));
3883
+ }
3884
+ }
3739
3885
  const createOptions = {
3740
3886
  aid: plan.aid,
3741
3887
  project: plan.project,
3742
3888
  baseagent: plan.baseagent,
3743
3889
  owner: plan.owner,
3890
+ owners: plan.owners,
3744
3891
  name: plan.name,
3745
3892
  description: plan.description,
3746
3893
  force: argv.includes('--force'),
3747
3894
  };
3748
- const result = agentApplicationService
3749
- ? await agentApplicationService.create(createOptions)
3750
- : await agentCreateNonInteractive(createOptions);
3751
- return !result.ok
3752
- ? fail(result.code ?? 'INTERNAL', result.error)
3753
- : { ok: true, result: { ...result, configPath: '[redacted]' } };
3895
+ try {
3896
+ const result = agentApplicationService
3897
+ ? await agentApplicationService.create(createOptions)
3898
+ : await agentCreateNonInteractive(createOptions);
3899
+ if (!result.ok) {
3900
+ if (handoffId)
3901
+ handoffRuntime.store.discard(taskAid, handoffId, 'operation_failed');
3902
+ return fail(result.code ?? 'INTERNAL', result.error);
3903
+ }
3904
+ return { ok: true, result: { ...result, configPath: '[redacted]', ...(handoffId ? { handoff_id: handoffId } : {}) } };
3905
+ }
3906
+ catch (error) {
3907
+ if (handoffId)
3908
+ handoffRuntime.store.discard(taskAid, handoffId, 'operation_failed');
3909
+ return fail('INTERNAL', error instanceof Error ? error.message : String(error));
3910
+ }
3754
3911
  }
3755
3912
  if (operation === 'agent.delete') {
3756
3913
  const result = agentApplicationService