evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -14,9 +14,47 @@ export function auditCommandAuthorization(event) {
14
14
  const auditRecord = buildAuditRecord(event);
15
15
  logAuditEvent(auditRecord);
16
16
  }
17
+ /**
18
+ * Record a tool-level preflight denial (e.g. protected-path block) in the
19
+ * structured command-audit log. This covers interactive sessions whose
20
+ * denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
21
+ */
22
+ export function auditToolPreflightDenial(input) {
23
+ auditCommandAuthorization({
24
+ ts: Date.now(),
25
+ source: 'agent-tool',
26
+ operation: 'tool.preflight.deny',
27
+ scope: 'filesystem',
28
+ dangerous: false,
29
+ decision: 'deny',
30
+ decisionSource: 'policy',
31
+ toolName: input.toolName,
32
+ policyCode: input.policyCode,
33
+ reason: input.reason,
34
+ argsSummary: input.summary ? { summary: input.summary } : undefined,
35
+ sessionId: input.sessionId,
36
+ agentAid: input.agentAid,
37
+ permissionMode: input.permissionMode,
38
+ channel: input.channel,
39
+ actorId: input.actorId,
40
+ role: input.role ?? 'unknown',
41
+ selfAid: input.selfAid,
42
+ peerKey: input.peerKey,
43
+ requestId: input.requestId,
44
+ taskId: input.taskId,
45
+ });
46
+ }
17
47
  function buildAuditRecord(event) {
18
48
  return {
19
49
  ts: event.ts,
50
+ correlationId: event.correlationId ?? event.requestId,
51
+ requestId: event.requestId,
52
+ sessionId: event.sessionId,
53
+ agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
54
+ permissionMode: event.permissionMode,
55
+ toolName: event.toolName,
56
+ policyCode: event.policyCode ?? event.code,
57
+ decisionSource: event.decisionSource,
20
58
  source: event.source,
21
59
  operation: event.operation,
22
60
  scope: event.scope,
@@ -71,6 +109,14 @@ function logAuditEvent(record) {
71
109
  `actor=${record.actorId || 'unknown'}`,
72
110
  record.taskId ? `task=${record.taskId}` : null,
73
111
  record.messageId ? `message=${record.messageId}` : null,
112
+ record.requestId ? `request=${record.requestId}` : null,
113
+ record.correlationId ? `correlation=${record.correlationId}` : null,
114
+ record.sessionId ? `session=${record.sessionId}` : null,
115
+ record.agentAid ? `agent=${record.agentAid}` : null,
116
+ record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
117
+ record.toolName ? `tool=${record.toolName}` : null,
118
+ record.policyCode ? `policy=${record.policyCode}` : null,
119
+ record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
74
120
  record.code ? `code=${record.code}` : null,
75
121
  record.matchedRule ? `rule=${record.matchedRule}` : null,
76
122
  record.dangerous ? 'dangerous=true' : null,
@@ -78,6 +124,7 @@ function logAuditEvent(record) {
78
124
  .filter(Boolean)
79
125
  .join(' ');
80
126
  logger[level](message);
127
+ logger[level](`[CommandAuditRecord] ${JSON.stringify(record)}`);
81
128
  if (record.reason)
82
129
  logger[level](` reason: ${record.reason}`);
83
130
  if (record.argvHash)
@@ -170,6 +170,7 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
170
170
  dangerous: opMeta.dangerous,
171
171
  matchedRule,
172
172
  constraints: permission.constraints,
173
+ permissionMode: permission.permissionMode,
173
174
  };
174
175
  }
175
176
  function matchCommandPermission(operation, category, commandPerms, isDangerous) {
@@ -65,7 +65,7 @@ const MSG_READ_OPERATIONS = [
65
65
  description,
66
66
  sources: ['agent-tool'],
67
67
  }));
68
- const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].map(kind => ({
68
+ const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'getfacl'].map(kind => ({
69
69
  id: `ec.fs.${kind}`,
70
70
  category: 'read',
71
71
  dangerous: false,
@@ -73,6 +73,14 @@ const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].ma
73
73
  description: `Read current group filesystem (${kind})`,
74
74
  sources: ['agent-tool'],
75
75
  }));
76
+ const GROUP_FS_WRITE_OPERATIONS = [['mkdir', 'write-own'], ['upload', 'write-own'], ['copy', 'write-own'], ['move', 'write-own'], ['remove', 'write-own'], ['setfacl', 'write-agent']].map(([kind, category]) => ({
77
+ id: `ec.fs.${kind}`,
78
+ category,
79
+ dangerous: false,
80
+ defaultScopes: ['relation'],
81
+ description: `Write current group filesystem (${kind})`,
82
+ sources: ['agent-tool'],
83
+ }));
76
84
  const HANDOFF_READ_OPERATIONS = ['status', 'list', 'trace'].map(kind => ({
77
85
  id: `ec.handoff.${kind}`,
78
86
  category: 'read',
@@ -856,6 +864,7 @@ const OPERATIONS = [
856
864
  ...GROUP_MANAGEMENT_OPERATIONS,
857
865
  ...MSG_READ_OPERATIONS,
858
866
  ...GROUP_FS_READ_OPERATIONS,
867
+ ...GROUP_FS_WRITE_OPERATIONS,
859
868
  ...HANDOFF_READ_OPERATIONS,
860
869
  ...HANDOFF_WRITE_OPERATIONS,
861
870
  ...QUEUE_OPERATIONS,
@@ -31,14 +31,21 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName) {
31
31
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
32
32
  if (hasMessageLogOperation(chatDir, operationId))
33
33
  return;
34
+ const existing = outbox.findByDedupeKey(aid, operationId);
35
+ if (existing?.delivery?.chatType === 'private' && existing.channelId === owner)
36
+ return;
37
+ if (existing)
38
+ outbox.remove(aid, existing.id);
34
39
  outbox.enqueue(aid, {
35
40
  channelId: owner,
41
+ delivery: { chatType: 'private' },
36
42
  dedupeKey: operationId,
37
43
  critical: true,
38
44
  type: 'text',
39
45
  text: renderPostBootstrapWelcome(aid, owner, ownerName),
40
46
  ttl: BOOTSTRAP_MESSAGE_TTL_MS,
41
47
  context: {
48
+ delivery: { chatType: 'private' },
42
49
  metadata: {
43
50
  source: 'daemon',
44
51
  chatmode: 'interactive',
@@ -49,12 +56,12 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName) {
49
56
  });
50
57
  }
51
58
  export function hasPendingPostBootstrapWelcomeOutbox(aid) {
52
- return !!outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
59
+ return outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid))?.delivery?.chatType === 'private';
53
60
  }
54
61
  /** Bind the durable completion welcome to the fresh post-bootstrap session. */
55
62
  export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
56
63
  const entry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
57
- if (!entry)
64
+ if (!entry || entry.delivery?.chatType !== 'private')
58
65
  return false;
59
66
  if (entry.context?.sessionId === sessionId)
60
67
  return true;
@@ -140,7 +140,13 @@ export class BootstrapService {
140
140
  this.inFlight.delete(key);
141
141
  return false;
142
142
  }
143
- const channelId = ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
143
+ // The bootstrap prompt is deliberately a private message to the configured
144
+ // owner. An inbound owner message may have arrived from a group, in which
145
+ // case ctx.channelId is the group AID and must not be paired with the
146
+ // private delivery route below (that would call message.send(to=group)).
147
+ const channelId = channelType === 'aun'
148
+ ? recipientId
149
+ : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
144
150
  if (!channelId) {
145
151
  this.inFlight.delete(key);
146
152
  return false;
@@ -166,6 +172,7 @@ export class BootstrapService {
166
172
  channelId,
167
173
  agentName: aid,
168
174
  replyContext: {
175
+ delivery: { chatType: 'private' },
169
176
  metadata: {
170
177
  source: 'daemon',
171
178
  persistRequired: true,
@@ -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,22 @@ 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
+ ...(parsed.command.aclGrantee ? { aclGrantee: parsed.command.aclGrantee } : {}),
1978
+ ...(parsed.command.aclPerms ? { aclPerms: parsed.command.aclPerms } : {}),
1979
+ ...(parsed.command.aclRemove !== undefined ? { aclRemove: parsed.command.aclRemove } : {}),
1980
+ };
1907
1981
  const decision = authorizeOperation({
1908
1982
  source: 'agent-tool',
1909
1983
  subject,
@@ -1911,17 +1985,60 @@ export class CommandHandler {
1911
1985
  operation: parsed.command.operationId,
1912
1986
  scope: 'relation',
1913
1987
  source: 'agent-tool',
1914
- args: {
1915
- self: actorAid,
1916
- targetId: currentGroup,
1917
- peer: currentGroup,
1918
- peerKey: formatPeerKey('aun', currentGroup),
1919
- },
1988
+ args: intentArgs,
1989
+ },
1990
+ auditMetadata: {
1991
+ argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId,
1992
+ sessionId, agentAid: actorAid, toolName: 'ec.fs',
1920
1993
  },
1921
- auditMetadata: { argvHash: hashArgv(parsed.command.canonicalArgv), taskId: grant.taskId, messageId: grant.messageId },
1922
1994
  });
1923
1995
  if (!decision.allow)
1924
1996
  return { ok: false, code: decision.code, error: decision.reason };
1997
+ if (parsed.command.kind === 'upload') {
1998
+ const localPath = String(parsed.command.localPath || '').trim();
1999
+ const projectPath = path.resolve(session.projectPath);
2000
+ if (!localPath)
2001
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'Managed upload source is required' };
2002
+ const resolvedPath = path.resolve(projectPath, localPath);
2003
+ const managedTempPath = this.getManagedSessionTempDir(session);
2004
+ const allowedRoots = [projectPath, managedTempPath].filter((value) => !!value);
2005
+ if (!allowedRoots.some(root => isSameOrDescendant(resolvedPath, root))) {
2006
+ return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source must be inside the current session project or managed TMPDIR' };
2007
+ }
2008
+ if (isHClassPath(resolvedPath) || isLClassPath(resolvedPath)) {
2009
+ return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source cannot be a protected EvolCore path' };
2010
+ }
2011
+ try {
2012
+ const realProjectPath = fs.realpathSync(projectPath);
2013
+ const stat = fs.lstatSync(resolvedPath);
2014
+ if (stat.isSymbolicLink() || !stat.isFile()) {
2015
+ return { ok: false, code: 'INVALID_LOCAL_FILE', error: 'Upload source must be a regular non-symlink file' };
2016
+ }
2017
+ const realPath = fs.realpathSync(resolvedPath);
2018
+ let realManagedTempPath;
2019
+ try {
2020
+ realManagedTempPath = managedTempPath ? fs.realpathSync(managedTempPath) : undefined;
2021
+ }
2022
+ catch {
2023
+ // A removed or invalid session temp root is not an upload root.
2024
+ }
2025
+ const realAllowedRoots = [realProjectPath, realManagedTempPath].filter((value) => !!value);
2026
+ if (!realAllowedRoots.some(root => isSameOrDescendant(realPath, root))
2027
+ || isHClassPath(realPath) || isLClassPath(realPath)) {
2028
+ return { ok: false, code: 'PROTECTED_LOCAL_PATH', error: 'Upload source resolves outside the allowed project or managed TMPDIR path' };
2029
+ }
2030
+ if (realManagedTempPath && isSameOrDescendant(realPath, realManagedTempPath) && stat.nlink > 1) {
2031
+ return { ok: false, code: 'INVALID_LOCAL_PATH', error: 'Upload source cannot be a hard link inside managed TMPDIR' };
2032
+ }
2033
+ if (stat.size > 16 * 1024 * 1024) {
2034
+ return { ok: false, code: 'LOCAL_FILE_TOO_LARGE', error: 'Managed upload is limited to 16 MiB' };
2035
+ }
2036
+ parsed.command.localPath = realPath;
2037
+ }
2038
+ catch (error) {
2039
+ return { ok: false, code: 'INVALID_LOCAL_FILE', error: error instanceof Error ? error.message : String(error) };
2040
+ }
2041
+ }
1925
2042
  try {
1926
2043
  return { ok: true, result: await executeManagedFsOperation(parsed.command) };
1927
2044
  }
@@ -2126,6 +2243,14 @@ export class CommandHandler {
2126
2243
  const showId = args.includes('--showid');
2127
2244
  const formatJson = args.includes('--format json');
2128
2245
  const full = args.includes('--full');
2246
+ const after = parseQueueTimeArg(this.extractArg(args, '--after'));
2247
+ const before = parseQueueTimeArg(this.extractArg(args, '--before'));
2248
+ if (this.extractArg(args, '--after') && after === undefined)
2249
+ return { ok: false, error: '❌ --after 必须是 epoch 毫秒或 ISO 时间' };
2250
+ if (this.extractArg(args, '--before') && before === undefined)
2251
+ return { ok: false, error: '❌ --before 必须是 epoch 毫秒或 ISO 时间' };
2252
+ if (after !== undefined && before !== undefined && after >= before)
2253
+ return { ok: false, error: '❌ --after 必须早于 --before' };
2129
2254
  // 操作分支
2130
2255
  if (args.includes('--clear')) {
2131
2256
  const count = this.messageQueue.clearBySession(sessionId);
@@ -2147,13 +2272,19 @@ export class CommandHandler {
2147
2272
  : { ok: false, error: `❌ 当前无处理中任务` };
2148
2273
  }
2149
2274
  // 查询
2150
- const items = this.messageQueue.getQueueItemsBySession(sessionId);
2275
+ const items = this.messageQueue.getQueueItemsBySession(sessionId, { after, before });
2151
2276
  if (formatJson) {
2152
2277
  return { ok: true, result: JSON.stringify({ items }, null, 2) };
2153
2278
  }
2154
2279
  return { ok: true, result: renderQueueItemsCtl(items, showId, full) };
2155
2280
  }
2156
2281
  }
2282
+ function parseQueueTimeArg(value) {
2283
+ if (!value)
2284
+ return undefined;
2285
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
2286
+ return Number.isFinite(parsed) ? parsed : undefined;
2287
+ }
2157
2288
  /**
2158
2289
  * ctl 专用渲染:不显示 session 标识列(因为只有一个 session)
2159
2290
  */
@@ -2176,14 +2307,33 @@ function renderQueueItemsCtl(items, showId, full) {
2176
2307
  parts.push(` ${name} `);
2177
2308
  }
2178
2309
  const content = full ? item.preview.replace('...', '') : item.preview;
2179
- parts.push(`"${content}"`);
2310
+ const timing = [
2311
+ item.receivedAt != null ? `recv=${new Date(item.receivedAt).toISOString()}` : undefined,
2312
+ item.enqueuedAt != null ? `enq=${new Date(item.enqueuedAt).toISOString()}` : undefined,
2313
+ item.queueWaitMs != null ? `wait=${formatQueueDuration(item.queueWaitMs)}` : undefined,
2314
+ ].filter(Boolean).join(' ');
2315
+ parts.push(`${timing ? `${timing} ` : ''}"${content}"`);
2180
2316
  lines.push(parts.join(''));
2181
2317
  }
2182
2318
  return lines.join('\n');
2183
2319
  }
2320
+ function formatQueueDuration(ms) {
2321
+ const sec = Math.max(0, Math.round(ms / 1000));
2322
+ return sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
2323
+ }
2184
2324
  function sameFeedbackTarget(target, origin) {
2185
2325
  if (!origin)
2186
2326
  return false;
2327
+ const targetChannelType = tryParseChannelKey(target.channelKey)?.type
2328
+ ?? target.channelKey.split('#', 1)[0];
2329
+ const targetChatType = target.chatType
2330
+ ?? (targetChannelType === 'aun' ? undefined : 'private');
2331
+ if (!targetChatType)
2332
+ return false;
2333
+ const originDelivery = origin.delivery;
2334
+ const originChatType = originDelivery?.chatType ?? 'private';
2335
+ if (targetChatType !== originChatType)
2336
+ return false;
2187
2337
  return target.channelKey === origin.channelKey
2188
2338
  && target.channelId === origin.channelId
2189
2339
  && 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,