evolcore 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -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';
@@ -9,6 +9,7 @@ import { isManagementRole } from '../../config/builtin-roles.js';
9
9
  import { formatPeerKey } from '../relation/peer-identity.js';
10
10
  import crypto from 'crypto';
11
11
  import path from 'path';
12
+ import fs from 'fs';
12
13
  import { parseDuration, parseTriggerSet, parseTriggerUpdate } from '../../trigger/parser.js';
13
14
  import { applyTriggerPatch, replaceEditableTriggerDefinition } from '../../trigger/patch.js';
14
15
  import { definitionRevision } from '../../trigger/validation.js';
@@ -17,16 +18,22 @@ import { displaySessionTitle } from '../session/session-title.js';
17
18
  import { buildSessionTurnList } from '../session/session-turns.js';
18
19
  import { resolveConfigCommand } from '../../config/resolved-config-op.js';
19
20
  import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
21
+ import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
20
22
  import { hashArgv } from '../auth/authorization-audit.js';
21
23
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
24
+ import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
25
+ import { executeManagedGroupOperation, isManagedGroupRelationRead, ManagedGroupOperationError, resolveManagedGroupOperation, executeManagedMsgOperation, ManagedMsgOperationError, resolveManagedMsgOperation, executeManagedFsOperation, ManagedFsOperationError, resolveManagedFsOperation, } from '../../aun/msg/managed-operation.js';
22
26
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
27
+ import { isHClassPath, isLClassPath, isSameOrDescendant } from '../protected-paths.js';
23
28
  import { validateModelSelectionForRole } from '../model/model-permission.js';
29
+ import { getModelInfo } from '../model/model-catalog.js';
24
30
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
25
31
  import { parsePeerKey } from '../relation/peer-identity.js';
26
32
  import { isQuickCommand } from './slash-gate.js';
27
33
  import { handleSlashCommand } from './slash-handler.js';
28
34
  import { resolveChatMode } from '../message/peer-mode.js';
29
35
  import { execMenuAction as menuExecMenuAction, execMenuForEcweb as menuExecMenuForEcweb, execMenuForControl as menuExecMenuForControl, execMenuQuery as menuExecMenuQuery, execMenuUpdate as menuExecMenuUpdate, getMenuItems as menuGetMenuItems, getSubMenuItems as menuGetSubMenuItems, } from './menu-handler.js';
36
+ import { getMenuCatalog, execMenuValueBatch, validateMenuValueBatchArgs } from './menu-catalog.js';
30
37
  export { isProcessLevelOwner } from './menu-handler.js';
31
38
  const CLI_EXEC_TIMEOUT_MS = 15_000;
32
39
  const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
@@ -93,6 +100,7 @@ export class CommandHandler {
93
100
  triggerSchedulerFallbackAid;
94
101
  daemonStatusProvider;
95
102
  agentDelegationRegistry;
103
+ handoffRuntime;
96
104
  /**
97
105
  * Get the runner for a (channel, baseagent) pair.
98
106
  *
@@ -125,6 +133,15 @@ export class CommandHandler {
125
133
  .filter(key => key.startsWith(prefix))
126
134
  .map(key => key.slice(prefix.length));
127
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
+ }
128
145
  formatBaseagentUnavailable(error) {
129
146
  const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
130
147
  return `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
@@ -314,6 +331,7 @@ export class CommandHandler {
314
331
  channelId: definition.origin.channelId,
315
332
  session: definition.origin.session,
316
333
  threadId: definition.origin.threadId,
334
+ chatType: definition.origin.delivery?.chatType,
317
335
  };
318
336
  }
319
337
  canAccessTriggerDefinition(definition, peerId, channel, isAdmin) {
@@ -396,9 +414,6 @@ export class CommandHandler {
396
414
  }
397
415
  const targetChannelType = this.resolveChannelType(feedbackChannel);
398
416
  const feedbackChannelId = parsed.targetChannelId ?? peerId;
399
- if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
400
- return { error: `AUN 渠道的 --target-channel-id 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
401
- }
402
417
  const schedulerAid = parsed.agentId
403
418
  ?? this.getOwningAgent(feedbackChannel)?.aid
404
419
  ?? tryParseChannelKey(feedbackChannel)?.selfAID
@@ -407,11 +422,22 @@ export class CommandHandler {
407
422
  if (!schedulerAid || !scheduler) {
408
423
  return { error: `目标 agent 不存在或未就绪:${schedulerAid ?? feedbackChannel}` };
409
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;
410
435
  const target = {
411
436
  channelKey: feedbackChannel,
412
437
  channelId: feedbackChannelId,
413
438
  session: parsed.targetSession,
414
439
  threadId: parsed.targetSession === 'thread' ? parsed.targetThreadId : undefined,
440
+ chatType: targetDelivery?.chatType,
415
441
  };
416
442
  if (target.session === 'thread') {
417
443
  const adapter = this.adapters.get(feedbackChannel);
@@ -432,6 +458,7 @@ export class CommandHandler {
432
458
  threadId: !originIsGroup ? threadId : undefined,
433
459
  peerId,
434
460
  sessionKey: activeSession?.sessionKey,
461
+ delivery: { chatType: 'private' },
435
462
  };
436
463
  if (!isAdmin && parsed.feedbackStrategy === 'target' && !sameFeedbackTarget(target, origin)) {
437
464
  return { error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
@@ -611,7 +638,7 @@ export class CommandHandler {
611
638
  }
612
639
  /** 从 session 提取渠道预构建的回复上下文 */
613
640
  getReplyContext(session) {
614
- return session.metadata?.replyContext;
641
+ return replyContextFromSession(session);
615
642
  }
616
643
  /**
617
644
  * 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
@@ -675,6 +702,9 @@ export class CommandHandler {
675
702
  setMessageQueue(messageQueue) {
676
703
  this.messageQueue = messageQueue;
677
704
  }
705
+ setHandoffRuntime(handoffRuntime) {
706
+ this.handoffRuntime = handoffRuntime;
707
+ }
678
708
  setPermissionGateway(gateway) {
679
709
  this.permissionGateway = gateway;
680
710
  }
@@ -818,6 +848,29 @@ export class CommandHandler {
818
848
  getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
819
849
  return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
820
850
  }
851
+ async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent') {
852
+ return await getMenuCatalog({
853
+ subject,
854
+ channelType: this.resolveChannelType(channel),
855
+ channelScope,
856
+ channelId,
857
+ request,
858
+ });
859
+ }
860
+ async execMenuValueBatch(channel, channelId, subject, args, request, channelScope = 'agent') {
861
+ validateMenuValueBatchArgs(args);
862
+ return await execMenuValueBatch({
863
+ subject,
864
+ channelType: this.resolveChannelType(channel),
865
+ channelScope,
866
+ channelId,
867
+ request,
868
+ items: args?.items,
869
+ schemaVersion: args?.schemaVersion,
870
+ consistency: args?.consistency,
871
+ execQuery: async (cmd, resolvedChannelId, queryArgs, context) => this.execMenuQuery(cmd, channel, resolvedChannelId, context.subject.actorId, queryArgs, context.subject.chatType, context.subject.fromControlChannel, context.subject.identity, context.subject, channelScope === 'control' ? 'control' : 'menu'),
872
+ });
873
+ }
821
874
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
822
875
  async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
823
876
  return await menuGetSubMenuItems.call(this, cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel, authSubject, source);
@@ -1393,7 +1446,7 @@ export class CommandHandler {
1393
1446
  * - taskId/chatmode:从 processing_state 和 effective chatmode 注入
1394
1447
  */
1395
1448
  buildCtlReplyContext(session) {
1396
- const ctx = {};
1449
+ const ctx = { delivery: deliveryTargetFromSession(session) };
1397
1450
  const meta = session.metadata;
1398
1451
  if (meta?.replyContext?.threadId)
1399
1452
  ctx.threadId = meta.replyContext.threadId;
@@ -1421,7 +1474,7 @@ export class CommandHandler {
1421
1474
  if (encrypted != null)
1422
1475
  ctx.metadata.encrypted = encrypted;
1423
1476
  }
1424
- return Object.keys(ctx).length > 0 ? ctx : undefined;
1477
+ return ctx;
1425
1478
  }
1426
1479
  resolveEffectiveChatmodeForSession(session) {
1427
1480
  const peerType = session.metadata?.peerType;
@@ -1476,35 +1529,51 @@ export class CommandHandler {
1476
1529
  conversationId,
1477
1530
  processOwners: loadDaemonConfig().owners ?? [],
1478
1531
  });
1479
- const resolved = resolveConfigCommand(argv, {
1480
- defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
1481
- });
1482
- if (!resolved.ok)
1483
- return { ok: false, code: resolved.code, error: resolved.reason };
1484
- const decision = authorizeOperation({
1485
- source: 'agent-tool',
1486
- subject,
1487
- intent: {
1488
- operation: resolved.command.operationId,
1489
- 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({
1490
1539
  source: 'agent-tool',
1491
- args: {
1492
- ...(resolved.command.self ? { self: resolved.command.self } : {}),
1493
- ...(resolved.command.peerKey ? { peer: resolved.command.peerKey, peerKey: resolved.command.peerKey } : {}),
1494
- ...('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,
1495
1551
  },
1496
- dangerous: resolved.command.dangerous,
1497
- },
1498
- resolvedConfigCommand: resolved.command,
1499
- auditMetadata: {
1500
- argvHash: hashArgv(resolved.command.canonicalArgv),
1501
- taskId: grant.taskId,
1502
- messageId: grant.messageId,
1503
- },
1504
- });
1505
- if (!decision.allow)
1506
- return { ok: false, code: decision.code, error: decision.reason };
1507
- 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);
1508
1577
  return result.ok
1509
1578
  ? { ok: true, result }
1510
1579
  : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
@@ -1569,6 +1638,415 @@ export class CommandHandler {
1569
1638
  ? { ok: true, result }
1570
1639
  : { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
1571
1640
  }
1641
+ /** Agent-managed AID CLI entrypoint. Private key and CA access stay inside the daemon. */
1642
+ async handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1643
+ if (!this.agentDelegationRegistry) {
1644
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1645
+ }
1646
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1647
+ if (!delegation.ok)
1648
+ return { ok: false, code: delegation.code, error: delegation.reason };
1649
+ const session = await this.sessionManager.getSessionById(sessionId);
1650
+ if (!session)
1651
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1652
+ const parsed = resolveManagedAidOperation(argv);
1653
+ if (!parsed.ok)
1654
+ return { ok: false, code: parsed.code, error: parsed.reason };
1655
+ const grantSelfAid = delegation.grant.selfAid.replace(/^@/, '');
1656
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1657
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1658
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1659
+ }
1660
+ if (parsed.command.aid !== grantSelfAid) {
1661
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can be targeted' };
1662
+ }
1663
+ let conversationId;
1664
+ try {
1665
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
1666
+ }
1667
+ catch {
1668
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1669
+ }
1670
+ const subject = buildAuthSubject({
1671
+ selfAid: delegation.grant.selfAid,
1672
+ actorId: delegation.grant.actorId,
1673
+ channel: delegation.grant.channel,
1674
+ channelType: delegation.grant.channelType,
1675
+ channelId: conversationId,
1676
+ chatType: delegation.grant.chatType,
1677
+ conversationId,
1678
+ processOwners: loadDaemonConfig().owners ?? [],
1679
+ });
1680
+ const managementRole = isManagementRole(subject.role);
1681
+ const decision = authorizeOperation({
1682
+ source: 'agent-tool',
1683
+ subject,
1684
+ intent: {
1685
+ operation: parsed.command.operationId,
1686
+ scope: managementRole ? 'agent' : 'relation',
1687
+ source: 'agent-tool',
1688
+ args: { self: parsed.command.aid, targetId: parsed.command.aid },
1689
+ },
1690
+ auditMetadata: {
1691
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1692
+ taskId: delegation.grant.taskId,
1693
+ messageId: delegation.grant.messageId,
1694
+ },
1695
+ });
1696
+ if (!decision.allow)
1697
+ return { ok: false, code: decision.code, error: decision.reason };
1698
+ try {
1699
+ return { ok: true, result: await executeManagedAidOperation(parsed.command) };
1700
+ }
1701
+ catch (error) {
1702
+ const message = error instanceof Error ? error.message : String(error);
1703
+ return {
1704
+ ok: false,
1705
+ code: error instanceof ManagedAidOperationError ? error.code : 'AID_OPERATION_FAILED',
1706
+ error: message,
1707
+ };
1708
+ }
1709
+ }
1710
+ /** Agent-managed group CLI entrypoint. AUN private material remains daemon-owned. */
1711
+ async handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1712
+ if (!this.agentDelegationRegistry) {
1713
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1714
+ }
1715
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1716
+ if (!delegation.ok)
1717
+ return { ok: false, code: delegation.code, error: delegation.reason };
1718
+ const session = await this.sessionManager.getSessionById(sessionId);
1719
+ if (!session)
1720
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1721
+ const parsed = resolveManagedGroupOperation(argv);
1722
+ if (!parsed.ok)
1723
+ return { ok: false, code: parsed.code, error: parsed.reason };
1724
+ const grant = delegation.grant;
1725
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
1726
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1727
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1728
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1729
+ }
1730
+ if (parsed.command.from !== grantSelfAid) {
1731
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can perform managed group operations' };
1732
+ }
1733
+ let conversationId;
1734
+ try {
1735
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1736
+ }
1737
+ catch {
1738
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1739
+ }
1740
+ const subject = buildAuthSubject({
1741
+ selfAid: grant.selfAid,
1742
+ actorId: grant.actorId,
1743
+ channel: grant.channel,
1744
+ channelType: grant.channelType,
1745
+ channelId: conversationId,
1746
+ chatType: grant.chatType,
1747
+ conversationId,
1748
+ processOwners: loadDaemonConfig().owners ?? [],
1749
+ });
1750
+ const managementRole = isManagementRole(subject.role);
1751
+ if (!managementRole && (grant.channelType !== 'aun' || !isManagedGroupRelationRead(parsed.command))) {
1752
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `Role ${subject.role} cannot manage Agent group membership or settings` };
1753
+ }
1754
+ if ((parsed.command.kind === 'dissolve' || parsed.command.kind === 'owner') && subject.role !== 'owner') {
1755
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${parsed.command.kind} requires Agent owner permission` };
1756
+ }
1757
+ const decision = authorizeOperation({
1758
+ source: 'agent-tool',
1759
+ subject,
1760
+ intent: {
1761
+ operation: parsed.command.operationId,
1762
+ scope: managementRole ? 'agent' : 'relation',
1763
+ source: 'agent-tool',
1764
+ args: {
1765
+ self: parsed.command.from,
1766
+ ...(parsed.command.groupId ? {
1767
+ targetId: parsed.command.groupId,
1768
+ peer: parsed.command.groupId,
1769
+ peerKey: formatPeerKey(grant.channelType, parsed.command.groupId),
1770
+ } : {}),
1771
+ },
1772
+ },
1773
+ auditMetadata: {
1774
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1775
+ taskId: grant.taskId,
1776
+ messageId: grant.messageId,
1777
+ },
1778
+ });
1779
+ if (!decision.allow)
1780
+ return { ok: false, code: decision.code, error: decision.reason };
1781
+ let rulesContent;
1782
+ if (parsed.command.kind === 'rules.set') {
1783
+ const filePath = String(parsed.command.filePath || '');
1784
+ const projectPath = path.resolve(session.projectPath);
1785
+ const resolvedPath = path.resolve(projectPath, filePath);
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' };
1790
+ }
1791
+ if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
1792
+ return { ok: false, code: 'PROTECTED_RULES_PATH', error: 'Rules file cannot be read from a protected EvolCore path' };
1793
+ }
1794
+ let rulesFd;
1795
+ try {
1796
+ const realProjectPath = fs.realpathSync(projectPath);
1797
+ const realPath = fs.realpathSync(resolvedPath);
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' };
1808
+ }
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);
1814
+ if (!stat.isFile() || stat.size < 1 || stat.size > 4 * 1024) {
1815
+ return { ok: false, code: 'INVALID_RULES_CONTENT', error: 'Rules file must be a regular file from 1 to 4096 bytes' };
1816
+ }
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));
1821
+ }
1822
+ catch (error) {
1823
+ return {
1824
+ ok: false,
1825
+ code: 'INVALID_RULES_CONTENT',
1826
+ error: error instanceof Error ? error.message : String(error),
1827
+ };
1828
+ }
1829
+ finally {
1830
+ if (rulesFd !== undefined)
1831
+ fs.closeSync(rulesFd);
1832
+ }
1833
+ }
1834
+ try {
1835
+ return { ok: true, result: await executeManagedGroupOperation(parsed.command, rulesContent) };
1836
+ }
1837
+ catch (error) {
1838
+ return {
1839
+ ok: false,
1840
+ code: error instanceof ManagedGroupOperationError ? error.code : 'GROUP_OPERATION_FAILED',
1841
+ error: error instanceof Error ? error.message : String(error),
1842
+ };
1843
+ }
1844
+ }
1845
+ /** Agent-managed private msg read entrypoint. AUN credentials and history roots remain daemon-owned. */
1846
+ async handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1847
+ if (!this.agentDelegationRegistry) {
1848
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1849
+ }
1850
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1851
+ if (!delegation.ok)
1852
+ return { ok: false, code: delegation.code, error: delegation.reason };
1853
+ const session = await this.sessionManager.getSessionById(sessionId);
1854
+ if (!session)
1855
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1856
+ const parsed = resolveManagedMsgOperation(argv);
1857
+ if (!parsed.ok)
1858
+ return { ok: false, code: parsed.code, error: parsed.reason };
1859
+ const grant = delegation.grant;
1860
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
1861
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1862
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1863
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1864
+ }
1865
+ if (grant.channelType !== 'aun' || grant.chatType !== 'private') {
1866
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed msg reads require the current AUN private relation' };
1867
+ }
1868
+ let peerAid;
1869
+ try {
1870
+ peerAid = parsePeerKey(grant.peerKey).channelId.replace(/^@/, '');
1871
+ }
1872
+ catch {
1873
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1874
+ }
1875
+ if (parsed.command.from !== grantSelfAid || parsed.command.target !== peerAid) {
1876
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
1877
+ }
1878
+ const subject = buildAuthSubject({
1879
+ selfAid: grant.selfAid,
1880
+ actorId: grant.actorId,
1881
+ channel: grant.channel,
1882
+ channelType: grant.channelType,
1883
+ channelId: peerAid,
1884
+ chatType: grant.chatType,
1885
+ conversationId: peerAid,
1886
+ processOwners: loadDaemonConfig().owners ?? [],
1887
+ });
1888
+ const decision = authorizeOperation({
1889
+ source: 'agent-tool',
1890
+ subject,
1891
+ intent: {
1892
+ operation: parsed.command.operationId,
1893
+ scope: 'relation',
1894
+ source: 'agent-tool',
1895
+ args: {
1896
+ self: parsed.command.from,
1897
+ targetId: parsed.command.target,
1898
+ peer: parsed.command.target,
1899
+ peerKey: formatPeerKey('aun', parsed.command.target),
1900
+ },
1901
+ },
1902
+ auditMetadata: {
1903
+ argvHash: hashArgv(parsed.command.canonicalArgv),
1904
+ taskId: grant.taskId,
1905
+ messageId: grant.messageId,
1906
+ },
1907
+ });
1908
+ if (!decision.allow)
1909
+ return { ok: false, code: decision.code, error: decision.reason };
1910
+ try {
1911
+ return { ok: true, result: await executeManagedMsgOperation(parsed.command) };
1912
+ }
1913
+ catch (error) {
1914
+ return {
1915
+ ok: false,
1916
+ code: error instanceof ManagedMsgOperationError ? error.code : 'MSG_OPERATION_FAILED',
1917
+ error: error instanceof Error ? error.message : String(error),
1918
+ };
1919
+ }
1920
+ }
1921
+ /** Agent-managed current-group filesystem entrypoint. */
1922
+ async handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash) {
1923
+ if (!this.agentDelegationRegistry) {
1924
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1925
+ }
1926
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1927
+ if (!delegation.ok)
1928
+ return { ok: false, code: delegation.code, error: delegation.reason };
1929
+ const session = await this.sessionManager.getSessionById(sessionId);
1930
+ if (!session)
1931
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1932
+ const parsed = resolveManagedFsOperation(argv);
1933
+ if (!parsed.ok)
1934
+ return { ok: false, code: parsed.code, error: parsed.reason };
1935
+ const grant = delegation.grant;
1936
+ const actorAid = grant.selfAid.replace(/^@/, '');
1937
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
1938
+ if (sessionSelfAid && sessionSelfAid !== actorAid) {
1939
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1940
+ }
1941
+ if (grant.channelType !== 'aun' || grant.chatType !== 'group') {
1942
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed fs operations require the current AUN group relation' };
1943
+ }
1944
+ let currentGroup;
1945
+ try {
1946
+ currentGroup = parsePeerKey(grant.peerKey).channelId;
1947
+ }
1948
+ catch {
1949
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1950
+ }
1951
+ if ((parsed.command.actorAid && parsed.command.actorAid !== actorAid) || parsed.command.groupId !== currentGroup) {
1952
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN group can be targeted' };
1953
+ }
1954
+ parsed.command.actorAid = actorAid;
1955
+ const subject = buildAuthSubject({
1956
+ selfAid: grant.selfAid,
1957
+ actorId: grant.actorId,
1958
+ channel: grant.channel,
1959
+ channelType: grant.channelType,
1960
+ channelId: currentGroup,
1961
+ chatType: grant.chatType,
1962
+ conversationId: currentGroup,
1963
+ processOwners: loadDaemonConfig().owners ?? [],
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
+ };
1978
+ const decision = authorizeOperation({
1979
+ source: 'agent-tool',
1980
+ subject,
1981
+ intent: {
1982
+ operation: parsed.command.operationId,
1983
+ scope: 'relation',
1984
+ source: 'agent-tool',
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',
1990
+ },
1991
+ });
1992
+ if (!decision.allow)
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
+ }
2039
+ try {
2040
+ return { ok: true, result: await executeManagedFsOperation(parsed.command) };
2041
+ }
2042
+ catch (error) {
2043
+ return {
2044
+ ok: false,
2045
+ code: error instanceof ManagedFsOperationError ? error.code : 'FS_OPERATION_FAILED',
2046
+ error: error instanceof Error ? error.message : String(error),
2047
+ };
2048
+ }
2049
+ }
1572
2050
  setAgentDelegationRegistry(registry) {
1573
2051
  this.agentDelegationRegistry = registry;
1574
2052
  }
@@ -1576,7 +2054,7 @@ export class CommandHandler {
1576
2054
  * Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
1577
2055
  * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
1578
2056
  */
1579
- async handleCtl(cmd, sessionId) {
2057
+ async handleCtl(cmd, sessionId, grant) {
1580
2058
  logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
1581
2059
  // 1. 白名单检查
1582
2060
  const inputCmd = cmd.split(' ')[0];
@@ -1588,9 +2066,56 @@ export class CommandHandler {
1588
2066
  if (!session) {
1589
2067
  return { ok: false, error: '无效的 session' };
1590
2068
  }
2069
+ if (!grant || grant.sessionId !== sessionId) {
2070
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'An active task delegation grant is required' };
2071
+ }
2072
+ const sessionSelfAid = session.selfAID?.replace(/^@/, '');
2073
+ const grantSelfAid = grant.selfAid.replace(/^@/, '');
2074
+ if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
2075
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
2076
+ }
2077
+ let conversationId;
2078
+ try {
2079
+ conversationId = parsePeerKey(grant.peerKey).channelId;
2080
+ }
2081
+ catch {
2082
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
2083
+ }
2084
+ const ctlSubject = buildAuthSubject({
2085
+ selfAid: grant.selfAid,
2086
+ actorId: grant.actorId,
2087
+ channel: grant.channel,
2088
+ channelType: grant.channelType,
2089
+ channelId: conversationId,
2090
+ chatType: grant.chatType,
2091
+ conversationId,
2092
+ processOwners: loadDaemonConfig().owners ?? [],
2093
+ });
2094
+ const ctlIntent = this.resolveCtlIntent(cmd, grantSelfAid, grant.peerKey);
2095
+ if (!ctlIntent)
2096
+ return { ok: false, code: 'NOT_ALLOWED', error: `不允许的指令: ${inputCmd}` };
2097
+ const ctlDecision = authorizeOperation({
2098
+ source: 'agent-tool',
2099
+ subject: ctlSubject,
2100
+ intent: ctlIntent,
2101
+ auditMetadata: {
2102
+ argvHash: hashArgv(['ctl', ...cmd.slice(1).split(/\s+/).filter(Boolean)]),
2103
+ taskId: grant.taskId,
2104
+ messageId: grant.messageId,
2105
+ },
2106
+ });
2107
+ if (!ctlDecision.allow)
2108
+ return { ok: false, code: ctlDecision.code, error: ctlDecision.reason };
2109
+ if (cmd.startsWith('/model info ')) {
2110
+ const modelId = cmd.slice('/model info '.length).trim();
2111
+ if (!modelId || /\s/.test(modelId))
2112
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'model info requires one model id' };
2113
+ const info = await getModelInfo(modelId, grantSelfAid, session.baseagent);
2114
+ return { ok: true, result: JSON.stringify(info, null, 2), data: { ok: true, ...info } };
2115
+ }
1591
2116
  // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
1592
2117
  const userId = session.metadata?.peerId;
1593
- const ctlIdentity = this.resolveCtlIdentity(session, userId);
2118
+ const ctlIdentity = ctlSubject.identity;
1594
2119
  // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
1595
2120
  if (cmd.startsWith('/send ') || cmd === '/send') {
1596
2121
  // 解析 --encrypt 标志和消息文本
@@ -1625,9 +2150,9 @@ export class CommandHandler {
1625
2150
  const parts = sendArgs.split(/\s+/);
1626
2151
  const filePath = parts[parts.length - 1];
1627
2152
  if (filePath) {
1628
- const resolved = path.resolve(session.projectPath, filePath).replace(/\\/g, '/');
1629
- const projectPath = session.projectPath.replace(/\\/g, '/');
1630
- if (!resolved.startsWith(projectPath)) {
2153
+ const resolved = path.resolve(session.projectPath, filePath);
2154
+ const relative = path.relative(path.resolve(session.projectPath), resolved);
2155
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
1631
2156
  return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
1632
2157
  }
1633
2158
  }
@@ -1640,7 +2165,7 @@ export class CommandHandler {
1640
2165
  // 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
1641
2166
  try {
1642
2167
  const result = await this._handleInternal(cmd, session.channel, session.channelId, undefined, // 不发送消息
1643
- userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity);
2168
+ userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity, ctlSubject);
1644
2169
  const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
1645
2170
  if (result && typeof result === 'object' && 'kind' in result && result.kind === 'command.error') {
1646
2171
  return { ok: false, error: text || '执行失败' };
@@ -1651,6 +2176,56 @@ export class CommandHandler {
1651
2176
  return { ok: false, error: err.message };
1652
2177
  }
1653
2178
  }
2179
+ resolveCtlIntent(cmd, selfAid, peerKey) {
2180
+ const [name = '', ...args] = cmd.trim().slice(1).trim().split(/\s+/);
2181
+ const relationArgs = { self: selfAid, peer: peerKey, peerKey };
2182
+ const intent = (operation, extra = {}) => ({
2183
+ operation,
2184
+ scope: 'relation',
2185
+ source: 'agent-tool',
2186
+ args: { ...relationArgs, ...extra },
2187
+ });
2188
+ if (name === 'send')
2189
+ return intent('ec.ctl.send');
2190
+ if (name === 'file')
2191
+ return intent('ec.ctl.file');
2192
+ if (name === 'status')
2193
+ return intent('session.current');
2194
+ if (name === 'pwd')
2195
+ return intent('project.current');
2196
+ if (name === 'name' || name === 'rename')
2197
+ return intent('session.rename');
2198
+ if (name === 'queue') {
2199
+ const action = args.includes('--clear') ? 'clear'
2200
+ : args.includes('--cancel') ? 'cancel'
2201
+ : args.includes('--interrupt') ? 'interrupt'
2202
+ : 'read';
2203
+ return intent(`ec.ctl.queue.${action}`);
2204
+ }
2205
+ if (name === 'model') {
2206
+ if (args.length === 0 || args[0] === 'list')
2207
+ return intent('model.list');
2208
+ if (args[0] === 'current')
2209
+ return intent('model.current');
2210
+ if (args[0] === 'info')
2211
+ return intent('model.info', { model: args[1] });
2212
+ if (args[0] === 'reset')
2213
+ return intent('model.reset');
2214
+ if (args[0] === 'check') {
2215
+ return { operation: 'model.check', scope: 'agent', source: 'agent-tool', args: { self: selfAid } };
2216
+ }
2217
+ return intent('model.use', { model: args[0] });
2218
+ }
2219
+ if (name === 'effort') {
2220
+ return args.length === 0
2221
+ ? intent('model.effort.current')
2222
+ : intent('model.effort', { effort: args[0] });
2223
+ }
2224
+ if (name === 'restart') {
2225
+ return { operation: 'system.restart', scope: 'process', source: 'agent-tool', args: { self: selfAid } };
2226
+ }
2227
+ return null;
2228
+ }
1654
2229
  // ── Queue command ──
1655
2230
  /** 提取命名参数值(如 --cancel msg123 → "msg123") */
1656
2231
  extractArg(args, flag) {
@@ -1665,6 +2240,14 @@ export class CommandHandler {
1665
2240
  const showId = args.includes('--showid');
1666
2241
  const formatJson = args.includes('--format json');
1667
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' };
1668
2251
  // 操作分支
1669
2252
  if (args.includes('--clear')) {
1670
2253
  const count = this.messageQueue.clearBySession(sessionId);
@@ -1686,13 +2269,19 @@ export class CommandHandler {
1686
2269
  : { ok: false, error: `❌ 当前无处理中任务` };
1687
2270
  }
1688
2271
  // 查询
1689
- const items = this.messageQueue.getQueueItemsBySession(sessionId);
2272
+ const items = this.messageQueue.getQueueItemsBySession(sessionId, { after, before });
1690
2273
  if (formatJson) {
1691
2274
  return { ok: true, result: JSON.stringify({ items }, null, 2) };
1692
2275
  }
1693
2276
  return { ok: true, result: renderQueueItemsCtl(items, showId, full) };
1694
2277
  }
1695
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
+ }
1696
2285
  /**
1697
2286
  * ctl 专用渲染:不显示 session 标识列(因为只有一个 session)
1698
2287
  */
@@ -1715,14 +2304,33 @@ function renderQueueItemsCtl(items, showId, full) {
1715
2304
  parts.push(` ${name} `);
1716
2305
  }
1717
2306
  const content = full ? item.preview.replace('...', '') : item.preview;
1718
- 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}"`);
1719
2313
  lines.push(parts.join(''));
1720
2314
  }
1721
2315
  return lines.join('\n');
1722
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
+ }
1723
2321
  function sameFeedbackTarget(target, origin) {
1724
2322
  if (!origin)
1725
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;
1726
2334
  return target.channelKey === origin.channelKey
1727
2335
  && target.channelId === origin.channelId
1728
2336
  && target.session === origin.session