evolcore 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
@@ -1,6 +1,6 @@
1
1
  import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
2
2
  import { renderCommandCardAsText } from '../interaction-router.js';
3
- import { buildEnvelope, sendInteractionPayload } from '../message/message-utils.js';
3
+ import { buildEnvelope, deliveryTargetFromSession, replyContextFromSession, sendInteractionPayload } from '../message/message-utils.js';
4
4
  import { resolvePaths, getPackageRoot } from '../../paths.js';
5
5
  import { loadDaemonConfig } from '../../config-store.js';
6
6
  import { logger } from '../../utils/logger.js';
@@ -18,6 +18,7 @@ import { displaySessionTitle } from '../session/session-title.js';
18
18
  import { buildSessionTurnList } from '../session/session-turns.js';
19
19
  import { resolveConfigCommand } from '../../config/resolved-config-op.js';
20
20
  import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
21
+ import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
21
22
  import { hashArgv } from '../auth/authorization-audit.js';
22
23
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
23
24
  import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
@@ -132,6 +133,15 @@ export class CommandHandler {
132
133
  .filter(key => key.startsWith(prefix))
133
134
  .map(key => key.slice(prefix.length));
134
135
  }
136
+ /** Return the runner-owned temporary root for a managed session, if any. */
137
+ getManagedSessionTempDir(session) {
138
+ try {
139
+ return this.getAgent(session.channel, session.baseagent).getManagedTempDir?.(session.id);
140
+ }
141
+ catch {
142
+ return undefined;
143
+ }
144
+ }
135
145
  formatBaseagentUnavailable(error) {
136
146
  const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
137
147
  return `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
@@ -321,6 +331,7 @@ export class CommandHandler {
321
331
  channelId: definition.origin.channelId,
322
332
  session: definition.origin.session,
323
333
  threadId: definition.origin.threadId,
334
+ chatType: definition.origin.delivery?.chatType,
324
335
  };
325
336
  }
326
337
  canAccessTriggerDefinition(definition, peerId, channel, isAdmin) {
@@ -403,9 +414,6 @@ export class CommandHandler {
403
414
  }
404
415
  const targetChannelType = this.resolveChannelType(feedbackChannel);
405
416
  const feedbackChannelId = parsed.targetChannelId ?? peerId;
406
- if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
407
- return { error: `AUN 渠道的 --target-channel-id 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
408
- }
409
417
  const schedulerAid = parsed.agentId
410
418
  ?? this.getOwningAgent(feedbackChannel)?.aid
411
419
  ?? tryParseChannelKey(feedbackChannel)?.selfAID
@@ -414,11 +422,22 @@ export class CommandHandler {
414
422
  if (!schedulerAid || !scheduler) {
415
423
  return { error: `目标 agent 不存在或未就绪:${schedulerAid ?? feedbackChannel}` };
416
424
  }
425
+ if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChatType) {
426
+ return { error: 'AUN 目标必须显式指定 --target-chat-type private 或 group' };
427
+ }
428
+ const targetDelivery = targetChannelType === 'aun'
429
+ ? parsed.targetChatType === 'group'
430
+ ? { chatType: 'group', groupId: feedbackChannelId }
431
+ : parsed.targetChatType === 'private'
432
+ ? { chatType: 'private' }
433
+ : { chatType: 'private' }
434
+ : undefined;
417
435
  const target = {
418
436
  channelKey: feedbackChannel,
419
437
  channelId: feedbackChannelId,
420
438
  session: parsed.targetSession,
421
439
  threadId: parsed.targetSession === 'thread' ? parsed.targetThreadId : undefined,
440
+ chatType: targetDelivery?.chatType,
422
441
  };
423
442
  if (target.session === 'thread') {
424
443
  const adapter = this.adapters.get(feedbackChannel);
@@ -439,6 +458,7 @@ export class CommandHandler {
439
458
  threadId: !originIsGroup ? threadId : undefined,
440
459
  peerId,
441
460
  sessionKey: activeSession?.sessionKey,
461
+ delivery: { chatType: 'private' },
442
462
  };
443
463
  if (!isAdmin && parsed.feedbackStrategy === 'target' && !sameFeedbackTarget(target, origin)) {
444
464
  return { error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
@@ -618,7 +638,7 @@ export class CommandHandler {
618
638
  }
619
639
  /** 从 session 提取渠道预构建的回复上下文 */
620
640
  getReplyContext(session) {
621
- return session.metadata?.replyContext;
641
+ return replyContextFromSession(session);
622
642
  }
623
643
  /**
624
644
  * 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
@@ -1426,7 +1446,7 @@ export class CommandHandler {
1426
1446
  * - taskId/chatmode:从 processing_state 和 effective chatmode 注入
1427
1447
  */
1428
1448
  buildCtlReplyContext(session) {
1429
- const ctx = {};
1449
+ const ctx = { delivery: deliveryTargetFromSession(session) };
1430
1450
  const meta = session.metadata;
1431
1451
  if (meta?.replyContext?.threadId)
1432
1452
  ctx.threadId = meta.replyContext.threadId;
@@ -1454,7 +1474,7 @@ export class CommandHandler {
1454
1474
  if (encrypted != null)
1455
1475
  ctx.metadata.encrypted = encrypted;
1456
1476
  }
1457
- return Object.keys(ctx).length > 0 ? ctx : undefined;
1477
+ return ctx;
1458
1478
  }
1459
1479
  resolveEffectiveChatmodeForSession(session) {
1460
1480
  const peerType = session.metadata?.peerType;
@@ -1509,35 +1529,51 @@ export class CommandHandler {
1509
1529
  conversationId,
1510
1530
  processOwners: loadDaemonConfig().owners ?? [],
1511
1531
  });
1512
- const resolved = resolveConfigCommand(argv, {
1513
- defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
1514
- });
1515
- if (!resolved.ok)
1516
- return { ok: false, code: resolved.code, error: resolved.reason };
1517
- const decision = authorizeOperation({
1518
- source: 'agent-tool',
1519
- subject,
1520
- intent: {
1521
- operation: resolved.command.operationId,
1522
- scope: resolved.command.commandScope,
1532
+ const executeOne = (fieldArgv) => {
1533
+ const resolved = resolveConfigCommand(fieldArgv, {
1534
+ defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
1535
+ });
1536
+ if (!resolved.ok)
1537
+ return { ok: false, code: resolved.code, error: resolved.reason };
1538
+ const decision = authorizeOperation({
1523
1539
  source: 'agent-tool',
1524
- args: {
1525
- ...(resolved.command.self ? { self: resolved.command.self } : {}),
1526
- ...(resolved.command.peerKey ? { peer: resolved.command.peerKey, peerKey: resolved.command.peerKey } : {}),
1527
- ...('field' in resolved.command && resolved.command.field ? { field: resolved.command.field } : {}),
1540
+ subject,
1541
+ intent: {
1542
+ operation: resolved.command.operationId,
1543
+ scope: resolved.command.commandScope,
1544
+ source: 'agent-tool',
1545
+ args: {
1546
+ ...(resolved.command.self ? { self: resolved.command.self } : {}),
1547
+ ...(resolved.command.peerKey ? { peer: resolved.command.peerKey, peerKey: resolved.command.peerKey } : {}),
1548
+ ...('field' in resolved.command && resolved.command.field ? { field: resolved.command.field } : {}),
1549
+ },
1550
+ dangerous: resolved.command.dangerous,
1528
1551
  },
1529
- dangerous: resolved.command.dangerous,
1530
- },
1531
- resolvedConfigCommand: resolved.command,
1532
- auditMetadata: {
1533
- argvHash: hashArgv(resolved.command.canonicalArgv),
1534
- taskId: grant.taskId,
1535
- messageId: grant.messageId,
1536
- },
1537
- });
1538
- if (!decision.allow)
1539
- return { ok: false, code: decision.code, error: decision.reason };
1540
- const result = executeResolvedConfigCommand(resolved.command, { role: subject.role });
1552
+ resolvedConfigCommand: resolved.command,
1553
+ auditMetadata: {
1554
+ argvHash: hashArgv(resolved.command.canonicalArgv),
1555
+ taskId: grant.taskId,
1556
+ messageId: grant.messageId,
1557
+ },
1558
+ });
1559
+ if (!decision.allow)
1560
+ return { ok: false, code: decision.code, error: decision.reason };
1561
+ return executeResolvedConfigCommand(resolved.command, { role: subject.role });
1562
+ };
1563
+ const batch = splitConfigBatchGetArgv(argv);
1564
+ if (batch && batch.fields.length > 1) {
1565
+ const results = batch.fields.map(field => {
1566
+ const result = executeOne(['config', 'get', field, ...batch.rest]);
1567
+ if (!result.ok)
1568
+ return { ok: false, field, code: result.code, error: result.error };
1569
+ if (result.subcommand !== 'get') {
1570
+ return { ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` };
1571
+ }
1572
+ return result;
1573
+ });
1574
+ return { ok: true, result: { ok: true, subcommand: 'batch-get', results } };
1575
+ }
1576
+ const result = executeOne(argv);
1541
1577
  return result.ok
1542
1578
  ? { ok: true, result }
1543
1579
  : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
@@ -1747,23 +1783,41 @@ export class CommandHandler {
1747
1783
  const filePath = String(parsed.command.filePath || '');
1748
1784
  const projectPath = path.resolve(session.projectPath);
1749
1785
  const resolvedPath = path.resolve(projectPath, filePath);
1750
- if (!filePath || !isSameOrDescendant(resolvedPath, projectPath)) {
1751
- return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file must be inside the current session project' };
1786
+ const managedTempPath = this.getManagedSessionTempDir(session);
1787
+ const allowedPaths = [projectPath, managedTempPath].filter((value) => !!value);
1788
+ if (!filePath || !allowedPaths.some(root => isSameOrDescendant(resolvedPath, root))) {
1789
+ return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file must be inside the current session project or managed TMPDIR' };
1752
1790
  }
1753
1791
  if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
1754
1792
  return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
1755
1793
  }
1794
+ let rulesFd;
1756
1795
  try {
1757
1796
  const realProjectPath = fs.realpathSync(projectPath);
1758
1797
  const realPath = fs.realpathSync(resolvedPath);
1759
- if (!isSameOrDescendant(realPath, realProjectPath)) {
1760
- return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file resolves outside the current session project' };
1798
+ let realManagedTempPath;
1799
+ try {
1800
+ realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
1801
+ }
1802
+ catch {
1803
+ // The session may have closed and removed its temporary directory.
1804
+ }
1805
+ const realAllowedPaths = [realProjectPath, realManagedTempPath].filter((value) => !!value);
1806
+ if (!realAllowedPaths.some(root => isSameOrDescendant(realPath, root))) {
1807
+ return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file resolves outside the current session project or managed TMPDIR' };
1761
1808
  }
1762
- const stat = fs.statSync(realPath);
1809
+ if (isHClassPath(realPath) || isLClassPath(realPath)) {
1810
+ return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
1811
+ }
1812
+ rulesFd = fs.openSync(realPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
1813
+ const stat = fs.fstatSync(rulesFd);
1763
1814
  if (!stat.isFile() || stat.size < 1 || stat.size > 4 * 1024) {
1764
1815
  return { ok: false, code: 'INVALID_RULES_CONTENT', error: 'Rules file must be a regular file from 1 to 4096 bytes' };
1765
1816
  }
1766
- rulesContent = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(realPath));
1817
+ if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
1818
+ return { ok: false, code: 'INVALID_RULES_PATH', error: 'Rules file cannot be a hard link' };
1819
+ }
1820
+ rulesContent = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(rulesFd));
1767
1821
  }
1768
1822
  catch (error) {
1769
1823
  return {
@@ -1772,6 +1826,10 @@ export class CommandHandler {
1772
1826
  error: error instanceof Error ? error.message : String(error),
1773
1827
  };
1774
1828
  }
1829
+ finally {
1830
+ if (rulesFd !== undefined)
1831
+ fs.closeSync(rulesFd);
1832
+ }
1775
1833
  }
1776
1834
  try {
1777
1835
  return { ok: true, result: await executeManagedGroupOperation(parsed.command, rulesContent) };
@@ -1860,7 +1918,7 @@ export class CommandHandler {
1860
1918
  };
1861
1919
  }
1862
1920
  }
1863
- /** Agent-managed current-group filesystem read entrypoint. */
1921
+ /** Agent-managed current-group filesystem entrypoint. */
1864
1922
  async handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1865
1923
  if (!this.agentDelegationRegistry) {
1866
1924
  return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
@@ -1881,7 +1939,7 @@ export class CommandHandler {
1881
1939
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1882
1940
  }
1883
1941
  if (grant.channelType !== 'aun' || grant.chatType !== 'group') {
1884
- return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed fs reads require the current AUN group relation' };
1942
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed fs operations require the current AUN group relation' };
1885
1943
  }
1886
1944
  let currentGroup;
1887
1945
  try {
@@ -1904,6 +1962,19 @@ export class CommandHandler {
1904
1962
  conversationId: currentGroup,
1905
1963
  processOwners: loadDaemonConfig().owners ?? [],
1906
1964
  });
1965
+ const intentArgs = {
1966
+ self: actorAid,
1967
+ targetId: currentGroup,
1968
+ peer: currentGroup,
1969
+ peerKey: formatPeerKey('aun', currentGroup),
1970
+ path: parsed.command.remotePath,
1971
+ targetPath: parsed.command.remotePath,
1972
+ ...(parsed.command.remoteSourcePath ? { sourcePath: parsed.command.remoteSourcePath } : {}),
1973
+ ...(parsed.command.localPath ? { localPath: parsed.command.localPath } : {}),
1974
+ ...(parsed.command.recursive !== undefined ? { recursive: parsed.command.recursive } : {}),
1975
+ ...(parsed.command.parents !== undefined ? { parents: parsed.command.parents } : {}),
1976
+ ...(parsed.command.overwrite !== undefined ? { overwrite: parsed.command.overwrite } : {}),
1977
+ };
1907
1978
  const decision = authorizeOperation({
1908
1979
  source: 'agent-tool',
1909
1980
  subject,
@@ -1911,17 +1982,60 @@ export class CommandHandler {
1911
1982
  operation: parsed.command.operationId,
1912
1983
  scope: 'relation',
1913
1984
  source: 'agent-tool',
1914
- args: {
1915
- self: actorAid,
1916
- targetId: currentGroup,
1917
- peer: currentGroup,
1918
- peerKey: formatPeerKey('aun', currentGroup),
1919
- },
1985
+ args: intentArgs,
1986
+ },
1987
+ auditMetadata: {
1988
+ argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId,
1989
+ sessionId, agentAid: actorAid, toolName: 'ec.fs',
1920
1990
  },
1921
- auditMetadata: { argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId },
1922
1991
  });
1923
1992
  if (!decision.allow)
1924
1993
  return { ok: false, code: decision.code, error: decision.reason };
1994
+ if (parsed.command.kind === 'upload') {
1995
+ const localPath = String(parsed.command.localPath || '').trim();
1996
+ const projectPath = path.resolve(session.projectPath);
1997
+ if (!localPath)
1998
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'Managed upload source is required' };
1999
+ const resolvedPath = path.resolve(projectPath, localPath);
2000
+ const managedTempPath = this.getManagedSessionTempDir(session);
2001
+ const allowedRoots = [projectPath, managedTempPath].filter((value) => !!value);
2002
+ if (!allowedRoots.some(root => isSameOrDescendant(resolvedPath, root))) {
2003
+ return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source must be inside the current session project or managed TMPDIR' };
2004
+ }
2005
+ if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
2006
+ return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source cannot be a protected EvolCore path' };
2007
+ }
2008
+ try {
2009
+ const realProjectPath = fs.realpathSync(projectPath);
2010
+ const stat = fs.lstatSync(resolvedPath);
2011
+ if (stat.isSymbolicLink() || !stat.isFile()) {
2012
+ return { ok: false, code: 'INVALID_LOCAL_FILE', error: 'Upload source must be a regular non-symlink file' };
2013
+ }
2014
+ const realPath = fs.realpathSync(resolvedPath);
2015
+ let realManagedTempPath;
2016
+ try {
2017
+ realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
2018
+ }
2019
+ catch {
2020
+ // A removed or invalid session temp root is not an upload root.
2021
+ }
2022
+ const realAllowedRoots = [realProjectPath, realManagedTempPath].filter((value) => !!value);
2023
+ if (!realAllowedRoots.some(root => isSameOrDescendant(realPath, root))
2024
+ || isHClassPath(realPath) || isLClassPath(realPath)) {
2025
+ return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source resolves outside the allowed project or managed TMPDIR path' };
2026
+ }
2027
+ if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
2028
+ return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source cannot be a hard link inside managed TMPDIR' };
2029
+ }
2030
+ if (stat.size > 16 * 1024 * 1024) {
2031
+ return { ok: false, code: 'LOCAL_FILE_TOO_LARGE', error: 'Managed upload is limited to 16 MiB' };
2032
+ }
2033
+ parsed.command.localPath = realPath;
2034
+ }
2035
+ catch (error) {
2036
+ return { ok: false, code: 'INVALID_LOCAL_FILE', error: error instanceof Error ? error.message : String(error) };
2037
+ }
2038
+ }
1925
2039
  try {
1926
2040
  return { ok: true, result: await executeManagedFsOperation(parsed.command) };
1927
2041
  }
@@ -2126,6 +2240,14 @@ export class CommandHandler {
2126
2240
  const showId = args.includes('--showid');
2127
2241
  const formatJson = args.includes('--format json');
2128
2242
  const full = args.includes('--full');
2243
+ const after = parseQueueTimeArg(this.extractArg(args, '--after'));
2244
+ const before = parseQueueTimeArg(this.extractArg(args, '--before'));
2245
+ if (this.extractArg(args, '--after') && after === undefined)
2246
+ return { ok: false, error: '❌ --after 必须是 epoch 毫秒或 ISO 时间' };
2247
+ if (this.extractArg(args, '--before') && before === undefined)
2248
+ return { ok: false, error: '❌ --before 必须是 epoch 毫秒或 ISO 时间' };
2249
+ if (after !== undefined && before !== undefined && after >= before)
2250
+ return { ok: false, error: '❌ --after 必须早于 --before' };
2129
2251
  // 操作分支
2130
2252
  if (args.includes('--clear')) {
2131
2253
  const count = this.messageQueue.clearBySession(sessionId);
@@ -2147,13 +2269,19 @@ export class CommandHandler {
2147
2269
  : { ok: false, error: `❌ 当前无处理中任务` };
2148
2270
  }
2149
2271
  // 查询
2150
- const items = this.messageQueue.getQueueItemsBySession(sessionId);
2272
+ const items = this.messageQueue.getQueueItemsBySession(sessionId, { after, before });
2151
2273
  if (formatJson) {
2152
2274
  return { ok: true, result: JSON.stringify({ items }, null, 2) };
2153
2275
  }
2154
2276
  return { ok: true, result: renderQueueItemsCtl(items, showId, full) };
2155
2277
  }
2156
2278
  }
2279
+ function parseQueueTimeArg(value) {
2280
+ if (!value)
2281
+ return undefined;
2282
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
2283
+ return Number.isFinite(parsed) ? parsed : undefined;
2284
+ }
2157
2285
  /**
2158
2286
  * ctl 专用渲染:不显示 session 标识列(因为只有一个 session)
2159
2287
  */
@@ -2176,14 +2304,33 @@ function renderQueueItemsCtl(items, showId, full) {
2176
2304
  parts.push(` ${name} `);
2177
2305
  }
2178
2306
  const content = full ? item.preview.replace('...', '') : item.preview;
2179
- parts.push(`"${content}"`);
2307
+ const timing = [
2308
+ item.receivedAt != null ? `recv=${new Date(item.receivedAt).toISOString()}` : undefined,
2309
+ item.enqueuedAt != null ? `enq=${new Date(item.enqueuedAt).toISOString()}` : undefined,
2310
+ item.queueWaitMs != null ? `wait=${formatQueueDuration(item.queueWaitMs)}` : undefined,
2311
+ ].filter(Boolean).join(' ');
2312
+ parts.push(`${timing ? `${timing} ` : ''}"${content}"`);
2180
2313
  lines.push(parts.join(''));
2181
2314
  }
2182
2315
  return lines.join('\n');
2183
2316
  }
2317
+ function formatQueueDuration(ms) {
2318
+ const sec = Math.max(0, Math.round(ms / 1000));
2319
+ return sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
2320
+ }
2184
2321
  function sameFeedbackTarget(target, origin) {
2185
2322
  if (!origin)
2186
2323
  return false;
2324
+ const targetChannelType = tryParseChannelKey(target.channelKey)?.type
2325
+ ?? target.channelKey.split('#', 1)[0];
2326
+ const targetChatType = target.chatType
2327
+ ?? (targetChannelType === 'aun' ? undefined : 'private');
2328
+ if (!targetChatType)
2329
+ return false;
2330
+ const originDelivery = origin.delivery;
2331
+ const originChatType = originDelivery?.chatType ?? 'private';
2332
+ if (targetChatType !== originChatType)
2333
+ return false;
2187
2334
  return target.channelKey === origin.channelKey
2188
2335
  && target.channelId === origin.channelId
2189
2336
  && target.session === origin.session
@@ -131,6 +131,8 @@ export async function resolveMenuCatalogContext(input) {
131
131
  const rebuilt = buildAuthSubject({
132
132
  selfAid: subject.selfAid,
133
133
  actorId: subject.actorId,
134
+ requestId: subject.requestId,
135
+ sessionId: subject.sessionId,
134
136
  channel: subject.channel,
135
137
  channelType: transport,
136
138
  channelId: subject.actorId,
@@ -191,6 +193,8 @@ export async function resolveMenuCatalogContext(input) {
191
193
  const rebuilt = buildAuthSubject({
192
194
  selfAid: subject.selfAid,
193
195
  actorId: subject.actorId,
196
+ requestId: subject.requestId,
197
+ sessionId: subject.sessionId,
194
198
  channel: subject.channel,
195
199
  channelType: transport,
196
200
  channelId: canonicalGroupId,
@@ -6,7 +6,7 @@ import { hasModelSwitcher } from '../../agents/runner-types.js';
6
6
  import { getCodexEfforts } from '../../agents/codex-runner.js';
7
7
  import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
9
- import { buildEnvelope } from '../message/message-utils.js';
9
+ import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
10
10
  import path from 'path';
11
11
  import fs from 'fs';
12
12
  import os from 'os';
@@ -28,7 +28,7 @@ import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArg
28
28
  import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
29
29
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
30
30
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
31
- import { splitConfigBatchGetArgv } from '../../config/config-batch-get.js';
31
+ import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
32
32
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
33
33
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
34
34
  import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
@@ -36,6 +36,7 @@ import { isManagementRole } from '../../config/builtin-roles.js';
36
36
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
37
37
  import { logger } from '../../utils/logger.js';
38
38
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
39
+ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
39
40
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
40
41
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
41
42
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
@@ -1013,8 +1014,13 @@ function buildRelationIntentArgs(params) {
1013
1014
  return out;
1014
1015
  }
1015
1016
  function buildMenuAuthSubject(owner, params) {
1016
- if (params.subject)
1017
- return params.subject;
1017
+ if (params.subject) {
1018
+ return {
1019
+ ...params.subject,
1020
+ ...(params.requestId && !params.subject.requestId ? { requestId: params.requestId } : {}),
1021
+ ...(params.session?.id && !params.subject.sessionId ? { sessionId: params.session.id } : {}),
1022
+ };
1023
+ }
1018
1024
  const agent = owner.getOwningAgent?.(params.channel);
1019
1025
  const selfAid = agent?.aid || params.session?.selfAID;
1020
1026
  const channelType = owner.resolveChannelType?.(params.channel) || params.session?.channelType;
@@ -1025,6 +1031,8 @@ function buildMenuAuthSubject(owner, params) {
1025
1031
  return buildAuthSubject({
1026
1032
  selfAid,
1027
1033
  actorId: params.userId,
1034
+ requestId: params.requestId,
1035
+ sessionId: params.session?.id,
1028
1036
  channel: params.channel,
1029
1037
  channelType: channelType || params.channel.split('#')[0],
1030
1038
  channelId: params.channelId,
@@ -1114,7 +1122,7 @@ function buildRoleMenuContext(owner, channel, subject) {
1114
1122
  channel: contactAdapter.channelName,
1115
1123
  channelId: applicantAid,
1116
1124
  agentName: owningAgent?.name ?? '<unknown>',
1117
- replyContext: { metadata: evolMenuResponseTransportMetadata() },
1125
+ replyContext: { delivery: { chatType: 'private' }, metadata: evolMenuResponseTransportMetadata() },
1118
1126
  }), {
1119
1127
  kind: 'custom',
1120
1128
  channelType: 'aun',
@@ -2710,6 +2718,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2710
2718
  const subject = buildMenuAuthSubject(this, {
2711
2719
  identity: authIdentity,
2712
2720
  session: authSession,
2721
+ requestId,
2713
2722
  explicitChatType,
2714
2723
  channel,
2715
2724
  channelId,
@@ -2927,6 +2936,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2927
2936
  if (triggerThread !== undefined && triggerThread !== 'per_run' && triggerThread !== 'by_run' && triggerThread !== 'by_trigger') {
2928
2937
  return { error: `无效 triggerThread: ${triggerThread}`, code: 'INVALID_ARGS' };
2929
2938
  }
2939
+ const targetChatType = args.targetChatType;
2940
+ if (targetChatType !== undefined && targetChatType !== 'private' && targetChatType !== 'group') {
2941
+ return { error: `无效 targetChatType: ${targetChatType}`, code: 'INVALID_ARGS' };
2942
+ }
2930
2943
  const parsed = {
2931
2944
  scheduleType: args.scheduleType,
2932
2945
  scheduleValue: String(args.scheduleValue ?? ''),
@@ -2937,6 +2950,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2937
2950
  name: args.name,
2938
2951
  targetChannel: args.targetChannel,
2939
2952
  targetChannelId: args.targetChannelId,
2953
+ targetChatType,
2940
2954
  targetThreadId: args.targetThreadId,
2941
2955
  agentId: args.agentId,
2942
2956
  model: args.model,
@@ -3313,13 +3327,56 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3313
3327
  }
3314
3328
  if (action === 'restart') {
3315
3329
  const suppressRealRestart = shouldSuppressRealRestart();
3330
+ let restartReplyContext;
3331
+ try {
3332
+ restartReplyContext = session
3333
+ ? replyContextFromSession(session, transportReplyContext)
3334
+ : transportReplyContext;
3335
+ }
3336
+ catch (error) {
3337
+ const code = error?.code;
3338
+ if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
3339
+ || (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
3340
+ throw error;
3341
+ }
3342
+ return {
3343
+ error: '当前 AUN 会话缺少有效的持久化出站路由,请从可信入站上下文重试',
3344
+ code,
3345
+ };
3346
+ }
3347
+ const restartDelivery = restartReplyContext?.delivery
3348
+ ?? (explicitChatType === 'group'
3349
+ ? { chatType: 'group', groupId: channelId }
3350
+ : explicitChatType === 'private'
3351
+ ? { chatType: 'private' }
3352
+ : undefined);
3353
+ if (channel.split('#', 1)[0].toLowerCase() === 'aun'
3354
+ && !isDeliveryTargetForChannel(restartDelivery, channelId)) {
3355
+ return {
3356
+ error: '无法确定有效的重启通知出站路由,请从可信会话上下文重试',
3357
+ code: isDeliveryTarget(restartDelivery)
3358
+ ? 'AUN_OUTBOUND_ROUTE_MISMATCH'
3359
+ : 'AUN_OUTBOUND_ROUTE_REQUIRED',
3360
+ };
3361
+ }
3316
3362
  if (!suppressRealRestart) {
3317
- const restartInfo = { channel, channelId, timestamp: Date.now() };
3363
+ const restartInfo = {
3364
+ channel,
3365
+ channelId,
3366
+ timestamp: Date.now(),
3367
+ ...(restartDelivery ? { delivery: restartDelivery } : {}),
3368
+ };
3318
3369
  const controlDir = daemonControlDir();
3319
3370
  fs.mkdirSync(controlDir, { recursive: true });
3320
3371
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3321
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3372
+ // Use the canonical CLI restart path so the restart itself performs
3373
+ // the version check and automatic upgrades before replacing the daemon.
3374
+ // Do not inherit managed-session credentials: otherwise cmdRestart can
3375
+ // delegate back to this daemon and recursively enqueue another restart.
3376
+ spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart'], {
3322
3377
  EVOLCORE_HOME: resolvePaths().root,
3378
+ EVOLCORE_SESSION_ID: '',
3379
+ [AGENT_DELEGATION_TOKEN_ENV]: '',
3323
3380
  });
3324
3381
  }
3325
3382
  else {
@@ -3386,6 +3443,17 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3386
3443
  ? (session?.metadata?.groupId || channelId)
3387
3444
  : userId;
3388
3445
  const cliPeerKey = cliChannelType && cliPeerKeyId ? formatPeerKey(cliChannelType, cliPeerKeyId) : undefined;
3446
+ const subject = buildMenuAuthSubject(this, {
3447
+ identity,
3448
+ session,
3449
+ requestId,
3450
+ explicitChatType,
3451
+ channel,
3452
+ channelId,
3453
+ userId,
3454
+ fromControlChannel: fromControlChannel ?? false,
3455
+ subject: authSubject,
3456
+ });
3389
3457
  if (Array.isArray(args?.argv)) {
3390
3458
  argv = withDefaultRelationContext(argv, { self: cliSelfAid, peer: cliPeerKey });
3391
3459
  }
@@ -3404,6 +3472,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3404
3472
  await auditCommandAuthorization({
3405
3473
  ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3406
3474
  scope: parsed.intent.scope, dangerous: true, actorId: userId,
3475
+ requestId, sessionId: subject.sessionId, agentAid: subject.selfAid,
3476
+ permissionMode: subject.permissionMode,
3407
3477
  channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
3408
3478
  reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
3409
3479
  name: 'cli', action: 'exec', args: { argv: [...requestedArgv] },
@@ -3412,16 +3482,6 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3412
3482
  }
3413
3483
  parsedCommands.push(parsed);
3414
3484
  }
3415
- const subject = buildMenuAuthSubject(this, {
3416
- identity,
3417
- session,
3418
- explicitChatType,
3419
- channel,
3420
- channelId,
3421
- userId,
3422
- fromControlChannel: fromControlChannel ?? false,
3423
- subject: authSubject,
3424
- });
3425
3485
  const selfAid = subject.selfAid;
3426
3486
  const peerKey = subject.peerKey;
3427
3487
  const isDaemonOwner = subject.isDaemonOwner;
@@ -3446,6 +3506,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3446
3506
  allowExplicitRelationTarget: true,
3447
3507
  auditAllowed: false,
3448
3508
  auditMetadata: {
3509
+ requestId,
3510
+ sessionId: subject.sessionId,
3511
+ agentAid: subject.selfAid,
3512
+ permissionMode: subject.permissionMode,
3449
3513
  taskId: requestId,
3450
3514
  argvHash: hashArgv(authArgv),
3451
3515
  name: 'cli',
@@ -3466,6 +3530,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3466
3530
  ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3467
3531
  scope: parsed.intent.scope, dangerous: parsed.intent.dangerous ?? false,
3468
3532
  actorId: userId, selfAid, peerKey,
3533
+ requestId, sessionId: subject.sessionId, agentAid: selfAid,
3534
+ permissionMode: subject.permissionMode,
3469
3535
  channel, channelId, role: identity.role, isDaemonOwner,
3470
3536
  fromControlChannel: fromControlChannel ?? false, decision: 'allow',
3471
3537
  matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
@@ -3509,7 +3575,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3509
3575
  return { error: '通道不存在', code: 'EXEC_FAILED' };
3510
3576
  if (!adapter.capabilities?.file)
3511
3577
  return { error: '通道不支持文件发送', code: 'NOT_SUPPORTED' };
3512
- const sessionReplyContext = session ? this.getReplyContext(session) : undefined;
3578
+ const sessionReplyContext = session ? replyContextFromSession(session, this.getReplyContext(session)) : undefined;
3513
3579
  const replyCtx = transportReplyContext
3514
3580
  ? {
3515
3581
  ...(sessionReplyContext ?? {}),
@@ -3593,6 +3659,7 @@ async function execMenuForSystemControl(payload, context) {
3593
3659
  const trustedSubject = buildAuthSubject({
3594
3660
  selfAid: targetAid,
3595
3661
  actorId: context.actorAid,
3662
+ requestId: id !== undefined && id !== null && String(id) ? String(id) : undefined,
3596
3663
  channel: targetChannel,
3597
3664
  channelType: 'aun',
3598
3665
  channelId: context.actorAid || SYSTEM_CONTROL_CHANNEL,