evolcore 0.0.31 → 0.0.32

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 (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/agents/codex-runner.js +28 -5
  3. package/dist/agents/ecagent-runner.js +4 -0
  4. package/dist/agents/gemini-runner.js +4 -0
  5. package/dist/aun/aid/store.js +0 -81
  6. package/dist/aun/group/fs-download.js +1 -10
  7. package/dist/aun/msg/managed-operation.js +130 -32
  8. package/dist/aun/service-proxy.js +12 -18
  9. package/dist/aun/storage/index.js +29 -0
  10. package/dist/channels/aun.js +10 -135
  11. package/dist/cli/agent-command.js +6 -7
  12. package/dist/cli/agent-create.js +43 -24
  13. package/dist/cli/agent.js +15 -69
  14. package/dist/cli/aun-commands.js +149 -5
  15. package/dist/cli/ctl-command.js +1 -2
  16. package/dist/cli/daemon-commands.js +1 -1
  17. package/dist/cli/diag.js +2 -2
  18. package/dist/cli/handoff-command.js +15 -5
  19. package/dist/cli/memory-command.js +85 -11
  20. package/dist/cli/model.js +14 -138
  21. package/dist/cli/queue-command.js +46 -0
  22. package/dist/cli/response.js +5 -54
  23. package/dist/cli/skill-command.js +9 -3
  24. package/dist/cli/stats.js +10 -4
  25. package/dist/cli/task-command.js +1 -0
  26. package/dist/cli/task-context.js +15 -0
  27. package/dist/cli/trigger-command.js +73 -49
  28. package/dist/config/builtin-roles.js +8 -7
  29. package/dist/core/agent-ipc-registration.js +65 -22
  30. package/dist/core/auth/agent-delegation.js +17 -1
  31. package/dist/core/auth/delegation-authorization.js +1 -0
  32. package/dist/core/auth/operation-catalog.js +11 -3
  33. package/dist/core/command/command-handler.js +164 -21
  34. package/dist/core/command/diag-service.js +5 -1
  35. package/dist/core/handoff/ipc-registration.js +38 -0
  36. package/dist/core/ipc-registration.js +71 -7
  37. package/dist/core/message/aun-ipc-sender.js +48 -4
  38. package/dist/core/message/message-queue.js +131 -3
  39. package/dist/core/message/response-engine.js +30 -2
  40. package/dist/core/permission/sandbox-runtime.js +14 -1
  41. package/dist/core/task/task-recovery-service.js +2 -0
  42. package/dist/core/task/task-resume-operation.js +11 -0
  43. package/dist/index.js +2 -2
  44. package/dist/ipc.js +146 -8
  45. package/dist/trigger/authorization.js +2 -1
  46. package/dist/trigger/ipc-service.js +49 -35
  47. package/dist/trigger/script-sandbox.js +58 -77
  48. package/kits/docs/AUTHORING.md +1 -1
  49. package/kits/docs/channels/aun.md +8 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/task.md +2 -0
  52. package/kits/docs/evolcore/trigger.md +2 -2
  53. package/kits/rules/01-overview.md +0 -9
  54. package/kits/rules/06-channel.md +7 -1
  55. package/kits/templates/system-fragments/commands.md +1 -1
  56. package/package.json +1 -1
@@ -21,6 +21,20 @@ export class MessageQueue {
21
21
  interruptBarriers = new Map();
22
22
  interruptFailures = new Map();
23
23
  activeTerminalReasons = new Map();
24
+ /**
25
+ * 允许「未提交输入」回队的 key(仅新消息到达触发的中断)。
26
+ * 与 activeTerminalReasons 分开:后者还承载 stop/timeout/priority 抢占等
27
+ * 语义,那些场景绝不能复活旧输入。
28
+ */
29
+ requeueableInterrupts = new Set();
30
+ /**
31
+ * queueKey → 触发本次 new_message 中断的消息。
32
+ *
33
+ * 回队前用它把「打断消息」从 pending 中排除:只有当队列里没有其它等待项时
34
+ * 才回队。否则重放旧输入会插到那些等待项(如被 defer 的群聊消息)前面,
35
+ * 打乱既有调度顺序。
36
+ */
37
+ interruptingMessages = new Map();
24
38
  eventBus;
25
39
  recentMessageIds = new Set();
26
40
  DEDUP_WINDOW = 60_000; // 1 分钟窗口
@@ -108,6 +122,19 @@ export class MessageQueue {
108
122
  }
109
123
  }
110
124
  });
125
+ subscribe.call(eventBus, 'input_accepted', event => {
126
+ if (!('inputId' in event) || !event.inputId)
127
+ return;
128
+ const inputEvent = event;
129
+ // inputId 格式为 sessionId 或 sessionId:xxx,取第一段匹配
130
+ const sessionId = inputEvent.inputId.split(':')[0];
131
+ for (const [queueKey, state] of this.activeStates) {
132
+ if (this.matchesSession(queueKey, sessionId)) {
133
+ state.inputSubmitted = true;
134
+ logger.debug(`[MessageQueue] input_accepted for ${queueKey}, taskId=${state.taskId}, generation=${state.generation}`);
135
+ }
136
+ }
137
+ });
111
138
  subscribe.call(eventBus, 'task:completed', event => {
112
139
  if ('sessionId' in event && event.sessionId)
113
140
  this.markActiveTerminal(event.sessionId, 'completed');
@@ -124,6 +151,37 @@ export class MessageQueue {
124
151
  this.activeTerminalReasons.set(queueKey, reason);
125
152
  }
126
153
  }
154
+ /**
155
+ * 显式允许/禁止该 key 的 active 批次在中断后回队。
156
+ *
157
+ * 只有「新消息到达触发的中断」才置 true:那种场景下旧输入尚未提交给模型,
158
+ * 用户也确实期望两条一起被看到。stop / 优先级抢占 / 超时等一律不允许复活。
159
+ */
160
+ markRequeueable(queueKey, requeueable) {
161
+ if (requeueable)
162
+ this.requeueableInterrupts.add(queueKey);
163
+ else
164
+ this.requeueableInterrupts.delete(queueKey);
165
+ }
166
+ /**
167
+ * 标记 active 批次的输入已真正提交给模型(runner 发出 input_accepted 后调用)。
168
+ *
169
+ * 这是 pre-submit 回队的关键判据:只有「未提交」的 active 才能在 new_message
170
+ * 中断后安全放回队列;已提交的批次若回队,会在 provider session 里产生重复用户轮次。
171
+ */
172
+ markInputSubmitted(sessionId, taskId) {
173
+ for (const [queueKey, state] of this.activeStates) {
174
+ if (!this.matchesSession(queueKey, sessionId))
175
+ continue;
176
+ // 同一 session 可能有多个 queueKey(不同 agent/project),taskId 用来精确定位。
177
+ if (taskId !== undefined && state.taskId !== undefined && state.taskId !== taskId)
178
+ continue;
179
+ state.inputSubmitted = true;
180
+ state.submittedMessageIds = Array.from(state.messageIds);
181
+ logger.debug(`[Queue] input submitted: key=${queueKey} task=${taskId ?? '<none>'} ids=${state.submittedMessageIds.join(',')}`);
182
+ return;
183
+ }
184
+ }
127
185
  /**
128
186
  * 注册一次性消息拦截器:下一条来自 sessionKey 的消息不入队、不触发 interrupt,
129
187
  * 直接传给 handler。用于 AskUserQuestion 的"手动输入"场景。
@@ -1082,7 +1140,12 @@ export class MessageQueue {
1082
1140
  activeState?.peerId === message.peerId &&
1083
1141
  !this.hasOtherPeerQueued(queueKey, message.peerId);
1084
1142
  if (interruptSingle || interruptGroupSamePeer) {
1085
- logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt (samePeer=${interruptGroupSamePeer})`);
1143
+ const inputSubmitted = activeState?.inputSubmitted ?? false;
1144
+ logger.info(`[Queue] new_message interrupt: key=${queueKey} samePeer=${interruptGroupSamePeer} taskId=${activeState?.taskId ?? '<none>'} generation=${activeState?.generation ?? '<none>'} inputSubmitted=${inputSubmitted} activeMsg=${Array.from(activeState?.messageIds ?? []).join(',')}`);
1145
+ // 新消息到达才允许 pre-submit 回队:旧输入尚未提交给模型,
1146
+ // 且用户期望它与刚到的消息一起被看到。
1147
+ this.markRequeueable(queueKey, true);
1148
+ this.interruptingMessages.set(queueKey, message);
1086
1149
  this.eventBus?.publish({
1087
1150
  type: 'task:interrupted',
1088
1151
  sessionId: sessionKey,
@@ -1218,7 +1281,7 @@ export class MessageQueue {
1218
1281
  this.activeBatches.set(queueKey, activeItem);
1219
1282
  this.processingStartTime.set(queueKey, Date.now());
1220
1283
  this.persistQueuesImmediate();
1221
- logger.info(`[Queue] processing batch: key=${queueKey} items=${rawItems.length} pending=${queue.length} rawPending=${this.queueRawMessageCount(queue)} batchRole=${merged.message.batchRole ?? '<mixed>'} peer=${activeState.peerId ?? '<multi>'} msg=${merged.message.messageId ?? '<none>'}`);
1284
+ logger.info(`[Queue] processing batch: key=${queueKey} items=${rawItems.length} pending=${queue.length} rawPending=${this.queueRawMessageCount(queue)} batchRole=${merged.message.batchRole ?? '<mixed>'} peer=${activeState.peerId ?? '<multi>'} msg=${merged.message.messageId ?? '<none>'} sourceIds=${Array.from(activeMessageIds).join(',')}`);
1222
1285
  try {
1223
1286
  await this.handler(merged.message);
1224
1287
  logger.debug(`[Queue] Message processed successfully`);
@@ -1230,10 +1293,17 @@ export class MessageQueue {
1230
1293
  }
1231
1294
  finally {
1232
1295
  if (!this.shuttingDown) {
1296
+ const terminalReason = this.activeTerminalReasons.get(queueKey);
1297
+ const disposition = this.disposeInterruptedBatch(queueKey, activeState, rawParts, merged.message.causation);
1298
+ if (disposition !== 'none') {
1299
+ logger.info(`[Queue] batch disposition: key=${queueKey} reason=${terminalReason ?? '<completed>'} disposition=${disposition}`);
1300
+ }
1233
1301
  this.activeBatches.delete(queueKey);
1234
1302
  this.processingStartTime.delete(queueKey);
1235
1303
  this.activeStates.delete(queueKey);
1236
1304
  this.activeTerminalReasons.delete(queueKey);
1305
+ this.requeueableInterrupts.delete(queueKey);
1306
+ this.interruptingMessages.delete(queueKey);
1237
1307
  this.persistQueuesImmediate();
1238
1308
  }
1239
1309
  }
@@ -1244,6 +1314,63 @@ export class MessageQueue {
1244
1314
  }
1245
1315
  }
1246
1316
  }
1317
+ /**
1318
+ * 决定被中断的 active 批次的下落。
1319
+ *
1320
+ * 只有同时满足两个条件才回队:
1321
+ * 1) 该 key 被显式标记为「新消息到达」(markRequeueable),而不是 stop/超时/优先级抢占;
1322
+ * 2) 该批次的输入尚未提交给模型(未收到 input_accepted / 未产出输出)。
1323
+ *
1324
+ * 已提交的批次若回队,会在 provider session 里产生重复用户轮次;
1325
+ * stop/timeout/recalled 是用户或系统的明确意图,绝不复活;
1326
+ * daemon_restart 由启动恢复流程处理,这里不重复入队。
1327
+ *
1328
+ * 回队到物理队首并保留原始 messageId/receivedAt,下一次 dequeueGreedy 会把它
1329
+ * 与期间到达的 pending 消息一起合并,等价于「未提交的旧输入 + 新输入」同批提交。
1330
+ */
1331
+ disposeInterruptedBatch(queueKey, activeState, rawParts, causation) {
1332
+ if (!this.requeueableInterrupts.has(queueKey))
1333
+ return 'none';
1334
+ if (!this.interruptCallback
1335
+ || this.activeTerminalReasons.get(queueKey) !== 'new_message')
1336
+ return 'none';
1337
+ if (rawParts.length === 0)
1338
+ return 'none';
1339
+ // 已提交:不能重放,交由 provider/session 侧的既有用户轮次保证上下文完整。
1340
+ if (activeState.inputSubmitted)
1341
+ return 'input_already_submitted';
1342
+ const queue = this.queues.get(queueKey);
1343
+ if (!queue) {
1344
+ logger.warn(`[Queue] pre-submit requeue skipped: queue gone key=${queueKey}`);
1345
+ return 'none';
1346
+ }
1347
+ const requeued = rawParts.map(part => this.itemFromPart(part));
1348
+ // 只有当队列里除打断消息外没有其它等待项时才回队。
1349
+ // 若已有其它 pending(例如被 defer 的群聊消息),说明旧输入本可在当前轮
1350
+ // 处理完毕,回队会插入一个与它们无关的额外轮次并打乱既有调度顺序;
1351
+ // 那种情况下保持原有中断行为。
1352
+ const interrupting = this.interruptingMessages.get(queueKey);
1353
+ const pendingWithoutInterrupt = interrupting
1354
+ ? queue.filter(item => item.message.messageId !== interrupting.messageId)
1355
+ : queue;
1356
+ if (pendingWithoutInterrupt.length > 0) {
1357
+ logger.info(`[Queue] pre-submit requeue skipped: key=${queueKey} other pending=${pendingWithoutInterrupt.length}`);
1358
+ return 'none';
1359
+ }
1360
+ queue.unshift(...requeued);
1361
+ // 按物理顺序重建逻辑队列,避免把旧输入追加到新消息之后,导致 FIFO
1362
+ // 在下一次 reorderPhysical 时颠倒回队消息与打断消息的顺序。
1363
+ // 与 enqueue 路径保持同一条件,避免把 owner-inject/interrupt 消息写进逻辑队列。
1364
+ this.logicalQueue.clear(queueKey);
1365
+ for (const item of queue) {
1366
+ if (item.message.source !== 'owner-inject' && item.message.attentionPolicy !== 'interrupt') {
1367
+ this.logicalQueue.enqueue(queueKey, item.message);
1368
+ }
1369
+ }
1370
+ logger.info(`[Queue] requeued pre-submit batch: key=${queueKey} ids=${requeued
1371
+ .flatMap(item => this.messageIdsFor(item)).join(',')} causation=${causation?.traceId ?? '<none>'}`);
1372
+ return 'requeued_pre_submit';
1373
+ }
1247
1374
  /**
1248
1375
  * 贪心弹出队首可合并消息。
1249
1376
  * 群聊入队携带 role,此时取出当前所有群聊 pending 并按时间线合并;
@@ -1337,7 +1464,8 @@ export class MessageQueue {
1337
1464
  && a.channelKey === b.channelKey
1338
1465
  && a.channelType === b.channelType
1339
1466
  && a.chatType === b.chatType
1340
- && a.peerKey === b.peerKey;
1467
+ && a.peerKey === b.peerKey
1468
+ && a.approvalInteractionPolicy === b.approvalInteractionPolicy;
1341
1469
  }
1342
1470
  firstTimestamp(item) {
1343
1471
  const parts = this.partsOf(item);
@@ -1893,6 +1893,20 @@ export class ResponseEngine {
1893
1893
  peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
1894
1894
  };
1895
1895
  }
1896
+ resolveTaskApprovalInteractionPolicy(message) {
1897
+ if (message.taskRecovery) {
1898
+ const policy = message.taskRecovery.approvalInteractionPolicy;
1899
+ if (policy === 'interactive' || policy === 'deny')
1900
+ return policy;
1901
+ return message.taskRecovery.automatic ? 'deny' : 'interactive';
1902
+ }
1903
+ if (message.source === 'handoff' && message.handoffDelivery?.direction === 'origin') {
1904
+ return message.handoffDelivery.originAuthorization?.approvalInteractionPolicy === 'interactive'
1905
+ ? 'interactive'
1906
+ : 'deny';
1907
+ }
1908
+ return message.source === 'trigger' ? 'deny' : 'interactive';
1909
+ }
1896
1910
  resolveHandoffOriginExecutionIdentity(message, selfAid) {
1897
1911
  const authorization = message.handoffDelivery?.originAuthorization;
1898
1912
  if (message.source !== 'handoff'
@@ -2701,6 +2715,7 @@ export class ResponseEngine {
2701
2715
  const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAid);
2702
2716
  const authorizationIdentity = triggerExecutionIdentity
2703
2717
  ?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
2718
+ const approvalInteractionPolicy = this.resolveTaskApprovalInteractionPolicy(message);
2704
2719
  const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
2705
2720
  const configChatType = authorizationIdentity
2706
2721
  ? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
@@ -3122,6 +3137,7 @@ export class ResponseEngine {
3122
3137
  : undefined;
3123
3138
  turnLease = await this.turnCoordinator.begin(session, taskId, inputId, configActorId ? {
3124
3139
  peerId: configActorId,
3140
+ approvalInteractionPolicy,
3125
3141
  encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
3126
3142
  baseagent: session.baseagent,
3127
3143
  agentSessionId: session.agentSessionId,
@@ -3439,7 +3455,7 @@ export class ResponseEngine {
3439
3455
  peerKey: authPeerKey,
3440
3456
  causation: taskCausation,
3441
3457
  approvalRouting,
3442
- approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
3458
+ approvalInteractionPolicy,
3443
3459
  permissionMode: effectivePermissionMode,
3444
3460
  recordExecutionAnomaly,
3445
3461
  pauseController: taskPauseController,
@@ -4253,6 +4269,7 @@ export class ResponseEngine {
4253
4269
  const taskRuntimeContext = {
4254
4270
  taskId,
4255
4271
  sessionId: session.id,
4272
+ projectPath: path.resolve(session.projectPath),
4256
4273
  messageId: message.messageId,
4257
4274
  baseagent: normalizedBaseagent.canonical,
4258
4275
  channel: configChannelType,
@@ -4300,7 +4317,9 @@ export class ResponseEngine {
4300
4317
  selfAid,
4301
4318
  peerKey,
4302
4319
  issuedRole: peerRole,
4320
+ executionSource: fullAccessAuthorization?.source ?? (message.source === 'trigger' ? 'trigger' : 'user'),
4303
4321
  permissionMode: effectivePermissionMode,
4322
+ approvalInteractionPolicy,
4304
4323
  ...(sessionRuntimeDir ? { sessionRuntimeDir } : {}),
4305
4324
  ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
4306
4325
  });
@@ -5932,14 +5951,23 @@ export class ResponseEngine {
5932
5951
  if (event.type === 'input_accepted') {
5933
5952
  inputAccepted = !turnLease || event.inputId === turnLease.inputId;
5934
5953
  logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
5954
+ // 通知队列:本批次输入已真正提交给模型,此后不可回队重放。
5955
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
5935
5956
  continue;
5936
5957
  }
5937
5958
  if (event.type === 'text') {
5938
- if (event.text.trim())
5959
+ const hasText = !!event.text.trim();
5960
+ // 模型已产出文本 ⇒ 输入必然已被消费。覆盖不发 input_accepted 的 runner
5961
+ // (codex/ecagent/gemini),避免它们在 pre-submit 回队时重复用户轮次。
5962
+ if (hasText)
5963
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
5964
+ if (hasText)
5939
5965
  textCount++;
5940
5966
  assistantUuid = event.assistantUuid ?? assistantUuid;
5941
5967
  }
5942
5968
  else if (event.type === 'tool_use') {
5969
+ // 产生工具调用同样证明输入已被消费,且此时重放会重复发起副作用。
5970
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
5943
5971
  toolUseCount++;
5944
5972
  assistantUuid = event.assistantUuid ?? assistantUuid;
5945
5973
  if (event.callId) {
@@ -563,6 +563,14 @@ function appendLClassReadOnlyBinds(args, root, options = {}) {
563
563
  args.push('--ro-bind', target.path, target.path);
564
564
  }
565
565
  }
566
+ function appendLClassMasks(args, root, options = {}) {
567
+ for (const target of getExistingLClassReadOnlyTargets(root, options)) {
568
+ if (target.kind === 'directory')
569
+ pushMaskedDirectory(args, target.path);
570
+ else
571
+ pushMaskedFile(args, target.path);
572
+ }
573
+ }
566
574
  export function ensureCodexManagedMountpoint(systemConfigDirectory = '/etc/codex', effectiveUid = typeof process.getuid === 'function' ? process.getuid() : undefined) {
567
575
  let systemConfigStat;
568
576
  try {
@@ -691,7 +699,12 @@ export function buildBubblewrapCommand(executable, executableArgs, options) {
691
699
  pushMaskedFile(args, resolved);
692
700
  }
693
701
  }
694
- appendLClassReadOnlyBinds(args, root, options);
702
+ if (options.denyLClassRead) {
703
+ appendLClassMasks(args, root, options);
704
+ }
705
+ else {
706
+ appendLClassReadOnlyBinds(args, root, options);
707
+ }
695
708
  appendHClassMasks(args, root, true, options);
696
709
  if (options.exposeDaemonIpc !== false)
697
710
  appendDaemonIpcSocket(args, root);
@@ -133,6 +133,8 @@ export class TaskRecoveryService {
133
133
  agentSessionId: session.agentSessionId, baseagent: session.baseagent,
134
134
  automatic: request.automatic === true,
135
135
  origin,
136
+ approvalInteractionPolicy: context?.approvalInteractionPolicy
137
+ ?? (request.automatic ? 'deny' : 'interactive'),
136
138
  };
137
139
  const replyContext = replyContextFromSession(session);
138
140
  const message = {
@@ -1,5 +1,16 @@
1
1
  /** IPC has authenticated the transport; this enforces the Agent/task boundary. */
2
2
  export async function executeTaskResume(request, deps) {
3
+ if (request.argv) {
4
+ const argv = request.argv;
5
+ if (argv[0] !== 'task' || argv[1] !== 'resume' || !argv[2]?.trim() || argv[2].startsWith('-')
6
+ || !(argv.length === 3 || (argv.length === 5 && argv[3] === '--format' && argv[4] === 'json'))) {
7
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'Invalid task resume argv' };
8
+ }
9
+ request = { ...request, targetSessionId: argv[2].trim() };
10
+ }
11
+ else if (request.sessionId) {
12
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Managed task resume requires argv' };
13
+ }
3
14
  const authorization = request.sessionId
4
15
  ? await deps.authorizeManaged({
5
16
  sessionId: request.sessionId,
package/dist/index.js CHANGED
@@ -640,11 +640,11 @@ async function main() {
640
640
  getAgentRuntimeError: () => agentRuntimeError,
641
641
  daemonAid: daemonCfg.aid,
642
642
  getControlAidState: () => controlChannel?.getAidState(),
643
- }), async (cmd, sessionId, delegationToken, delegationCommandHash) => {
643
+ }), async (argv, sessionId, delegationToken, delegationCommandHash) => {
644
644
  const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
645
645
  if (!delegation.ok)
646
646
  return { ok: false, code: delegation.code, error: delegation.reason };
647
- return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
647
+ return cmdHandler.handleManagedCliOperation(argv, sessionId, delegation.grant);
648
648
  });
649
649
  ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
650
650
  const { reloadHooks, reloadCoordinator } = createAgentReloadRuntime({
package/dist/ipc.js CHANGED
@@ -21,6 +21,7 @@ import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
21
21
  import { remoteAccessService } from './core/auth/remote-access-store.js';
22
22
  import { setModelCatalogDaemonOwner } from './core/model/model-catalog.js';
23
23
  import { RUNTIME_ENVIRONMENT_NAMES, setRuntimeEnvironmentOverride } from './config/runtime-env.js';
24
+ import { MAIN_CLI_BIN } from './product.js';
24
25
  const isWindows = process.platform === 'win32';
25
26
  const isNamedPipe = (p) => isWindows && p.toLowerCase().startsWith('\\\\.\\pipe\\');
26
27
  const CPU_SAMPLE_INTERVAL_MS = 1000;
@@ -75,6 +76,16 @@ const LOCAL_CONTROL_COMMANDS = new Set([
75
76
  'remote-auth.redeem',
76
77
  'remote-auth.validate',
77
78
  ]);
79
+ const MEMORY_COMMAND_ENDPOINTS = new Set([
80
+ 'memory.note', 'memory.note.update', 'memory.note.drop', 'memory.note.list',
81
+ 'memory.create', 'memory.update', 'memory.archive', 'memory.demote', 'memory.consolidate', 'memory.feedback',
82
+ ]);
83
+ const DELEGATED_ARGV_ENDPOINTS = new Set([
84
+ 'agent.op', 'config.op', 'contact.op', 'aid.op', 'group.op', 'msg.op', 'fs.op',
85
+ 'ctl', 'diag', 'menu-client.request', 'task.resume', 'queue.op', 'storage.auth', 'storage.upload',
86
+ 'trigger.op', 'local-command-context', 'evolagent.bootstrapComplete', ...MEMORY_COMMAND_ENDPOINTS,
87
+ 'handoff-return', 'handoff-recover-return', 'handoff-status', 'handoff-list', 'handoff-trace',
88
+ ]);
78
89
  let ipcQuerySequence = 0;
79
90
  function ipcLogToken(value, fallback = 'unknown') {
80
91
  const token = String(value ?? '').replace(/[^A-Za-z0-9_.:-]/g, '_').slice(0, 128);
@@ -925,6 +936,75 @@ export class IpcServer {
925
936
  error: 'The task delegation token belongs to a previous daemon runtime; start a new task authorization and do not retry this command in the current task',
926
937
  };
927
938
  }
939
+ if (typeof cmd.delegationToken === 'string'
940
+ && (DELEGATED_ARGV_ENDPOINTS.has(cmd.type) || (cmd.type === 'aun-msg-send' && cmd.argv !== undefined))) {
941
+ if (!Array.isArray(cmd.argv) || cmd.argv.some(value => typeof value !== 'string')) {
942
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Invalid command argv' };
943
+ }
944
+ const expectedHash = hashDelegatedCommandArgv([
945
+ MAIN_CLI_BIN,
946
+ ...(cmd.type === 'menu-client.request' ? ['menu-client'] : []),
947
+ ...cmd.argv,
948
+ ]);
949
+ if (!expectedHash || cmd.delegationCommandHash !== expectedHash) {
950
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Command hash does not match the submitted argv' };
951
+ }
952
+ }
953
+ if (cmd.argv !== undefined && ['handoff-status', 'handoff-list', 'handoff-trace'].includes(cmd.type)) {
954
+ try {
955
+ const { parseList, parseTrace } = await import('./cli/handoff-command.js');
956
+ if (!Array.isArray(cmd.argv) || cmd.argv[0] !== 'handoff' || `handoff-${cmd.argv[1]}` !== cmd.type) {
957
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Handoff endpoint does not match argv' };
958
+ }
959
+ if (cmd.type === 'handoff-list') {
960
+ const parsed = parseList(cmd.argv.slice(2));
961
+ cmd = { ...cmd, agent: parsed.agent, state: parsed.state, filterSessionId: parsed.sessionId, limit: parsed.limit };
962
+ }
963
+ else {
964
+ const parsed = parseTrace(cmd.argv.slice(2));
965
+ if (cmd.type === 'handoff-status' && cmd.argv.length !== 3)
966
+ throw new Error('Invalid handoff status arguments');
967
+ cmd = { ...cmd, agent: parsed.agent, handoffId: parsed.handoffId, limit: parsed.limit };
968
+ }
969
+ }
970
+ catch (error) {
971
+ return { ok: false, code: 'INVALID_ARGUMENT', error: error instanceof Error ? error.message : String(error) };
972
+ }
973
+ }
974
+ if (MEMORY_COMMAND_ENDPOINTS.has(cmd.type)) {
975
+ if (typeof cmd.sessionId !== 'string' || !cmd.sessionId)
976
+ return { ok: false, code: 'INVALID_REQUEST', error: 'missing sessionId' };
977
+ if (cmd.debug === true) {
978
+ if (cmd.delegationToken || !this.hasValidControlToken(cmd.controlToken)) {
979
+ return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'Memory debug requires a local control token, not task delegation' };
980
+ }
981
+ }
982
+ else {
983
+ if (!this.taskRuntimeContextAuthorizer)
984
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Memory authorization is not configured' };
985
+ if (!Array.isArray(cmd.argv) || cmd.argv.some((value) => typeof value !== 'string') || cmd.argv.includes('--debug')) {
986
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Managed memory requires argv without --debug' };
987
+ }
988
+ const authorization = this.taskRuntimeContextAuthorizer({
989
+ sessionId: cmd.sessionId, delegationToken: cmd.delegationToken,
990
+ delegationCommandHash: cmd.delegationCommandHash, consumeApproval: true,
991
+ });
992
+ if (!authorization.ok)
993
+ return authorization;
994
+ }
995
+ if (cmd.argv !== undefined) {
996
+ try {
997
+ const { resolveMemoryCommandArgv } = await import('./cli/memory-command.js');
998
+ const operation = resolveMemoryCommandArgv(cmd.argv);
999
+ if (operation.type !== cmd.type)
1000
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Memory endpoint does not match argv' };
1001
+ cmd = { ...operation, sessionId: cmd.sessionId, debug: cmd.debug === true };
1002
+ }
1003
+ catch (error) {
1004
+ return { ok: false, code: 'INVALID_ARGUMENT', error: error instanceof Error ? error.message : String(error) };
1005
+ }
1006
+ }
1007
+ }
928
1008
  switch (cmd.type) {
929
1009
  case 'diag':
930
1010
  return this.diagExecutor ? this.diagExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', message: '诊断入口未配置' };
@@ -1168,6 +1248,7 @@ export class IpcServer {
1168
1248
  sessionId: cmd.sessionId,
1169
1249
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
1170
1250
  delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
1251
+ argv: Array.isArray(cmd.argv) ? cmd.argv : undefined,
1171
1252
  });
1172
1253
  }
1173
1254
  case 'status.auth': {
@@ -1191,6 +1272,7 @@ export class IpcServer {
1191
1272
  return { ok: false, code: 'INVALID_REQUEST', error: 'invalid storage operation' };
1192
1273
  }
1193
1274
  return await this.storageAuthorizationExecutor({
1275
+ argv: cmd.argv,
1194
1276
  sessionId: cmd.sessionId,
1195
1277
  aid: cmd.aid,
1196
1278
  operation: cmd.operation,
@@ -1208,6 +1290,7 @@ export class IpcServer {
1208
1290
  return { ok: false, code: 'INVALID_REQUEST', error: 'missing sessionId, aid, localFile, or remotePath' };
1209
1291
  }
1210
1292
  return await this.storageUploadExecutor({
1293
+ argv: cmd.argv,
1211
1294
  sessionId: cmd.sessionId,
1212
1295
  aid: cmd.aid,
1213
1296
  localFile: cmd.localFile,
@@ -1235,9 +1318,10 @@ export class IpcServer {
1235
1318
  }
1236
1319
  }
1237
1320
  case 'task.resume': {
1238
- const allowed = new Set(['type', 'targetSessionId', 'sessionId', 'delegationToken',
1321
+ const allowed = new Set(['type', 'argv', 'targetSessionId', 'sessionId', 'delegationToken',
1239
1322
  'delegationCommandHash', 'controlToken', 'daemonRuntimeEpoch', 'requestId', 'id']);
1240
1323
  if (Object.keys(cmd).some(key => !allowed.has(key))
1324
+ || (cmd.argv !== undefined && (!Array.isArray(cmd.argv) || cmd.argv.some((value) => typeof value !== 'string')))
1241
1325
  || typeof cmd.targetSessionId !== 'string' || !cmd.targetSessionId.trim()
1242
1326
  || (cmd.sessionId !== undefined && (typeof cmd.sessionId !== 'string' || !cmd.sessionId.trim()))) {
1243
1327
  return { ok: false, code: 'INVALID_REQUEST', error: 'task.resume accepts only a target session and caller credentials' };
@@ -1250,6 +1334,7 @@ export class IpcServer {
1250
1334
  try {
1251
1335
  return await this.taskResumeExecutor({
1252
1336
  targetSessionId: cmd.targetSessionId,
1337
+ argv: Array.isArray(cmd.argv) ? cmd.argv : undefined,
1253
1338
  sessionId: cmd.sessionId,
1254
1339
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
1255
1340
  delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
@@ -1260,6 +1345,34 @@ export class IpcServer {
1260
1345
  return { ok: false, code: 'TASK_RESUME_FAILED', error: error instanceof Error ? error.message : String(error) };
1261
1346
  }
1262
1347
  }
1348
+ case 'local-command-context': {
1349
+ if (!Array.isArray(cmd.argv) || cmd.argv.some((value) => typeof value !== 'string')
1350
+ || !['skill', 'stats'].includes(cmd.argv[0]) || typeof cmd.sessionId !== 'string' || !cmd.sessionId
1351
+ || typeof cmd.delegationToken !== 'string' || !cmd.delegationToken) {
1352
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Local command requires argv and task credentials' };
1353
+ }
1354
+ if (!this.taskRuntimeContextAuthorizer || !this.taskRuntimeContextProvider) {
1355
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Command authorization is unavailable' };
1356
+ }
1357
+ const authorization = this.taskRuntimeContextAuthorizer({
1358
+ sessionId: cmd.sessionId, delegationToken: cmd.delegationToken,
1359
+ delegationCommandHash: cmd.delegationCommandHash, consumeApproval: true,
1360
+ });
1361
+ if (!authorization.ok)
1362
+ return authorization;
1363
+ const context = this.taskRuntimeContextProvider({ sessionId: cmd.sessionId });
1364
+ if (!context?.selfAid || !context.peerRole || context.peerRole === 'none') {
1365
+ return { ok: false, code: 'NOT_ALLOWED', error: 'Current task identity and role are unavailable' };
1366
+ }
1367
+ if (cmd.argv[0] === 'stats' && cmd.argv.some((value) => /^--(?:sql|rebuild)(?:=|$)/.test(value))) {
1368
+ return { ok: false, code: 'NOT_ALLOWED', error: 'Managed stats cannot use --sql or --rebuild' };
1369
+ }
1370
+ if (cmd.argv[0] === 'skill' && context.permissionMode === 'readonly'
1371
+ && ['create', 'install', 'remove', 'enable', 'disable'].includes(cmd.argv[1])) {
1372
+ return { ok: false, code: 'NOT_ALLOWED', error: 'Readonly tasks cannot modify skills' };
1373
+ }
1374
+ return { ok: true, context };
1375
+ }
1263
1376
  case 'task-runtime-context': {
1264
1377
  if (!this.taskRuntimeContextProvider)
1265
1378
  return { ok: false, error: 'task runtime context not configured' };
@@ -1279,6 +1392,15 @@ export class IpcServer {
1279
1392
  return { ok: true, context };
1280
1393
  }
1281
1394
  case 'aun-msg-send': {
1395
+ if (cmd.argv !== undefined) {
1396
+ if (!this.aunMsgSender)
1397
+ return { ok: false, code: 'NOT_CONFIGURED', error: 'AUN sender is not configured' };
1398
+ return this.aunMsgSender({
1399
+ aid: '', to: '', scope: 'msg', argv: cmd.argv,
1400
+ originSessionId: cmd.originSessionId, originMessageId: cmd.originMessageId,
1401
+ delegationToken: cmd.delegationToken, delegationCommandHash: cmd.delegationCommandHash,
1402
+ });
1403
+ }
1282
1404
  if (cmd.handoffContext !== undefined && (typeof cmd.handoffContext !== 'string'
1283
1405
  || !cmd.handoffContext.trim() || cmd.handoffContext.length > 64 * 1024 || cmd.scope === 'group')) {
1284
1406
  return { ok: false, code: 'INVALID_HANDOFF_CONTEXT', error: 'Private handoff context must contain 1–65536 characters' };
@@ -1356,6 +1478,7 @@ export class IpcServer {
1356
1478
  return { ok: false, code: 'HANDOFF_RETURN_CONTENT_REQUIRED', error: 'missing content' };
1357
1479
  try {
1358
1480
  return await this.handoffReturnExecutor({
1481
+ argv: cmd.argv,
1359
1482
  sessionId: typeof cmd.sessionId === 'string' ? cmd.sessionId : undefined,
1360
1483
  handoffId: typeof cmd.handoffId === 'string' ? cmd.handoffId : undefined,
1361
1484
  content: cmd.content,
@@ -1378,6 +1501,7 @@ export class IpcServer {
1378
1501
  return { ok: false, code: 'HANDOFF_RETURN_CONTENT_REQUIRED', error: 'missing content' };
1379
1502
  try {
1380
1503
  return await this.handoffRecoverReturnExecutor({
1504
+ argv: cmd.argv,
1381
1505
  sessionId: cmd.sessionId,
1382
1506
  handoffId: cmd.handoffId,
1383
1507
  content: cmd.content,
@@ -1444,10 +1568,13 @@ export class IpcServer {
1444
1568
  case 'ctl': {
1445
1569
  if (!this.commandExecutor)
1446
1570
  return { ok: false, error: 'ctl not configured' };
1447
- const { cmd: slashCmd, sessionId } = cmd;
1448
- if (!slashCmd || !sessionId)
1449
- return { ok: false, error: 'missing cmd or sessionId' };
1450
- return await this.commandExecutor(slashCmd, sessionId, typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
1571
+ const { sessionId, argv } = cmd;
1572
+ if (!sessionId || !Array.isArray(argv))
1573
+ return { ok: false, code: 'INVALID_REQUEST', error: 'missing argv or sessionId' };
1574
+ if (argv.some(value => typeof value !== 'string')) {
1575
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Invalid command argv' };
1576
+ }
1577
+ return await this.commandExecutor(argv, sessionId, typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
1451
1578
  }
1452
1579
  case 'memory.note': {
1453
1580
  const { sessionId, slug, content, tags, speakers } = cmd;
@@ -1864,11 +1991,11 @@ export class IpcServer {
1864
1991
  }
1865
1992
  return await this.agentOperationExecutor(argv, sessionId, delegationToken, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
1866
1993
  }
1994
+ case 'trigger.op':
1867
1995
  case 'trigger.list':
1868
1996
  case 'trigger.show':
1869
1997
  case 'trigger.history':
1870
1998
  case 'trigger.eventCatalog':
1871
- case 'trigger.authorizeLocalFiles':
1872
1999
  case 'trigger.create':
1873
2000
  case 'trigger.update':
1874
2001
  case 'trigger.setEnabled':
@@ -1917,6 +2044,17 @@ export class IpcServer {
1917
2044
  const aid = cmd.aid;
1918
2045
  if (!aid || typeof aid !== 'string')
1919
2046
  return { ok: false, error: 'missing aid' };
2047
+ if (cmd.argv !== undefined) {
2048
+ if (!Array.isArray(cmd.argv) || cmd.argv.some((value) => typeof value !== 'string')) {
2049
+ return { ok: false, code: 'INVALID_REQUEST', error: 'Invalid command argv' };
2050
+ }
2051
+ const argv = cmd.argv;
2052
+ const validFormat = argv.length === 3
2053
+ || (argv.length === 5 && argv[3] === '--format' && argv[4] === 'json');
2054
+ if (argv[0] !== 'agent' || argv[1] !== 'ready' || argv[2] !== aid || !validFormat) {
2055
+ return { ok: false, code: 'INVALID_REQUEST', error: 'agent ready endpoint does not match argv' };
2056
+ }
2057
+ }
1920
2058
  if (!this.bootstrapCompleteExecutor)
1921
2059
  return { ok: false, error: 'bootstrap complete executor not configured' };
1922
2060
  try {
@@ -2333,7 +2471,7 @@ export async function ipcQuery(socketPath, cmd, timeoutMs = 3000, options = {})
2333
2471
  function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
2334
2472
  let authenticatedCmd = cmd;
2335
2473
  if (!process.env.EVOLCORE_SESSION_ID
2336
- && requiresLocalControlToken(cmd)
2474
+ && (requiresLocalControlToken(cmd) || (MEMORY_COMMAND_ENDPOINTS.has(cmd.type) && cmd.debug === true))
2337
2475
  && typeof cmd.controlToken !== 'string') {
2338
2476
  try {
2339
2477
  const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
@@ -2345,7 +2483,7 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
2345
2483
  const requestArgv = Array.isArray(authenticatedCmd.argv)
2346
2484
  && authenticatedCmd.argv.every((value) => typeof value === 'string')
2347
2485
  ? [
2348
- 'ec',
2486
+ MAIN_CLI_BIN,
2349
2487
  ...(authenticatedCmd.type === 'menu-client.request' ? ['menu-client'] : []),
2350
2488
  ...authenticatedCmd.argv,
2351
2489
  ]
@@ -247,7 +247,7 @@ export function authorizeTriggerExecution(definition, dependencies) {
247
247
  }
248
248
  const permissionDecision = constrainRuntimePermissionMode({
249
249
  selfAid: definition.agentAid,
250
- role,
250
+ role: typeof role === 'string' ? role : undefined,
251
251
  requestedValue: definition.execution.permissionMode,
252
252
  });
253
253
  if (definition.execution.permissionMode && permissionDecision.constrained) {
@@ -260,6 +260,7 @@ export function authorizeTriggerExecution(definition, dependencies) {
260
260
  allowed: true,
261
261
  scope: privilegedReason && hasDaemonTriggerAuthority(subject) ? 'daemon' : 'agent',
262
262
  permissionMode: permissionDecision.effectiveValue,
263
+ role: role ?? undefined,
263
264
  };
264
265
  }
265
266
  export async function authenticatedTriggerActor(dependencies, agentAid, actorSessionId, delegationToken, delegationCommandHash, controlToken, contextOnly = false) {