evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ import { buildEnvelope, isDeliveryTargetForChannel, replyContextFromSession } fr
42
42
  import { ResponseEngine } from './core/message/response-engine.js';
43
43
  import { MessageQueue } from './core/message/message-queue.js';
44
44
  import { MessageBridge } from './core/message/message-bridge.js';
45
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
45
+ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
46
46
  import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
47
47
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
48
48
  import { HandoffRuntime } from './core/handoff/runtime.js';
@@ -51,13 +51,14 @@ import { postBootstrapWelcomeOperationId } from './core/bootstrap-messages.js';
51
51
  import { MessageCache } from './core/message/message-cache.js';
52
52
  import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
53
53
  import { EventBus } from './core/event-bus.js';
54
- import { getEventCatalog } from './core/event-catalog.js';
54
+ import { eventPatternIncludesInternal, getEventCatalog } from './core/event-catalog.js';
55
55
  import { StatsCollector } from './utils/stats.js';
56
56
  import { AidStatsCollector } from './utils/stats.js';
57
57
  import { PermissionGateway } from './core/permission/approval-gateway.js';
58
58
  import { InteractionRouter, registerAdapterInteractions } from './core/interaction-router.js';
59
59
  import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend } from './core/auth/agent-delegation.js';
60
- import { authorizeOperation, buildAuthSubject, isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation, } from './core/auth/auth-gateway.js';
60
+ import { authorizeOperation, buildAuthSubject, isCrossAgentTriggerOperationAllowed, } from './core/auth/auth-gateway.js';
61
+ import { authorizationDenialData, formatAuthorizationDenial } from './core/auth/authorization-denial.js';
61
62
  import { parsePeerKey } from './core/relation/peer-identity.js';
62
63
  import { isHClassPath } from './core/protected-paths.js';
63
64
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
@@ -481,6 +482,9 @@ async function runBindBootstrapDaemon(daemonCfg) {
481
482
  });
482
483
  bindService.startCleanup();
483
484
  let controlChannel;
485
+ let shutdown;
486
+ let pendingShutdownReason;
487
+ let shutdownInProgress = false;
484
488
  controlChannel = new AUNChannel({
485
489
  aid: daemonCfg.aid,
486
490
  agentName: daemonCfg.aid,
@@ -535,12 +539,21 @@ async function runBindBootstrapDaemon(daemonCfg) {
535
539
  gatewayUrl: aidState.gatewayUrl,
536
540
  reconnectCount: aidState.reconnectCount,
537
541
  lastConnectedAt: aidState.lastConnectedAt,
542
+ lastTransportEventAt: aidState.lastTransportEventAt,
543
+ lastInboundAt: aidState.lastInboundAt,
538
544
  lastError: aidState.lastError,
539
545
  } : {}),
540
546
  };
541
547
  })(),
542
548
  }));
543
549
  ipcServer.setControlToken(controlToken);
550
+ ipcServer.setShutdownExecutor(async (reason) => {
551
+ if (!shutdown) {
552
+ pendingShutdownReason = reason;
553
+ return;
554
+ }
555
+ await shutdown(reason);
556
+ });
544
557
  ipcServer.setBindExecutor({
545
558
  begin: (cmd) => bindService.begin(cmd),
546
559
  status: (taskId) => bindService.status(taskId),
@@ -549,9 +562,16 @@ async function runBindBootstrapDaemon(daemonCfg) {
549
562
  await ipcServer.start();
550
563
  fs.writeFileSync(resolvePaths().readySignal, String(Date.now()));
551
564
  logger.info(`✓ Bind bootstrap ready signal written: ${resolvePaths().readySignal}`);
552
- const shutdown = async (signal) => {
565
+ shutdown = async (signal) => {
566
+ if (shutdownInProgress)
567
+ return;
568
+ shutdownInProgress = true;
553
569
  logger.info(`[bind-bootstrap] shutting down${signal ? ` (${signal})` : ''}`);
554
570
  ipcServer.stop();
571
+ try {
572
+ fs.unlinkSync(resolvePaths().readySignal);
573
+ }
574
+ catch { }
555
575
  bindService.stopCleanup();
556
576
  if (controlChannel) {
557
577
  try {
@@ -562,6 +582,10 @@ async function runBindBootstrapDaemon(daemonCfg) {
562
582
  removeAll();
563
583
  process.exit(0);
564
584
  };
585
+ if (pendingShutdownReason) {
586
+ const reason = pendingShutdownReason;
587
+ setImmediate(() => { void shutdown?.(reason); });
588
+ }
565
589
  process.on('SIGINT', () => shutdown('SIGINT'));
566
590
  process.on('SIGTERM', () => shutdown('SIGTERM'));
567
591
  process.on('exit', () => removeAll());
@@ -1390,11 +1414,29 @@ async function main() {
1390
1414
  throw Object.assign(new Error('AUN trigger feedback target requires a valid explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
1391
1415
  }
1392
1416
  };
1393
- const validateTriggerDefinitionForActor = (definition, actor) => {
1394
- if (definition.execution.type === 'script' && !actor.management) {
1395
- throw new Error('only owner/admin may create or update a script trigger');
1417
+ const daemonPrivilegedTriggerReason = (definition) => {
1418
+ if (definition.origin?.channelKey === 'daemon' || definition.origin?.channelType === 'daemon') {
1419
+ return 'daemon/control-origin Trigger';
1396
1420
  }
1397
- if (definition.execution.permissionMode) {
1421
+ if (definition.execution.type === 'script')
1422
+ return 'script Trigger';
1423
+ if (definition.execution.permissionMode === 'bypass')
1424
+ return 'persistent bypass Trigger';
1425
+ if (definition.source.type === 'event' && eventPatternIncludesInternal(definition.source.eventPattern)) {
1426
+ return 'internal-event Trigger';
1427
+ }
1428
+ if (definition.feedback.strategy === 'target'
1429
+ && definition.feedback.target?.channelKey === 'daemon') {
1430
+ return 'daemon-control feedback';
1431
+ }
1432
+ return undefined;
1433
+ };
1434
+ const hasDaemonTriggerAuthority = (subject) => (subject.isDaemonOwner || subject.processRole === 'daemon-service');
1435
+ const validateTriggerDefinitionForActor = (definition, actor) => {
1436
+ const privilegedReason = daemonPrivilegedTriggerReason(definition);
1437
+ if (privilegedReason && !actor.daemonPrivileged)
1438
+ throw new Error(`${privilegedReason} requires DaemonOwner`);
1439
+ if (definition.execution.permissionMode && !actor.daemonPrivileged) {
1398
1440
  const permissionDecision = constrainRuntimePermissionMode({
1399
1441
  selfAid: definition.agentAid,
1400
1442
  role: actor.role,
@@ -1404,7 +1446,7 @@ async function main() {
1404
1446
  throw new Error(`trigger permissionMode ${definition.execution.permissionMode} exceeds current role ${actor.role}`);
1405
1447
  }
1406
1448
  }
1407
- if (definition.execution.model) {
1449
+ if (definition.execution.model && !actor.daemonPrivileged) {
1408
1450
  const agent = agentRegistry.get(definition.agentAid);
1409
1451
  const modelDecision = validateModelSelectionForRole({
1410
1452
  role: actor.role,
@@ -1415,7 +1457,7 @@ async function main() {
1415
1457
  if (!modelDecision.ok)
1416
1458
  throw new Error(modelDecision.message || 'trigger model is not allowed by current role');
1417
1459
  }
1418
- if (definition.execution.effort) {
1460
+ if (definition.execution.effort && !actor.daemonPrivileged) {
1419
1461
  const baseagent = definition.execution.baseagent
1420
1462
  ?? agentRegistry.get(definition.agentAid)?.baseagent
1421
1463
  ?? 'claude';
@@ -1453,7 +1495,7 @@ async function main() {
1453
1495
  channelId: origin.channelId,
1454
1496
  chatType: 'private',
1455
1497
  conversationId: origin.peerId,
1456
- identity: roleToSessionIdentity('owner'),
1498
+ trustedProcessRole: 'daemon-service',
1457
1499
  fromControlChannel: true,
1458
1500
  });
1459
1501
  const decision = authorizeOperation({
@@ -1493,6 +1535,7 @@ async function main() {
1493
1535
  conversationId: origin.peerId,
1494
1536
  roleDetail,
1495
1537
  identity: roleToSessionIdentity(role),
1538
+ processOwners: loadDaemonConfig().owners ?? [],
1496
1539
  fromControlChannel: false,
1497
1540
  });
1498
1541
  const operationDecision = authorizeOperation({
@@ -1508,8 +1551,9 @@ async function main() {
1508
1551
  if (!operationDecision.allow) {
1509
1552
  return { allowed: false, reason: operationDecision.reason };
1510
1553
  }
1511
- if (definition.execution.type === 'script' && !isManagementRole(role)) {
1512
- return { allowed: false, reason: `script trigger requires current owner/admin role (role=${role ?? 'none'})` };
1554
+ const privilegedReason = daemonPrivilegedTriggerReason(definition);
1555
+ if (privilegedReason && !hasDaemonTriggerAuthority(subject)) {
1556
+ return { allowed: false, reason: `${privilegedReason} requires current DaemonOwner` };
1513
1557
  }
1514
1558
  if (definition.feedback.strategy === 'target' && !isManagementRole(role)) {
1515
1559
  const target = definition.feedback.target;
@@ -1548,14 +1592,16 @@ async function main() {
1548
1592
  channelId: origin.channelId,
1549
1593
  chatType: 'private',
1550
1594
  conversationId: origin.peerId,
1551
- identity: roleToSessionIdentity('owner'),
1595
+ trustedProcessRole: 'daemon-service',
1552
1596
  fromControlChannel: true,
1553
1597
  });
1554
1598
  return {
1555
1599
  origin,
1556
1600
  management: true,
1601
+ daemonOwner: false,
1602
+ daemonPrivileged: true,
1557
1603
  control: true,
1558
- role: 'owner',
1604
+ role: 'none',
1559
1605
  subject,
1560
1606
  };
1561
1607
  }
@@ -1580,9 +1626,6 @@ async function main() {
1580
1626
  conversationId: validation.grant.actorId,
1581
1627
  });
1582
1628
  const currentRole = roleDetail.effectiveRole;
1583
- if (!checkRoleAccess(currentRole, agentAid)) {
1584
- throw new Error('trigger actor no longer has access to this agent');
1585
- }
1586
1629
  const subject = buildAuthSubject({
1587
1630
  selfAid: agentAid,
1588
1631
  actorId: validation.grant.actorId,
@@ -1593,11 +1636,20 @@ async function main() {
1593
1636
  conversationId: validation.grant.actorId,
1594
1637
  roleDetail,
1595
1638
  identity: roleToSessionIdentity(currentRole),
1639
+ processOwners: loadDaemonConfig().owners ?? [],
1596
1640
  fromControlChannel: false,
1597
1641
  });
1642
+ if (validation.grant.selfAid !== agentAid && !subject.isDaemonOwner) {
1643
+ throw new Error('cross-agent Trigger operations require DaemonOwner');
1644
+ }
1645
+ if (!subject.isDaemonOwner && !checkRoleAccess(currentRole, agentAid)) {
1646
+ throw new Error('trigger actor no longer has access to this agent');
1647
+ }
1598
1648
  return {
1599
1649
  origin: actorOrigin,
1600
- management: isManagementRole(currentRole),
1650
+ management: isManagementRole(currentRole) || subject.isDaemonOwner,
1651
+ daemonOwner: subject.isDaemonOwner,
1652
+ daemonPrivileged: subject.isDaemonOwner,
1601
1653
  selfAid: validation.grant.selfAid,
1602
1654
  control: false,
1603
1655
  role: currentRole || 'none',
@@ -2124,6 +2176,8 @@ async function main() {
2124
2176
  // A failed bind (for example, a stale Windows named pipe) must abort startup
2125
2177
  // without creating a second AUN session that can kick the old daemon.
2126
2178
  let controlChannel;
2179
+ let shutdown;
2180
+ let pendingShutdownReason;
2127
2181
  const ipcServer = new IpcServer(resolvePaths().socket, () => {
2128
2182
  const channels = {};
2129
2183
  const channelsByType = {};
@@ -2176,6 +2230,8 @@ async function main() {
2176
2230
  gatewayUrl: aidState.gatewayUrl,
2177
2231
  reconnectCount: aidState.reconnectCount,
2178
2232
  lastConnectedAt: aidState.lastConnectedAt,
2233
+ lastTransportEventAt: aidState.lastTransportEventAt,
2234
+ lastInboundAt: aidState.lastInboundAt,
2179
2235
  lastError: aidState.lastError,
2180
2236
  } : {}),
2181
2237
  };
@@ -2259,7 +2315,15 @@ async function main() {
2259
2315
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
2260
2316
  // 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
2261
2317
  controlChannel.onMessage(async (opts) => {
2318
+ let activeMenuRequest;
2319
+ let menuCompletionLogged = false;
2320
+ const menuFlowContext = {
2321
+ source: 'control-aun',
2322
+ selfAid: daemonCfg.aid,
2323
+ messageId: opts.messageId,
2324
+ };
2262
2325
  try {
2326
+ const menuTiming = normalizeMenuResponseTiming(opts);
2263
2327
  const text = (opts.content || '').trim();
2264
2328
  let parsed;
2265
2329
  try {
@@ -2269,6 +2333,28 @@ async function main() {
2269
2333
  parsed = null;
2270
2334
  }
2271
2335
  const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2336
+ const sendControlMenuResponse = async (response) => {
2337
+ const timedResponse = withMenuProcessingTime(response, menuTiming);
2338
+ const request = activeMenuRequest ?? {
2339
+ type: 'menu.unknown',
2340
+ id: response.id,
2341
+ ...(response.name ? { name: response.name } : {}),
2342
+ };
2343
+ try {
2344
+ await controlChannel.sendStructured(opts.channelId, timedResponse, menuResponseContext());
2345
+ logMenuRequestCompleted(request, timedResponse, menuFlowContext, { delivery: 'sent' });
2346
+ menuCompletionLogged = true;
2347
+ }
2348
+ catch (error) {
2349
+ logMenuRequestCompleted(request, timedResponse, menuFlowContext, {
2350
+ delivery: 'failed',
2351
+ reason: 'transport-error',
2352
+ transportError: error,
2353
+ });
2354
+ menuCompletionLogged = true;
2355
+ throw error;
2356
+ }
2357
+ };
2272
2358
  if (bindService && parsed?.type === 'bind.request') {
2273
2359
  const response = await bindService.handleRequest(parsed, opts.peerId);
2274
2360
  if (response) {
@@ -2280,18 +2366,32 @@ async function main() {
2280
2366
  return;
2281
2367
  }
2282
2368
  const menuControl = parseMenuControl(text);
2369
+ if (menuControl.isMenu) {
2370
+ activeMenuRequest = menuControl.request;
2371
+ logMenuRequestReceived(menuControl.request, menuFlowContext);
2372
+ }
2283
2373
  const isRoleMenu = menuControl.isMenu
2284
2374
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
2375
+ const currentProcessOwners = loadDaemonConfig().owners ?? [];
2285
2376
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2286
2377
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
2378
+ logMenuRequestCompleted(menuControl.request, undefined, menuFlowContext, {
2379
+ delivery: 'dropped',
2380
+ reason: 'missing-id',
2381
+ });
2382
+ menuCompletionLogged = true;
2287
2383
  return;
2288
2384
  }
2289
- if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
2290
- const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: 'Current identity cannot access the control channel' });
2291
- await controlChannel.sendStructured(opts.channelId, response, menuResponseContext());
2385
+ if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2386
+ const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, {
2387
+ code: 'NOT_ALLOWED',
2388
+ message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
2389
+ data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
2390
+ });
2391
+ await sendControlMenuResponse(response);
2292
2392
  return;
2293
2393
  }
2294
- if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
2394
+ if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, currentProcessOwners)) {
2295
2395
  logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
2296
2396
  return;
2297
2397
  }
@@ -2316,12 +2416,12 @@ async function main() {
2316
2416
  protectedHeaders: opts.protectedHeaders,
2317
2417
  });
2318
2418
  if (versionError) {
2319
- await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, versionError), menuResponseContext());
2419
+ await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, versionError));
2320
2420
  return;
2321
2421
  }
2322
2422
  const validationError = validateMenuRequest(menuControl.request);
2323
2423
  if (validationError) {
2324
- await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError), menuResponseContext());
2424
+ await sendControlMenuResponse(menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError));
2325
2425
  return;
2326
2426
  }
2327
2427
  const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
@@ -2340,13 +2440,20 @@ async function main() {
2340
2440
  : deduped.value;
2341
2441
  // 同 bind.response:sendStructured 直发 typed payload(payload.type='menu.response'),
2342
2442
  // 不能用 sendMessage(会包成 {type:'text',...});encrypted 跟随入站请求保持对称。
2343
- await controlChannel.sendStructured(opts.channelId, response, menuResponseContext());
2443
+ await sendControlMenuResponse(response);
2344
2444
  return;
2345
2445
  }
2346
2446
  // owner 发的其他内容:提示可用指令
2347
2447
  await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2348
2448
  }
2349
2449
  catch (e) {
2450
+ if (activeMenuRequest && !menuCompletionLogged) {
2451
+ logMenuRequestCompleted(activeMenuRequest, undefined, menuFlowContext, {
2452
+ delivery: 'failed',
2453
+ reason: 'handler-error',
2454
+ transportError: e,
2455
+ });
2456
+ }
2350
2457
  logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
2351
2458
  }
2352
2459
  });
@@ -2502,6 +2609,13 @@ async function main() {
2502
2609
  }
2503
2610
  }
2504
2611
  function configureIpc() {
2612
+ ipcServer.setShutdownExecutor(async (reason) => {
2613
+ if (!shutdown) {
2614
+ pendingShutdownReason = reason;
2615
+ return;
2616
+ }
2617
+ await shutdown(reason);
2618
+ });
2505
2619
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2506
2620
  ipcServer.setAgentRegistry(agentRegistry);
2507
2621
  ipcServer.setControlToken(triggerControlToken);
@@ -2657,7 +2771,7 @@ async function main() {
2657
2771
  for (const runner of new Set(agentMap.values())) {
2658
2772
  if (typeof runner?.evaluatePreToolUse !== 'function')
2659
2773
  continue;
2660
- const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput);
2774
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal);
2661
2775
  if (result.applicable)
2662
2776
  return result;
2663
2777
  }
@@ -2699,7 +2813,7 @@ async function main() {
2699
2813
  channelId: conversationId,
2700
2814
  chatType: delegation.grant.chatType,
2701
2815
  conversationId,
2702
- processOwners: daemonCfg.owners ?? [],
2816
+ processOwners: loadDaemonConfig().owners ?? [],
2703
2817
  });
2704
2818
  const managementRole = isManagementRole(subject.role);
2705
2819
  const decision = authorizeOperation({
@@ -3317,9 +3431,6 @@ async function main() {
3317
3431
  if (!session || session.selfAID !== delegation.grant.selfAid) {
3318
3432
  return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation does not match the current session' };
3319
3433
  }
3320
- if (params.targetAid && params.targetAid !== delegation.grant.selfAid) {
3321
- return { ok: false, code: 'SCOPE_MISMATCH', error: 'only the current Agent can be targeted' };
3322
- }
3323
3434
  let conversationId;
3324
3435
  try {
3325
3436
  conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
@@ -3335,10 +3446,10 @@ async function main() {
3335
3446
  channelId: conversationId,
3336
3447
  chatType: delegation.grant.chatType,
3337
3448
  conversationId,
3338
- processOwners: daemonCfg.owners ?? [],
3449
+ processOwners: loadDaemonConfig().owners ?? [],
3339
3450
  });
3340
- if (!isManagementRole(subject.role)) {
3341
- return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${operation} requires Agent admin permission` };
3451
+ if (params.targetAid && params.targetAid !== delegation.grant.selfAid && !subject.isDaemonOwner) {
3452
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'only the current Agent can be targeted' };
3342
3453
  }
3343
3454
  const decision = authorizeOperation({
3344
3455
  source: 'agent-tool',
@@ -3355,6 +3466,180 @@ async function main() {
3355
3466
  ? { ok: true, aid: delegation.grant.selfAid }
3356
3467
  : { ok: false, code: decision.code, error: decision.reason };
3357
3468
  }
3469
+ ipcServer.setAgentOperationExecutor(async (argv, sessionId, delegationToken, delegationCommandHash) => {
3470
+ const fail = (code, error, reasonCode) => ({
3471
+ ok: false,
3472
+ code,
3473
+ error,
3474
+ ...(reasonCode ? { data: authorizationDenialData(reasonCode) } : {}),
3475
+ });
3476
+ if (!Array.isArray(argv) || argv[0] !== 'agent')
3477
+ return fail('INVALID_REQUEST', 'agent.op requires an agent command');
3478
+ const sub = argv[1];
3479
+ if (!sub)
3480
+ return fail('INVALID_ARGS', 'missing agent subcommand');
3481
+ const value = (flag) => {
3482
+ const index = argv.indexOf(flag);
3483
+ return index >= 0 && index + 1 < argv.length && !argv[index + 1].startsWith('--') ? argv[index + 1] : undefined;
3484
+ };
3485
+ const taskSession = await sessionManager.getSessionById(sessionId);
3486
+ const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
3487
+ if (!delegation.ok)
3488
+ return fail(delegation.code, delegation.reason);
3489
+ if (!taskSession || taskSession.selfAID !== delegation.grant.selfAid) {
3490
+ return fail('INVALID_DELEGATION', 'delegation does not match the current session');
3491
+ }
3492
+ const taskAid = delegation.grant.selfAid;
3493
+ const targetAid = sub === 'list'
3494
+ ? undefined
3495
+ : sub === 'show'
3496
+ ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : taskAid)
3497
+ : sub === 'reload'
3498
+ ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined)
3499
+ : (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined);
3500
+ const operation = sub === 'list' ? 'agent.list'
3501
+ : sub === 'show' ? 'agent.show'
3502
+ : sub === 'new' ? 'agent.create'
3503
+ : sub === 'enable' ? 'agent.enable'
3504
+ : sub === 'disable' ? 'agent.disable'
3505
+ : sub === 'reload' ? 'agent.reload'
3506
+ : sub === 'delete' ? 'agent.delete' : undefined;
3507
+ if (!operation)
3508
+ return fail('INVALID_ARGS', `unsupported managed agent command: ${sub}`);
3509
+ if (sub === 'new' && !argv.includes('--non-interactive')) {
3510
+ return fail('MANAGED_NON_INTERACTIVE_REQUIRED', 'interactive Agent creation is unavailable in a managed task', 'MANAGED_NON_INTERACTIVE_REQUIRED');
3511
+ }
3512
+ if (sub === 'delete' && argv.includes('--purge') && !argv.includes('--confirm-purge')) {
3513
+ return fail('PURGE_CONFIRMATION_REQUIRED', 'agent delete --purge requires explicit --confirm-purge', 'PURGE_CONFIRMATION_REQUIRED');
3514
+ }
3515
+ let conversationId;
3516
+ try {
3517
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
3518
+ }
3519
+ catch {
3520
+ return fail('INVALID_DELEGATION', 'delegation contains an invalid relation key');
3521
+ }
3522
+ const subject = buildAuthSubject({
3523
+ selfAid: taskAid,
3524
+ actorId: delegation.grant.actorId,
3525
+ channel: delegation.grant.channel,
3526
+ channelType: delegation.grant.channelType,
3527
+ channelId: conversationId,
3528
+ chatType: delegation.grant.chatType,
3529
+ conversationId,
3530
+ processOwners: loadDaemonConfig().owners ?? [],
3531
+ });
3532
+ const decision = authorizeOperation({
3533
+ source: 'agent-tool',
3534
+ subject,
3535
+ intent: {
3536
+ operation,
3537
+ scope: operation === 'agent.show'
3538
+ ? 'agent'
3539
+ : operation === 'agent.reload' && targetAid
3540
+ ? 'agent'
3541
+ : 'control',
3542
+ source: 'agent-tool',
3543
+ args: {
3544
+ self: taskAid,
3545
+ aid: targetAid ?? taskAid,
3546
+ taskAgentAid: taskAid,
3547
+ targetAgentAid: targetAid ?? taskAid,
3548
+ },
3549
+ },
3550
+ auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
3551
+ });
3552
+ if (!decision.allow) {
3553
+ const reasonCode = decision.command?.reasonCode;
3554
+ return fail(decision.code, decision.reason, reasonCode);
3555
+ }
3556
+ const { execAgentAction, execAgentOptions, execAgentQuery } = await import('./core/command/agent-control.js');
3557
+ if (operation === 'agent.list') {
3558
+ const result = await execAgentOptions({ options: 'all' });
3559
+ if ('error' in result)
3560
+ return fail(result.code, result.error);
3561
+ const agents = Array.isArray(result.data?.agents) ? result.data.agents.map((agent) => ({
3562
+ aid: agent.aid,
3563
+ name: agent.name,
3564
+ personalName: agent.personalName ?? null,
3565
+ status: agent.status,
3566
+ channels: Array.isArray(agent.channels) ? agent.channels : [],
3567
+ projectPath: null,
3568
+ baseagent: agent.baseagent ?? null,
3569
+ model: agent.model ?? null,
3570
+ lastActivity: agent.lastActivity ?? null,
3571
+ })) : [];
3572
+ return { ok: true, result: { ok: true, agents, redacted: true } };
3573
+ }
3574
+ if (operation === 'agent.show') {
3575
+ const result = await execAgentQuery({ aid: targetAid });
3576
+ if ('error' in result)
3577
+ return fail(result.code, result.error);
3578
+ const agent = result.data ?? {};
3579
+ return { ok: true, result: {
3580
+ ok: true,
3581
+ aid: agent.aid,
3582
+ status: agent.status,
3583
+ identity: agent.identity ?? { name: null, description: null },
3584
+ config: {
3585
+ baseagent: agent.config?.baseagent ?? null,
3586
+ model: agent.config?.model ?? null,
3587
+ effort: agent.config?.effort ?? null,
3588
+ chatmode: agent.config?.chatmode ?? null,
3589
+ channels: Array.isArray(agent.config?.channels) ? agent.config.channels : [],
3590
+ },
3591
+ connection: agent.connection ?? null,
3592
+ sessions: agent.sessions ?? { active: 0, last_activity: null },
3593
+ paths: { config: null, agent_md: null, project: null, data: null },
3594
+ redacted: true,
3595
+ ...(agent.createProgress ? { createProgress: agent.createProgress } : {}),
3596
+ } };
3597
+ }
3598
+ if (operation === 'agent.create') {
3599
+ const aid = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
3600
+ if (!aid)
3601
+ return fail('INVALID_ARGS', 'ec agent new requires an AID');
3602
+ const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan } = await import('./cli/agent.js');
3603
+ const plan = resolveAgentCreatePlan({
3604
+ aid,
3605
+ project: value('--project'),
3606
+ baseagent: value('--baseagent'),
3607
+ owner: value('--owner'),
3608
+ name: value('--name'),
3609
+ description: value('--description'),
3610
+ force: argv.includes('--force'),
3611
+ });
3612
+ const validationError = validateAgentCreatePlan(plan);
3613
+ if (validationError)
3614
+ return fail('INVALID_ARGS', validationError);
3615
+ if (argv.includes('--dry-run'))
3616
+ return { ok: true, result: { ok: true, dryRun: true, plan } };
3617
+ const result = await agentCreateNonInteractive({
3618
+ aid: plan.aid,
3619
+ project: plan.project,
3620
+ baseagent: plan.baseagent,
3621
+ owner: plan.owner,
3622
+ name: plan.name,
3623
+ description: plan.description,
3624
+ force: argv.includes('--force'),
3625
+ });
3626
+ return !result.ok
3627
+ ? fail(result.code ?? 'INTERNAL', result.error)
3628
+ : { ok: true, result: { ...result, configPath: '[redacted]' } };
3629
+ }
3630
+ if (operation === 'agent.delete') {
3631
+ const { agentDelete } = await import('./cli/agent.js');
3632
+ const result = await agentDelete(targetAid ?? '', argv.includes('--purge'));
3633
+ return 'error' in result ? fail(result.code ?? 'INTERNAL', result.error) : { ok: true, result };
3634
+ }
3635
+ if (operation === 'agent.reload' && !targetAid) {
3636
+ const { agentReload } = await import('./cli/agent.js');
3637
+ const result = await agentReload();
3638
+ return !result.ok ? fail(result.code ?? 'INTERNAL', result.error) : { ok: true, result };
3639
+ }
3640
+ const result = await execAgentAction(sub, { aid: targetAid ?? taskAid, force: argv.includes('--force') }, delegation.grant.actorId, eventBus);
3641
+ return 'error' in result ? fail(result.code, result.error) : { ok: true, result: { ok: true, ...(result.data ?? {}) } };
3642
+ });
3358
3643
  ipcServer.setQueueOperationExecutor(async (params) => {
3359
3644
  const operation = `ec.queue.${params.action ?? 'read'}`;
3360
3645
  const authorization = await authorizeManagedAgentOperation(params, operation);
@@ -3416,17 +3701,14 @@ async function main() {
3416
3701
  const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
3417
3702
  if (!isCrossAgentTriggerOperationAllowed({
3418
3703
  control: actor.control,
3704
+ daemonOwner: actor.daemonOwner,
3419
3705
  taskAgentAid: actor.selfAid,
3420
3706
  targetAgentAid: agentAid,
3421
3707
  targetManagement: actor.management,
3422
3708
  operation,
3423
3709
  })) {
3424
- if (isCrossAgent && !actor.management) {
3425
- throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
3426
- }
3427
- if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
3428
- throw new Error('cross-agent trigger operations are limited to list, show, and history');
3429
- }
3710
+ if (isCrossAgent)
3711
+ throw new Error('cross-agent Trigger operations require DaemonOwner');
3430
3712
  throw new Error('trigger agent does not match authenticated task agent');
3431
3713
  }
3432
3714
  const operationDecision = authorizeOperation({
@@ -3438,6 +3720,8 @@ async function main() {
3438
3720
  source: actor.control ? 'control' : 'agent-tool',
3439
3721
  args: {
3440
3722
  self: agentAid,
3723
+ taskAgentAid: actor.selfAid,
3724
+ targetAgentAid: agentAid,
3441
3725
  peer: actor.origin.peerId,
3442
3726
  peerKey: actor.subject.peerKey,
3443
3727
  ...(triggerId ? { triggerId } : {}),
@@ -3446,11 +3730,15 @@ async function main() {
3446
3730
  });
3447
3731
  if (!operationDecision.allow)
3448
3732
  throw new Error(operationDecision.reason);
3449
- if (actor.management)
3450
- return actor;
3451
3733
  if (!triggerId)
3452
3734
  return actor;
3453
3735
  const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
3736
+ const privilegedReason = definition ? daemonPrivilegedTriggerReason(definition) : undefined;
3737
+ if (privilegedReason && !actor.daemonPrivileged) {
3738
+ throw new Error(`${privilegedReason} requires DaemonOwner`);
3739
+ }
3740
+ if (actor.management)
3741
+ return actor;
3454
3742
  if (!definition
3455
3743
  || definition.origin?.peerId !== actor.origin?.peerId
3456
3744
  || definition.origin?.channelKey !== actor.origin?.channelKey) {
@@ -3463,8 +3751,9 @@ async function main() {
3463
3751
  const agentAid = requireAgent(cmd.agentAid);
3464
3752
  const actor = await authorizeTrigger(agentAid, 'trigger.list');
3465
3753
  const scheduler = schedulerFor(agentAid);
3466
- const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
3467
- || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
3754
+ const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => ((actor.daemonPrivileged || !daemonPrivilegedTriggerReason(trigger))
3755
+ && (actor.management
3756
+ || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey))));
3468
3757
  return { ok: true, triggers: scheduler.listItems(definitions) };
3469
3758
  }
3470
3759
  case 'trigger.show': {
@@ -3487,16 +3776,28 @@ async function main() {
3487
3776
  if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
3488
3777
  throw new Error('invalid triggerId');
3489
3778
  }
3779
+ let actor;
3490
3780
  if (cmd.triggerId)
3491
3781
  await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
3492
- else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
3782
+ else
3783
+ actor = await authorizeTrigger(agentAid, 'trigger.history');
3784
+ if (!cmd.triggerId && !actor?.management) {
3493
3785
  throw new Error('triggerId is required for non-management history access');
3494
3786
  }
3495
- return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
3787
+ const events = schedulerFor(agentAid).history(cmd.triggerId, limit);
3788
+ if (cmd.triggerId || actor?.daemonPrivileged)
3789
+ return { ok: true, events };
3790
+ const privilegedIds = new Set(schedulerFor(agentAid).list({ all: true })
3791
+ .filter(trigger => !!daemonPrivilegedTriggerReason(trigger))
3792
+ .map(trigger => trigger.id));
3793
+ return { ok: true, events: events.filter((event) => !privilegedIds.has(String(event.triggerId ?? ''))) };
3496
3794
  }
3497
3795
  case 'trigger.eventCatalog': {
3498
3796
  const agentAid = requireAgent(cmd.agentAid);
3499
- await authorizeTrigger(agentAid, 'trigger.eventCatalog');
3797
+ const actor = await authorizeTrigger(agentAid, 'trigger.eventCatalog');
3798
+ if (cmd.includeInternal === true && !actor.daemonPrivileged) {
3799
+ throw new Error('internal Trigger event catalog requires DaemonOwner');
3800
+ }
3500
3801
  return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
3501
3802
  }
3502
3803
  case 'trigger.create': {
@@ -3597,7 +3898,7 @@ async function main() {
3597
3898
  // 优雅关闭
3598
3899
  let shutdownSignal = 'unknown';
3599
3900
  let shutdownInProgress = false;
3600
- const shutdown = async (signal) => {
3901
+ shutdown = async (signal) => {
3601
3902
  if (shutdownInProgress)
3602
3903
  return;
3603
3904
  shutdownInProgress = true;
@@ -3608,6 +3909,10 @@ async function main() {
3608
3909
  logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
3609
3910
  ipcServer.stopCpuTracking();
3610
3911
  ipcServer.stop();
3912
+ try {
3913
+ fs.unlinkSync(readySignalPath);
3914
+ }
3915
+ catch { }
3611
3916
  bindService?.stopCleanup();
3612
3917
  // 必须在第一个 await 之前同步冻结 active batch、发布 task:interrupted,
3613
3918
  // 否则 handler continuation 可能先提交完成并清掉待恢复的主消息。
@@ -3664,6 +3969,10 @@ async function main() {
3664
3969
  process.exit(0);
3665
3970
  }
3666
3971
  };
3972
+ if (pendingShutdownReason) {
3973
+ const reason = pendingShutdownReason;
3974
+ setImmediate(() => { void shutdown?.(reason); });
3975
+ }
3667
3976
  process.on('SIGINT', () => shutdown('SIGINT'));
3668
3977
  process.on('SIGTERM', () => shutdown('SIGTERM'));
3669
3978
  // 全局错误处理:防止未捕获的 WebSocket 错误导致进程崩溃