evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ import { MessageQueue } from './core/message/message-queue.js';
46
46
  import { MessageBridge } from './core/message/message-bridge.js';
47
47
  import { evaluateOutboundContactAdmission } from './core/message/message-admission.js';
48
48
  import { enforceMenuResponseSize, evaluateMenuClientCompatibility, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
49
- import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
49
+ import { computeBuildId, readBuildIdentity, readEvolcoreRuntimeFlavor, readEvolcoreStartedAtIso, readInstalledEvolcoreVersion, tagErrorTextWithEvolcoreVersion, } from './utils/evolcore-version.js';
50
50
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
51
51
  import { HandoffRuntime } from './core/handoff/runtime.js';
52
52
  import { BootstrapService, completeBootstrapWithWelcome } from './core/bootstrap-service.js';
@@ -75,6 +75,7 @@ import { buildReloadHooks } from './core/channel-loader.js';
75
75
  import { IpcServer } from './ipc.js';
76
76
  import { logger, setLogLevel } from './utils/logger.js';
77
77
  import { remoteAccessService } from './core/auth/remote-access-store.js';
78
+ import { DiagService, executeDiagIpc } from './core/command/diag-service.js';
78
79
  import { createEcwebSupervisor } from './utils/ecweb-supervisor.js';
79
80
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
80
81
  import { detectDuplicates } from './core/evolagent-registry.js';
@@ -147,6 +148,45 @@ function summarizeOutboundPayload(payload) {
147
148
  }
148
149
  return s;
149
150
  }
151
+ /**
152
+ * 给用户可见的错误文案打上 EvolCore 版本标记,便于从对端截图直接判断版本与运行形态。
153
+ *
154
+ * 打标范围只含"出错了"语义的 payload:
155
+ * - `result.error` / `system.error`:任务级与系统级失败(含沙箱、权限、超时)
156
+ * - `status.error`:终态元数据,附带结构化版本字段供前端不受截断影响地展示
157
+ * - `command.error`:仅当带 `reasonCode`(授权/权限拒绝)时才打。命令参数写错、
158
+ * 未知命令这类输入校验反馈不打——它们与版本无关,逐条加前缀只会变成噪音。
159
+ *
160
+ * 返回新对象,不改调用方持有的 payload(同一 payload 可能被重复发送到多个渠道)。
161
+ */
162
+ function tagErrorPayloadWithEvolcoreVersion(payload) {
163
+ switch (payload.kind) {
164
+ case 'result.error':
165
+ case 'system.error':
166
+ return { ...payload, text: tagErrorTextWithEvolcoreVersion(payload.text) };
167
+ case 'command.error':
168
+ if (!payload.reasonCode)
169
+ return payload;
170
+ return { ...payload, text: tagErrorTextWithEvolcoreVersion(payload.text) };
171
+ case 'status.error': {
172
+ const metadata = payload.metadata;
173
+ return {
174
+ ...payload,
175
+ metadata: {
176
+ ...metadata,
177
+ ...(typeof metadata?.message === 'string'
178
+ ? { message: tagErrorTextWithEvolcoreVersion(metadata.message) }
179
+ : {}),
180
+ evolcoreVersion: readInstalledEvolcoreVersion(),
181
+ evolcoreFlavor: readEvolcoreRuntimeFlavor(),
182
+ evolcoreStartedAt: readEvolcoreStartedAtIso(),
183
+ },
184
+ };
185
+ }
186
+ default:
187
+ return payload;
188
+ }
189
+ }
150
190
  function shouldCountSentPayload(payload) {
151
191
  return [
152
192
  'result.text',
@@ -610,6 +650,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
610
650
  }));
611
651
  ipcServer.setControlToken(controlToken);
612
652
  ipcServer.setRemoteAccessService(remoteAccessService);
653
+ ipcServer.setRemoteAccessOwnerProvider(() => daemonCfg.owners?.[0]);
613
654
  ipcServer.setShutdownExecutor(async (reason) => {
614
655
  if (!shutdown) {
615
656
  pendingShutdownReason = reason;
@@ -757,6 +798,12 @@ async function main() {
757
798
  // Service managers do not necessarily export TMPDIR. Establish the private
758
799
  // managed root before any runner or task runtime is initialized.
759
800
  ensureProcessManagedTempDir();
801
+ const runtimeEntry = fileURLToPath(import.meta.url);
802
+ const diskDistDir = path.join(getPackageRoot(), 'dist');
803
+ const runtimeIsDist = path.resolve(path.dirname(runtimeEntry)) === path.resolve(diskDistDir);
804
+ const runtimeBuildIdentity = readBuildIdentity(runtimeIsDist ? path.dirname(runtimeEntry) : diskDistDir, readEvolcoreVersion());
805
+ if (!runtimeIsDist)
806
+ runtimeBuildIdentity.id = null;
760
807
  // 启动信息:目录类型 + 版本号 + 代码最新时间戳
761
808
  {
762
809
  const pkgRoot = getPackageRoot();
@@ -798,6 +845,10 @@ async function main() {
798
845
  console.error(`[EvolCore] 代码时间: ${latestMtime ? fmtTime(latestMtime) : '?'}`);
799
846
  }
800
847
  console.error('[EvolCore] ✓ Version info printed');
848
+ const diskBuildId = computeBuildId(diskDistDir);
849
+ console.error(`[EvolCore] 运行 build ID: ${runtimeIsDist ? (runtimeBuildIdentity.id ?? '?') : 'source'}`);
850
+ console.error(`[EvolCore] 磁盘 dist build ID: ${diskBuildId ?? '?'}`);
851
+ logger.info(`[Build] runtimeMode=${runtimeIsDist ? 'dist' : 'source'} runtimeBuildId=${runtimeBuildIdentity.id ?? 'source'} diskDistBuildId=${diskBuildId ?? 'unknown'} match=${runtimeIsDist && runtimeBuildIdentity.id !== null && runtimeBuildIdentity.id === diskBuildId}`);
801
852
  // 过滤飞书 SDK 的 info 日志
802
853
  const originalLog = console.log;
803
854
  const originalInfo = console.info;
@@ -975,13 +1026,13 @@ async function main() {
975
1026
  port: ecwebConfig.port ?? DEFAULT_ECWEB_PORT,
976
1027
  logger,
977
1028
  });
978
- try {
979
- await ecwebSupervisor.start();
980
- onShutdown(() => ecwebSupervisor?.stop());
981
- }
982
- catch (error) {
1029
+ // ECWeb is optional and has its own health checks/restart loop. Starting
1030
+ // it must not hold up the daemon's agent/IPC initialization or ready
1031
+ // signal; status will report it as starting until the first healthy probe.
1032
+ onShutdown(() => ecwebSupervisor?.stop());
1033
+ void ecwebSupervisor.start().catch((error) => {
983
1034
  logger.warn(`ECWeb supervisor 启动失败(不影响 daemon 主流程): ${error?.message || error}`);
984
- }
1035
+ });
985
1036
  }
986
1037
  if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
987
1038
  await runBindBootstrapDaemon(daemonCfg);
@@ -1918,7 +1969,9 @@ async function main() {
1918
1969
  function registerChannelInstance(inst) {
1919
1970
  // 0. 包装 adapter.send,记录所有出站到 channel-out.log
1920
1971
  const originalSend = inst.adapter.send.bind(inst.adapter);
1921
- inst.adapter.send = async (envelope, payload) => {
1972
+ inst.adapter.send = async (envelope, rawPayload) => {
1973
+ // 版本标记在记账之前打,让 channel-out.log 与对端看到的文案一致。
1974
+ const payload = rawPayload ? tagErrorPayloadWithEvolcoreVersion(rawPayload) : rawPayload;
1922
1975
  const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey)
1923
1976
  ?? agentRegistry.resolveByChannel(inst.adapter.channelName);
1924
1977
  logger.channelOut({
@@ -2316,6 +2369,14 @@ async function main() {
2316
2369
  // A failed bind (for example, a stale Windows named pipe) must abort startup
2317
2370
  // without creating a second AUN session that can kick the old daemon.
2318
2371
  let controlChannel;
2372
+ const diagService = new DiagService(() => ({ controlAid: loadDaemonConfig().aid, owners: loadDaemonConfig().owners ?? [] }), async (to, text) => {
2373
+ if (!controlChannel)
2374
+ throw new Error('Control AID channel is unavailable');
2375
+ const result = await controlChannel.sendDaemonMsg({ to, payload: { type: 'text', text }, encrypt: true });
2376
+ if (!result.ok)
2377
+ throw new Error(result.error ?? 'Invitation send failed');
2378
+ });
2379
+ cmdHandler.setDiagService(diagService);
2319
2380
  let shutdown;
2320
2381
  let pendingShutdownReason;
2321
2382
  const ipcServer = new IpcServer(resolvePaths().socket, () => {
@@ -2335,6 +2396,13 @@ async function main() {
2335
2396
  return {
2336
2397
  pid: process.pid,
2337
2398
  uptime: snap.uptimeMs,
2399
+ build: {
2400
+ runtimeMode: runtimeIsDist ? 'dist' : 'source',
2401
+ runtimeId: runtimeBuildIdentity.id,
2402
+ diskDistId: computeBuildId(diskDistDir),
2403
+ entry: runtimeBuildIdentity.entry,
2404
+ version: runtimeBuildIdentity.version,
2405
+ },
2338
2406
  controlPlane: {
2339
2407
  ready: true,
2340
2408
  owned: processLevelOwners.length > 0,
@@ -2384,6 +2452,7 @@ async function main() {
2384
2452
  return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
2385
2453
  });
2386
2454
  ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
2455
+ ipcServer.setRemoteAccessOwnerProvider(() => processLevelOwners[0]);
2387
2456
  cmdHandler.setDaemonMonitorProvider(() => ipcServer.getMonitorSnapshot().snapshot);
2388
2457
  // Register every IPC executor/provider before exposing the endpoint. The
2389
2458
  // function declaration is hoisted, while its invocation remains here so a
@@ -2446,14 +2515,16 @@ async function main() {
2446
2515
  aunSdkLog: daemonCfg.debug?.aunSdkLog,
2447
2516
  });
2448
2517
  // connect() 失败不置空实例:AUNChannel 内部有无限重连(SDK auto_reconnect +
2449
- // scheduleReconnect),首连失败后台会自愈;保留实例供 status 显示 disconnected。
2450
- try {
2451
- await controlChannel.connect();
2518
+ // scheduleReconnect),首连失败会自愈;启动连接放到后台,避免 ready signal
2519
+ // 被远端网关/TLS 首连延迟阻塞。Attach the handlers below before the
2520
+ // promise can complete so an early control message is still accepted.
2521
+ const controlConnectPromise = controlChannel.connect()
2522
+ .then(() => {
2452
2523
  logger.info(`✓ 控制 AID 已连接: ${daemonCfg.aid}`);
2453
- }
2454
- catch (e) {
2524
+ })
2525
+ .catch((e) => {
2455
2526
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2456
- }
2527
+ });
2457
2528
  // 控制 AID 接收 daemon-owner 或有效远程访问 token 授权的指令:
2458
2529
  // 1. /pair — daemon 远程控制配对码(文本快路径)
2459
2530
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
@@ -2522,6 +2593,13 @@ async function main() {
2522
2593
  const isRoleMenu = menuControl.isMenu
2523
2594
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
2524
2595
  const currentProcessOwners = loadDaemonConfig().owners ?? [];
2596
+ if (/^\/diag(?:\s|$)/i.test(text)) {
2597
+ const command = text.slice(5).trim();
2598
+ const result = await diagService.execute(command.toLowerCase() === 'stop'
2599
+ ? { action: 'stop' } : { action: 'start', problem: command }, opts.peerId);
2600
+ await controlChannel.sendMessage(opts.channelId, result.message, controlReplyContext(opts));
2601
+ return;
2602
+ }
2525
2603
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2526
2604
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
2527
2605
  logMenuRequestCompleted(menuControl.request, undefined, menuFlowContext, {
@@ -2595,7 +2673,7 @@ async function main() {
2595
2673
  return;
2596
2674
  }
2597
2675
  // owner 发的其他内容:提示可用指令
2598
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取远程控制配对码)', controlReplyContext(opts));
2676
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取远程控制配对码)、/diag <问题描述>、/diag stop', controlReplyContext(opts));
2599
2677
  }
2600
2678
  catch (e) {
2601
2679
  if (activeMenuRequest && !menuCompletionLogged) {
@@ -2608,6 +2686,7 @@ async function main() {
2608
2686
  logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
2609
2687
  }
2610
2688
  });
2689
+ void controlConnectPromise;
2611
2690
  // ── Service Proxy:把本地服务(ecweb 等)通过控制 AID 暴露到 AUN 网络 ──
2612
2691
  // 挂在控制 AUNChannel 上,动态解引用其 client(规避重连换 client)。
2613
2692
  // 失败只 warn,不影响 daemon 主流程。
@@ -2859,6 +2938,17 @@ async function main() {
2859
2938
  ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2860
2939
  ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2861
2940
  ipcServer.setAidOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash));
2941
+ ipcServer.setDiagExecutor(cmd => executeDiagIpc(diagService, agentDelegationRegistry, loadDaemonConfig().owners ?? [], cmd));
2942
+ ipcServer.setMenuClientExecutor(async (cmd) => {
2943
+ if (!Array.isArray(cmd.argv) || cmd.argv.some((v) => typeof v !== 'string') || typeof cmd.sessionId !== 'string')
2944
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'A managed session and Menu argv are required' };
2945
+ return cmdHandler.handleMenuClientOperation(cmd.argv, cmd.sessionId, cmd.delegationToken, cmd.delegationCommandHash, async (options) => {
2946
+ const instance = channelInstances.find(item => item.channelType === 'aun' && item.channel.getAidState()?.aid === options.fromAid);
2947
+ if (!instance)
2948
+ throw new Error('Current Agent AUN channel is unavailable');
2949
+ return instance.channel.requestRemoteMenu(options);
2950
+ });
2951
+ });
2862
2952
  ipcServer.setGroupOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash));
2863
2953
  ipcServer.setMsgOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash));
2864
2954
  ipcServer.setFsOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash));
@@ -3005,7 +3095,14 @@ async function main() {
3005
3095
  error: decision.reason,
3006
3096
  };
3007
3097
  }
3008
- return { ok: true, aid: selfAid, currentSessionId: params.sessionId, managementRole };
3098
+ return {
3099
+ ok: true,
3100
+ aid: selfAid,
3101
+ currentSessionId: params.sessionId,
3102
+ managementRole,
3103
+ role: subject.role,
3104
+ actorId: delegation.grant.actorId,
3105
+ };
3009
3106
  };
3010
3107
  const resolveHandoffQueryScope = async (params, operation) => {
3011
3108
  if (params.sessionId) {
@@ -3027,6 +3124,22 @@ async function main() {
3027
3124
  return scope;
3028
3125
  return responseEngine.returnAuthorizedHandoffResult({ sessionId: params.sessionId, handoffId: params.handoffId, content: params.content }, scope.aid);
3029
3126
  });
3127
+ ipcServer.setHandoffRecoverReturnExecutor(async (params) => {
3128
+ if (!params.sessionId)
3129
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is required' };
3130
+ const scope = await resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, 'ec.handoff.recover');
3131
+ if (!scope.ok)
3132
+ return scope;
3133
+ if (scope.role !== 'owner') {
3134
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: 'handoff recovery requires the current Agent owner' };
3135
+ }
3136
+ return responseEngine.recoverAuthorizedHandoffResult({
3137
+ sessionId: params.sessionId,
3138
+ handoffId: params.handoffId,
3139
+ content: params.content,
3140
+ actorId: scope.actorId,
3141
+ }, scope.aid);
3142
+ });
3030
3143
  ipcServer.setHandoffStatusExecutor(async (params) => {
3031
3144
  const scope = await resolveHandoffQueryScope(params, 'ec.handoff.status');
3032
3145
  if (!scope.ok)
@@ -3177,6 +3290,7 @@ async function main() {
3177
3290
  thread: params.thread,
3178
3291
  targetChatType: params.scope === 'group' ? 'group' : 'private',
3179
3292
  explicitReturnPolicy: params.returnPolicy,
3293
+ handoffContext: params.handoffContext,
3180
3294
  originAuthorization: {
3181
3295
  actorId: delegation.grant.actorId,
3182
3296
  channelKey: delegation.grant.channel,
@@ -3663,7 +3777,8 @@ async function main() {
3663
3777
  });
3664
3778
  if (params.targetAid
3665
3779
  && params.targetAid !== delegation.grant.selfAid
3666
- && subject.dataScope !== 'daemon') {
3780
+ && subject.dataScope !== 'daemon'
3781
+ && !(operation === 'agent.show' && isManagementRole(subject.role))) {
3667
3782
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'only the current Agent can be targeted' };
3668
3783
  }
3669
3784
  const decision = authorizeOperation({
@@ -3710,18 +3825,21 @@ async function main() {
3710
3825
  const taskAid = delegation.grant.selfAid;
3711
3826
  const targetAid = sub === 'list'
3712
3827
  ? undefined
3713
- : sub === 'show'
3714
- ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : taskAid)
3715
- : sub === 'reload'
3716
- ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined)
3717
- : (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined);
3828
+ : sub === 'persona'
3829
+ ? taskAid
3830
+ : sub === 'show'
3831
+ ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : taskAid)
3832
+ : sub === 'reload'
3833
+ ? (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined)
3834
+ : (argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined);
3718
3835
  const operation = sub === 'list' ? 'agent.list'
3719
3836
  : sub === 'show' ? 'agent.show'
3720
- : sub === 'new' ? 'agent.create'
3721
- : sub === 'enable' ? 'agent.enable'
3722
- : sub === 'disable' ? 'agent.disable'
3723
- : sub === 'reload' ? 'agent.reload'
3724
- : sub === 'delete' ? 'agent.delete' : undefined;
3837
+ : sub === 'persona' ? 'agent.persona'
3838
+ : sub === 'new' ? 'agent.create'
3839
+ : sub === 'enable' ? 'agent.enable'
3840
+ : sub === 'disable' ? 'agent.disable'
3841
+ : sub === 'reload' ? 'agent.reload'
3842
+ : sub === 'delete' ? 'agent.delete' : undefined;
3725
3843
  if (!operation)
3726
3844
  return fail('INVALID_ARGS', `unsupported managed agent command: ${sub}`);
3727
3845
  if (sub === 'new' && !argv.includes('--non-interactive')) {
@@ -3753,7 +3871,7 @@ async function main() {
3753
3871
  subject,
3754
3872
  intent: {
3755
3873
  operation,
3756
- scope: operation === 'agent.show'
3874
+ scope: operation === 'agent.show' || operation === 'agent.persona'
3757
3875
  ? 'agent'
3758
3876
  : operation === 'agent.reload' && targetAid
3759
3877
  ? 'agent'
@@ -3799,6 +3917,7 @@ async function main() {
3799
3917
  ok: true,
3800
3918
  aid: agent.aid,
3801
3919
  status: agent.status,
3920
+ lifecycle: agent.lifecycle ?? null,
3802
3921
  identity: agent.identity ?? { name: null, description: null },
3803
3922
  config: {
3804
3923
  baseagent: agent.config?.baseagent ?? null,
@@ -3809,16 +3928,50 @@ async function main() {
3809
3928
  },
3810
3929
  connection: agent.connection ?? null,
3811
3930
  sessions: agent.sessions ?? { active: 0, last_activity: null },
3812
- paths: { config: null, agent_md: null, project: null, data: null },
3813
- redacted: true,
3931
+ paths: subject.dataScope === 'daemon'
3932
+ ? (agent.paths ?? { config: null, agent_md: null, project: null, data: null })
3933
+ : { config: null, agent_md: null, project: null, data: null },
3934
+ ...(subject.dataScope === 'daemon' ? {} : { redacted: true }),
3814
3935
  ...(agent.createProgress ? { createProgress: agent.createProgress } : {}),
3815
3936
  } };
3816
3937
  }
3938
+ if (operation === 'agent.persona') {
3939
+ const rawPersonaArgs = argv.slice(2);
3940
+ const hasFormatSuffix = rawPersonaArgs.length >= 2
3941
+ && rawPersonaArgs.at(-2) === '--format'
3942
+ && rawPersonaArgs.at(-1) === 'json';
3943
+ const personaArgs = hasFormatSuffix ? rawPersonaArgs.slice(0, -2) : rawPersonaArgs;
3944
+ if (!hasFormatSuffix && rawPersonaArgs.includes('--format')) {
3945
+ return fail('INVALID_ARGS', 'persona 仅支持末尾的 --format json');
3946
+ }
3947
+ if (personaArgs.length === 2 && personaArgs[0] === '--file') {
3948
+ return fail('INVALID_ARGS', '托管 persona --file 必须由 CLI 读取后作为文本提交');
3949
+ }
3950
+ const wantsShow = personaArgs.length === 1 && personaArgs[0] === 'show';
3951
+ const text = personaArgs.length === 2 && personaArgs[0] === '--text'
3952
+ ? personaArgs[1]
3953
+ : undefined;
3954
+ if (!wantsShow && !(personaArgs.length === 2 && personaArgs[0] === '--text')) {
3955
+ return fail('INVALID_ARGS', '用法: ec agent persona show | --text <text>');
3956
+ }
3957
+ if (text === undefined && !wantsShow)
3958
+ return fail('INVALID_ARGS', 'persona --text 缺少参数');
3959
+ const { agentPersona } = await import('./cli/agent.js');
3960
+ const result = await agentPersona(taskAid, wantsShow ? { trustedLocal: true } : { text, trustedLocal: true });
3961
+ if (!result.ok)
3962
+ return fail(result.code ?? 'INTERNAL', result.error);
3963
+ return { ok: true, result: {
3964
+ ok: true,
3965
+ aid: taskAid,
3966
+ content: result.content,
3967
+ ...(result.saved ? { saved: true } : {}),
3968
+ } };
3969
+ }
3817
3970
  if (operation === 'agent.create') {
3818
3971
  const aid = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
3819
3972
  if (!aid)
3820
3973
  return fail('INVALID_ARGS', 'ec agent new requires an AID');
3821
- const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan, parseAgentOwnerArgs } = await import('./cli/agent-create.js');
3974
+ const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan, parseAgentOwnerArgs, parseAgentTags } = await import('./cli/agent-create.js');
3822
3975
  let ownerOptions;
3823
3976
  try {
3824
3977
  ownerOptions = parseAgentOwnerArgs(argv);
@@ -3826,6 +3979,13 @@ async function main() {
3826
3979
  catch (error) {
3827
3980
  return fail('INVALID_ARGS', error instanceof Error ? error.message : String(error));
3828
3981
  }
3982
+ let tags;
3983
+ try {
3984
+ tags = parseAgentTags(value('--tags'));
3985
+ }
3986
+ catch (error) {
3987
+ return fail('INVALID_ARGS', error instanceof Error ? error.message : String(error));
3988
+ }
3829
3989
  const plan = resolveAgentCreatePlan({
3830
3990
  aid,
3831
3991
  project: value('--project'),
@@ -3834,6 +3994,8 @@ async function main() {
3834
3994
  handoff: value('--handoff'),
3835
3995
  name: value('--name'),
3836
3996
  description: value('--description'),
3997
+ type: value('--type'),
3998
+ tags,
3837
3999
  force: argv.includes('--force'),
3838
4000
  });
3839
4001
  const validationError = validateAgentCreatePlan(plan);
@@ -3890,6 +4052,8 @@ async function main() {
3890
4052
  owners: plan.owners,
3891
4053
  name: plan.name,
3892
4054
  description: plan.description,
4055
+ type: plan.type,
4056
+ tags: plan.tags,
3893
4057
  force: argv.includes('--force'),
3894
4058
  };
3895
4059
  try {
package/dist/ipc.js CHANGED
@@ -76,7 +76,7 @@ function requiresLocalControlToken(cmd) {
76
76
  // Managed bootstrap completion is authenticated by the session delegation
77
77
  // ticket. A standalone CLI invocation still needs the daemon control token.
78
78
  return LOCAL_CONTROL_COMMANDS.has(String(cmd.type ?? ''))
79
- || (cmd.type === 'evolagent.bootstrapComplete' && typeof cmd.sessionId !== 'string');
79
+ || ((cmd.type === 'evolagent.bootstrapComplete' || cmd.type === 'diag') && typeof cmd.sessionId !== 'string');
80
80
  }
81
81
  export function summarizeMonitorAgents(agents, aids) {
82
82
  const connectedAgents = new Set(aids.filter(aid => aid.status === 'connected').map(aid => aid.agentName || aid.aid));
@@ -116,7 +116,10 @@ export class IpcServer {
116
116
  taskRuntimeContextProvider;
117
117
  codexPreToolUseExecutor;
118
118
  aunMsgSender;
119
+ diagExecutor;
120
+ menuClientExecutor;
119
121
  handoffReturnExecutor;
122
+ handoffRecoverReturnExecutor;
120
123
  handoffStatusExecutor;
121
124
  handoffListExecutor;
122
125
  handoffTraceExecutor;
@@ -135,6 +138,8 @@ export class IpcServer {
135
138
  controlToken;
136
139
  daemonRuntimeEpoch;
137
140
  remoteAccessService = remoteAccessService;
141
+ /** Current daemon owner used to bind locally issued ECWeb pairing codes. */
142
+ remoteAccessOwnerProvider;
138
143
  // CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
139
144
  // 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
140
145
  // procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
@@ -189,6 +194,15 @@ export class IpcServer {
189
194
  setRemoteAccessService(service) {
190
195
  this.remoteAccessService = service;
191
196
  }
197
+ /**
198
+ * Supply the live process-level owner for pairing requests that do not carry
199
+ * an explicit owner (for example `ec watch web` and the ECWeb supervisor).
200
+ * The provider is evaluated for every request so owner updates take effect
201
+ * without restarting ECWeb.
202
+ */
203
+ setRemoteAccessOwnerProvider(provider) {
204
+ this.remoteAccessOwnerProvider = provider;
205
+ }
192
206
  /** Set the process-lifetime marker used to reject stale managed children. */
193
207
  setDaemonRuntimeEpoch(epoch) {
194
208
  this.daemonRuntimeEpoch = epoch;
@@ -362,10 +376,15 @@ export class IpcServer {
362
376
  setAunMsgSender(sender) {
363
377
  this.aunMsgSender = sender;
364
378
  }
379
+ setDiagExecutor(executor) { this.diagExecutor = executor; }
380
+ setMenuClientExecutor(executor) { this.menuClientExecutor = executor; }
365
381
  /** Inject internal handoff result return executor for `ec handoff return`. */
366
382
  setHandoffReturnExecutor(executor) {
367
383
  this.handoffReturnExecutor = executor;
368
384
  }
385
+ setHandoffRecoverReturnExecutor(executor) {
386
+ this.handoffRecoverReturnExecutor = executor;
387
+ }
369
388
  setHandoffStatusExecutor(executor) {
370
389
  this.handoffStatusExecutor = executor;
371
390
  }
@@ -807,6 +826,10 @@ export class IpcServer {
807
826
  };
808
827
  }
809
828
  switch (cmd.type) {
829
+ case 'diag':
830
+ return this.diagExecutor ? this.diagExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', message: '诊断入口未配置' };
831
+ case 'menu-client.request':
832
+ return this.menuClientExecutor ? this.menuClientExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', error: 'Managed Menu client is not configured' };
810
833
  case 'model.catalog': {
811
834
  if ((cmd.self !== undefined && typeof cmd.self !== 'string')
812
835
  || (cmd.baseagent !== undefined && !['claude', 'codex', 'ecagent', 'gemini'].includes(cmd.baseagent))) {
@@ -887,7 +910,9 @@ export class IpcServer {
887
910
  });
888
911
  }
889
912
  case 'remote-auth.pair-code': {
890
- const pair = this.remoteAccessService.getPairingCode(typeof cmd.authorizedBy === 'string' ? cmd.authorizedBy : undefined);
913
+ const requestedOwner = typeof cmd.authorizedBy === 'string' ? cmd.authorizedBy.trim() : undefined;
914
+ const authorizedBy = requestedOwner || this.remoteAccessOwnerProvider?.();
915
+ const pair = this.remoteAccessService.getPairingCode(authorizedBy);
891
916
  return { ok: true, ...pair };
892
917
  }
893
918
  case 'remote-auth.issue-local': {
@@ -1094,6 +1119,10 @@ export class IpcServer {
1094
1119
  return { ok: true, context };
1095
1120
  }
1096
1121
  case 'aun-msg-send': {
1122
+ if (cmd.handoffContext !== undefined && (typeof cmd.handoffContext !== 'string'
1123
+ || !cmd.handoffContext.trim() || cmd.handoffContext.length > 64 * 1024 || cmd.scope === 'group')) {
1124
+ return { ok: false, code: 'INVALID_HANDOFF_CONTEXT', error: 'Private handoff context must contain 1–65536 characters' };
1125
+ }
1097
1126
  if (!this.aunMsgSender)
1098
1127
  return { ok: false, error: 'aun msg sender not configured' };
1099
1128
  if (!cmd.aid || typeof cmd.aid !== 'string')
@@ -1148,6 +1177,7 @@ export class IpcServer {
1148
1177
  encrypt: typeof cmd.encrypt === 'boolean' ? cmd.encrypt : undefined,
1149
1178
  thread: typeof cmd.thread === 'string' ? cmd.thread : undefined,
1150
1179
  returnPolicy: cmd.returnPolicy === 'required' || cmd.returnPolicy === 'none' ? cmd.returnPolicy : undefined,
1180
+ handoffContext: cmd.handoffContext,
1151
1181
  originSessionId: typeof cmd.originSessionId === 'string' ? cmd.originSessionId : undefined,
1152
1182
  originMessageId: typeof cmd.originMessageId === 'string' ? cmd.originMessageId : undefined,
1153
1183
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
@@ -1177,6 +1207,28 @@ export class IpcServer {
1177
1207
  return { ok: false, error: e?.message || String(e) };
1178
1208
  }
1179
1209
  }
1210
+ case 'handoff-recover-return': {
1211
+ if (!this.handoffRecoverReturnExecutor)
1212
+ return { ok: false, error: 'handoff recovery not configured' };
1213
+ if (typeof cmd.sessionId !== 'string' || !cmd.sessionId)
1214
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'missing sessionId' };
1215
+ if (typeof cmd.handoffId !== 'string' || !cmd.handoffId)
1216
+ return { ok: false, code: 'INVALID_HANDOFF_ID', error: 'missing handoff_id' };
1217
+ if (typeof cmd.content !== 'string')
1218
+ return { ok: false, code: 'HANDOFF_RETURN_CONTENT_REQUIRED', error: 'missing content' };
1219
+ try {
1220
+ return await this.handoffRecoverReturnExecutor({
1221
+ sessionId: cmd.sessionId,
1222
+ handoffId: cmd.handoffId,
1223
+ content: cmd.content,
1224
+ delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
1225
+ delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
1226
+ });
1227
+ }
1228
+ catch (e) {
1229
+ return { ok: false, error: e?.message || String(e) };
1230
+ }
1231
+ }
1180
1232
  case 'handoff-status': {
1181
1233
  if (!this.handoffStatusExecutor)
1182
1234
  return { ok: false, code: 'HANDOFF_STATUS_UNAVAILABLE', error: 'handoff status not configured' };
@@ -12,7 +12,18 @@ const STATE_KEY = 'proactive';
12
12
  const TOOL_REPORT_INTERVAL = 10;
13
13
  const SEND_RECEIPT_RE = /["']?(?:message[_ -]?id|handoff[_ -]?id)["']?\s*[:=]\s*["']?([A-Za-z0-9][A-Za-z0-9._:-]*)/gi;
14
14
  const HANDOFF_TEXT_RECEIPT_RE = /\bhandoff\s+(h-[A-Za-z0-9._-]+)/i;
15
- function hasSuccessfulSendReceipt(result, isError) {
15
+ const RETURN_CONTENT_RE = /["']?(?:return[_ -]?content|returnContent)["']?\s*[:=]\s*["']?([^}\n,]*)/i;
16
+ function hasRequiredReturnFlag(toolInput) {
17
+ if (!toolInput || typeof toolInput !== 'object')
18
+ return false;
19
+ const input = toolInput;
20
+ const commandValues = ['command', 'cmd', 'script'].flatMap(key => {
21
+ const value = input[key];
22
+ return typeof value === 'string' ? [value] : [];
23
+ });
24
+ return commandValues.some(command => /(?:^|\s)--return(?:=|\s+)['"]?required(?:['"]?)(?=\s|$)/i.test(command));
25
+ }
26
+ function hasSuccessfulSendReceipt(result, isError, requiresBusinessReturn = false) {
16
27
  if (isError === true)
17
28
  return false;
18
29
  const hasReceiptValue = (value) => {
@@ -40,6 +51,41 @@ function hasSuccessfulSendReceipt(result, isError) {
40
51
  };
41
52
  if (hasExplicitFailure(result, new Set()))
42
53
  return false;
54
+ const isBusinessReturn = (value) => {
55
+ if (typeof value !== 'string')
56
+ return false;
57
+ const normalized = value.trim();
58
+ if (!normalized || /^(?:null|undefined|-)$/.test(normalized))
59
+ return false;
60
+ // Contact admission/refusal is a real reply, but it is not task completion.
61
+ if (/(?:当前不接受陌生人消息|可发送\s*\/request|contact\s+(?:required|request)|stranger message)/i.test(normalized))
62
+ return false;
63
+ return true;
64
+ };
65
+ const inspectBusinessReturn = (value, seen) => {
66
+ if (!value || typeof value !== 'object')
67
+ return false;
68
+ if (seen.has(value))
69
+ return false;
70
+ seen.add(value);
71
+ if (Array.isArray(value))
72
+ return value.some(item => inspectBusinessReturn(item, seen));
73
+ for (const [key, item] of Object.entries(value)) {
74
+ if ((key === 'return_content' || key === 'returnContent') && isBusinessReturn(item))
75
+ return true;
76
+ if (inspectBusinessReturn(item, seen))
77
+ return true;
78
+ }
79
+ return false;
80
+ };
81
+ if (requiresBusinessReturn) {
82
+ if (inspectBusinessReturn(result, new Set()))
83
+ return true;
84
+ const returnText = typeof result === 'string'
85
+ ? result.match(RETURN_CONTENT_RE)?.[1]
86
+ : undefined;
87
+ return isBusinessReturn(returnText);
88
+ }
43
89
  const inspect = (value, seen) => {
44
90
  if (!value || typeof value !== 'object')
45
91
  return false;
@@ -153,6 +199,11 @@ export const proactiveFlow = {
153
199
  // Codex only invokes this hook for local tool paths. Hosted tools stay
154
200
  // enabled and do not satisfy or clear the pending first-send gate.
155
201
  const isSendCommand = ctx.isSendCommand(toolName, toolInput);
202
+ if (!isSendCommand && ((state.firstSendRequired && !state.firstToolDone) || state.toolReportPending)) {
203
+ const sendFailure = ctx.describeSendCommandFailure?.(toolName, toolInput);
204
+ if (sendFailure)
205
+ return { block: true, policyCode: 'proactive_send_invalid', reason: sendFailure };
206
+ }
156
207
  if (state.firstSendRequired && !state.firstToolDone) {
157
208
  if (!isSendCommand) {
158
209
  const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
@@ -221,7 +272,13 @@ export const proactiveFlow = {
221
272
  return;
222
273
  if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
223
274
  return;
224
- if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error))
275
+ const requiresBusinessReturn = hasRequiredReturnFlag(ctx.toolInput)
276
+ && (typeof ctx.result === 'string'
277
+ ? HANDOFF_TEXT_RECEIPT_RE.test(ctx.result)
278
+ : !!ctx.result && typeof ctx.result === 'object'
279
+ && ('handoff_id' in ctx.result || 'handoffId' in ctx.result));
280
+ HANDOFF_TEXT_RECEIPT_RE.lastIndex = 0;
281
+ if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error, requiresBusinessReturn))
225
282
  return;
226
283
  if (state.firstSendRequired && !state.firstToolDone)
227
284
  state.firstToolDone = true;