evolcore 0.0.23 → 0.0.24

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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
package/dist/index.js CHANGED
@@ -100,6 +100,7 @@ import { MAIN_PACKAGE_NAME } from './product.js';
100
100
  import fs from 'fs';
101
101
  import crypto from 'crypto';
102
102
  import { fileURLToPath } from 'url';
103
+ import * as platform from './utils/cross-platform.js';
103
104
  const controlMenuDeduper = new MenuRequestDeduper();
104
105
  /** 出站 payload 摘要(用于 channel-out.log) */
105
106
  function summarizeOutboundPayload(payload) {
@@ -134,6 +135,7 @@ function summarizeOutboundPayload(payload) {
134
135
  break;
135
136
  case 'status.started':
136
137
  case 'status.progress':
138
+ case 'status.notice':
137
139
  case 'status.requires_action':
138
140
  case 'status.queued':
139
141
  case 'status.completed':
@@ -326,11 +328,21 @@ function seedUpgradeCheckTrigger(manager, owner) {
326
328
  const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
327
329
  const packageJsonPath = path.join(packageRoot, 'package.json');
328
330
  const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
331
+ // Resolve the launch argv at generation time: in a checkout this points the
332
+ // monitor at src/ through tsx, so a `npm run build` that wipes dist/ cannot
333
+ // take the watchdog down with it.
334
+ const monitorEntry = platform.resolveCliEntry(packageRoot);
335
+ const stageParent = path.join(resolvePaths().root, 'data', 'monitor-stage');
329
336
  fs.writeFileSync(scriptPath, `#!/usr/bin/env node
330
- const { existsSync, readFileSync } = require('node:fs');
337
+ const fs = require('node:fs');
338
+ const path = require('node:path');
339
+ const { existsSync, readFileSync } = fs;
331
340
  const { spawn } = require('node:child_process');
332
341
 
333
- const CLI_PATH = ${JSON.stringify(cliPath)};
342
+ const CLI_PATH = ${JSON.stringify(monitorEntry.entry)};
343
+ const CLI_ARGS = ${JSON.stringify(monitorEntry.args)};
344
+ const PACKAGE_ROOT = ${JSON.stringify(packageRoot)};
345
+ const STAGE_PARENT = ${JSON.stringify(stageParent)};
334
346
  const PACKAGE_JSON = ${JSON.stringify(packageJsonPath)};
335
347
  const PACKAGE_NAME = ${JSON.stringify(MAIN_PACKAGE_NAME)};
336
348
  const DEV_MODE = ${devMode ? 'true' : 'false'};
@@ -405,14 +417,60 @@ async function main() {
405
417
  return;
406
418
  }
407
419
 
420
+ // Installed mode: copy the monitor out of the package before it upgrades that
421
+ // same package. Mirrors prepareMonitorStage(); kept inline because this file
422
+ // is generated standalone CJS. Any failure degrades to launching in place —
423
+ // losing upgrade isolation beats losing the upgrade.
424
+ let launchArgs = CLI_ARGS;
425
+ const launchEnv = { ...process.env };
426
+ if (!DEV_MODE && STAGE_PARENT) {
427
+ try {
428
+ const stageDir = path.join(STAGE_PARENT, 'upgrade-' + process.pid + '-' + Date.now());
429
+ fs.mkdirSync(stageDir, { recursive: true });
430
+ fs.cpSync(path.join(PACKAGE_ROOT, 'dist'), path.join(stageDir, 'dist'), { recursive: true });
431
+ if (existsSync(PACKAGE_JSON)) fs.copyFileSync(PACKAGE_JSON, path.join(stageDir, 'package.json'));
432
+ const modulesSource = path.join(PACKAGE_ROOT, 'node_modules');
433
+ if (existsSync(modulesSource)) {
434
+ const staged = path.join(stageDir, 'node_modules');
435
+ try {
436
+ // junction works unelevated on Windows; dir symlink elsewhere.
437
+ fs.symlinkSync(modulesSource, staged, process.platform === 'win32' ? 'junction' : 'dir');
438
+ fs.readdirSync(staged);
439
+ } catch (linkError) {
440
+ fs.rmSync(staged, { recursive: true, force: true });
441
+ fs.cpSync(modulesSource, staged, { recursive: true });
442
+ }
443
+ }
444
+ const stagedEntry = path.join(stageDir, 'dist', 'cli', 'index.js');
445
+ if (existsSync(stagedEntry)) {
446
+ launchArgs = [stagedEntry];
447
+ launchEnv.EVOLCORE_TARGET_PACKAGE_ROOT = PACKAGE_ROOT;
448
+ launchEnv.EVOLCORE_MONITOR_STAGE = stageDir;
449
+ }
450
+ } catch (stageError) {
451
+ // Fall through with the in-place entry.
452
+ }
453
+ }
454
+
408
455
  try {
409
- const child = spawn(process.execPath, [CLI_PATH, 'restart-monitor'], {
456
+ const child = spawn(process.execPath, [...launchArgs, 'restart-monitor'], {
410
457
  detached: true,
411
458
  stdio: 'ignore',
412
459
  windowsHide: process.platform === 'win32',
413
- env: { ...process.env },
460
+ env: launchEnv,
414
461
  });
415
462
  child.unref();
463
+ const pid = child.pid;
464
+ // The daemon is still running here — the monitor is what stops it. So a
465
+ // monitor that dies on load costs the upgrade, not the service; still,
466
+ // report it accurately instead of claiming success.
467
+ await new Promise(resolve => setTimeout(resolve, 3000));
468
+ let alive = false;
469
+ try { process.kill(pid, 0); alive = true; } catch (e) { alive = false; }
470
+ if (!alive) {
471
+ emit('error', 'restart-monitor exited during startup; upgrade to ' + remoteVersion + ' not applied');
472
+ return;
473
+ }
416
474
  emit('success', 'evolcore upgrade available: ' + localVersion + ' -> ' + remoteVersion + '; restart-monitor started');
417
475
  } catch (error) {
418
476
  emit('error', 'failed to start restart-monitor: ' + (error && error.message ? error.message : String(error)));
@@ -1066,7 +1124,7 @@ async function main() {
1066
1124
  agentLoader.register(new GeminiAgentPlugin());
1067
1125
  agentLoader.register(new EcagentAgentPlugin());
1068
1126
  const creationErrors = [];
1069
- const agentInstances = agentLoader.createAll(agentRegistry, {
1127
+ const agentInstances = await agentLoader.createAll(agentRegistry, {
1070
1128
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
1071
1129
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
1072
1130
  },
@@ -1493,8 +1551,9 @@ async function main() {
1493
1551
  }
1494
1552
  if (definition.execution.effort && !actor.daemonPrivileged) {
1495
1553
  const baseagent = definition.execution.baseagent
1496
- ?? agentRegistry.get(definition.agentAid)?.baseagent
1497
- ?? 'claude';
1554
+ ?? agentRegistry.get(definition.agentAid)?.baseagent;
1555
+ if (!baseagent)
1556
+ throw new Error('cannot determine Trigger baseagent');
1498
1557
  const effortDecision = validateRuntimeStringFieldOverride({
1499
1558
  selfAid: definition.agentAid,
1500
1559
  role: actor.role,
@@ -2154,6 +2213,7 @@ async function main() {
2154
2213
  const pendingRouteContext = restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {};
2155
2214
  const replyContext = {
2156
2215
  ...(pendingRouteContext ?? {}),
2216
+ ...(pending.threadId ? { threadId: pending.threadId } : {}),
2157
2217
  ...(pending.rootId ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId } : {}),
2158
2218
  };
2159
2219
  const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
@@ -2165,10 +2225,23 @@ async function main() {
2165
2225
  ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
2166
2226
  replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
2167
2227
  });
2228
+ // restart-monitor 会在回滚/升级时写入 outcome:必须如实告诉发起方这次跑的是
2229
+ // 哪个版本,否则 agent 会以为自己的改动或升级已经生效。
2230
+ const restartNotice = pending.outcome === 'rolled-back'
2231
+ ? {
2232
+ text: `⚠️ 服务已重启,但运行的是上一个版本。${pending.outcomeReason ? `原因:${pending.outcomeReason}。` : ''}本次改动未生效,请检查后重试。`,
2233
+ subtype: 'restarted-rolled-back',
2234
+ }
2235
+ : pending.outcome === 'upgraded'
2236
+ ? {
2237
+ text: `✅ 服务重启成功!${pending.outcomeReason ? `${pending.outcomeReason}` : ''}`,
2238
+ subtype: 'restarted-upgraded',
2239
+ }
2240
+ : { text: '✅ 服务重启成功!', subtype: 'restarted' };
2168
2241
  await sendSystemPayload(adapter, envelope, {
2169
2242
  kind: 'system.notice',
2170
- text: '✅ 服务重启成功!',
2171
- subtype: 'restarted',
2243
+ text: restartNotice.text,
2244
+ subtype: restartNotice.subtype,
2172
2245
  });
2173
2246
  pendingRestartNoticeSent = true;
2174
2247
  fs.rmSync(pendingFile, { force: true });
@@ -2658,9 +2731,18 @@ async function main() {
2658
2731
  continue;
2659
2732
  }
2660
2733
  const evolName = owningAgent.aid;
2661
- const baseagentName = session.baseagent || primaryBaseagent;
2662
- const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
2734
+ const baseagentName = typeof session.baseagent === 'string' ? session.baseagent.trim() : '';
2735
+ if (!baseagentName) {
2736
+ logger.error(`[Resume] session ${session.id}: missing baseagent binding, refusing implicit runner fallback`);
2737
+ sessionManager.clearProcessing(session.id);
2738
+ continue;
2739
+ }
2740
+ // Recovery must resume on the exact Agent × baseagent runner recorded by
2741
+ // the Session. Never fall back to the primary runner: doing so can bind
2742
+ // an in-flight session to a different backend.
2743
+ const agent = agentMap.get(`${evolName}::${baseagentName}`);
2663
2744
  if (!agent) {
2745
+ logger.error(`[Resume] session ${session.id}: runner unavailable for ${evolName}::${baseagentName}; refusing fallback`);
2664
2746
  sessionManager.clearProcessing(session.id);
2665
2747
  continue;
2666
2748
  }
@@ -2727,11 +2809,12 @@ async function main() {
2727
2809
  return owner?.aid === aid;
2728
2810
  });
2729
2811
  const welcome = aunInstance?.channel;
2730
- const ownerAid = getFirstStaticAgentOwner(aid);
2731
2812
  const owningAgent = agentRegistry.get(aid);
2732
- const sessions = aunInstance && ownerAid && owningAgent
2813
+ const firstOwnerAid = getFirstStaticAgentOwner(aid);
2814
+ const sessions = aunInstance && firstOwnerAid && owningAgent
2733
2815
  ? {
2734
- ensurePostBootstrapMainSession: async () => {
2816
+ ensurePostBootstrapMainSession: async (owner) => {
2817
+ const ownerAid = owner || firstOwnerAid;
2735
2818
  const channel = aunInstance.adapter.channelName;
2736
2819
  const active = await sessionManager.getActiveSession(channel, ownerAid);
2737
2820
  const ensured = await sessionManager.ensureNewMainSessionForOperation({
@@ -2739,7 +2822,7 @@ async function main() {
2739
2822
  channelId: ownerAid,
2740
2823
  projectPath: active?.projectPath || owningAgent.projectPath || defaultProjectPath,
2741
2824
  baseagent: owningAgent.baseagent || active?.baseagent || primaryBaseagent,
2742
- operationId: postBootstrapWelcomeOperationId(aid),
2825
+ operationId: postBootstrapWelcomeOperationId(aid, ownerAid),
2743
2826
  identityMetadata: {
2744
2827
  ...(active?.metadata ?? {}),
2745
2828
  channelKey: aunInstance.adapter.channelKey,
@@ -2851,15 +2934,18 @@ async function main() {
2851
2934
  for (const runner of new Set(agentMap.values())) {
2852
2935
  if (typeof runner?.evaluatePreToolUse !== 'function')
2853
2936
  continue;
2854
- const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId);
2937
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId, params.sessionToken);
2855
2938
  if (result.applicable)
2856
2939
  return result;
2857
2940
  }
2941
+ // A credential that no runner issued is forged or belongs to a thread
2942
+ // whose rollout is gone. Never allow it; uncredentialed calls were
2943
+ // already allowed by the IPC layer before reaching this executor.
2858
2944
  return {
2859
2945
  ok: false,
2860
2946
  applicable: true,
2861
2947
  decision: 'deny',
2862
- reason: 'No active EvolCore Codex runner accepted this managed hook request',
2948
+ reason: 'No active EvolCore Codex runner accepted this managed hook credential',
2863
2949
  };
2864
2950
  });
2865
2951
  const resolveManagedHandoffScope = async (params, operation) => {
@@ -3192,7 +3278,7 @@ async function main() {
3192
3278
  const creationErrors = [];
3193
3279
  const staged = candidate.config.enabled === false
3194
3280
  ? []
3195
- : agentLoader.createForAgent(candidate, {
3281
+ : await agentLoader.createForAgent(candidate, {
3196
3282
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
3197
3283
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
3198
3284
  },
@@ -3292,7 +3378,7 @@ async function main() {
3292
3378
  throw new Error(agentRuntimeError);
3293
3379
  }
3294
3380
  const creationErrors = [];
3295
- const newAgentInstances = agentLoader.createForAgent(agent, {
3381
+ const newAgentInstances = await agentLoader.createForAgent(agent, {
3296
3382
  onSessionIdUpdate: async (sessionId, agentSessionId, context) => {
3297
3383
  return await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId, context?.turn);
3298
3384
  },
@@ -3483,7 +3569,7 @@ async function main() {
3483
3569
  hotLoad: coreHotLoadAgent,
3484
3570
  resync: coreResyncAgents,
3485
3571
  create: async (options) => {
3486
- const { agentCreateNonInteractive } = await import('./cli/agent.js');
3572
+ const { agentCreateNonInteractive } = await import('./cli/agent-create.js');
3487
3573
  return await agentCreateNonInteractive(options, { hotLoadAgent: coreHotLoadAgent });
3488
3574
  },
3489
3575
  aunAids: () => channelInstances.flatMap((inst) => {
@@ -3732,12 +3818,20 @@ async function main() {
3732
3818
  const aid = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
3733
3819
  if (!aid)
3734
3820
  return fail('INVALID_ARGS', 'ec agent new requires an AID');
3735
- const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan } = await import('./cli/agent.js');
3821
+ const { agentCreateNonInteractive, resolveAgentCreatePlan, validateAgentCreatePlan, parseAgentOwnerArgs } = await import('./cli/agent-create.js');
3822
+ let ownerOptions;
3823
+ try {
3824
+ ownerOptions = parseAgentOwnerArgs(argv);
3825
+ }
3826
+ catch (error) {
3827
+ return fail('INVALID_ARGS', error instanceof Error ? error.message : String(error));
3828
+ }
3736
3829
  const plan = resolveAgentCreatePlan({
3737
3830
  aid,
3738
3831
  project: value('--project'),
3739
3832
  baseagent: value('--baseagent'),
3740
- owner: value('--owner'),
3833
+ ...ownerOptions,
3834
+ handoff: value('--handoff'),
3741
3835
  name: value('--name'),
3742
3836
  description: value('--description'),
3743
3837
  force: argv.includes('--force'),
@@ -3745,23 +3839,75 @@ async function main() {
3745
3839
  const validationError = validateAgentCreatePlan(plan);
3746
3840
  if (validationError)
3747
3841
  return fail('INVALID_ARGS', validationError);
3842
+ if (argv.includes('--handoff')) {
3843
+ if (!delegation.grant.messageId)
3844
+ return fail('INVALID_ARGS', '--handoff requires an originating message');
3845
+ if (!plan.handoff?.trim())
3846
+ return fail('INVALID_ARGS', '--handoff requires non-empty context');
3847
+ if ((plan.owners?.[0] ?? plan.owner) !== taskAid) {
3848
+ return fail('INVALID_ARGS', '--handoff requires the creating Agent to be the first Owner');
3849
+ }
3850
+ if (plan.configExists)
3851
+ return fail('INVALID_ARGS', '--handoff requires a new Agent');
3852
+ }
3748
3853
  if (argv.includes('--dry-run'))
3749
3854
  return { ok: true, result: { ok: true, dryRun: true, plan } };
3855
+ let handoffId;
3856
+ if (plan.handoff) {
3857
+ try {
3858
+ const created = await handoffRuntime.createOutbound({
3859
+ selfAid: taskAid,
3860
+ to: aid,
3861
+ originSessionId: sessionId,
3862
+ originMessageId: delegation.grant.messageId,
3863
+ payload: { type: 'text', text: plan.handoff },
3864
+ encrypt: true,
3865
+ targetChatType: 'private',
3866
+ explicitReturnPolicy: 'required',
3867
+ contextOnly: true,
3868
+ originAuthorization: {
3869
+ actorId: delegation.grant.actorId,
3870
+ channelKey: delegation.grant.channel,
3871
+ channelType: delegation.grant.channelType,
3872
+ chatType: delegation.grant.chatType,
3873
+ peerKey: delegation.grant.peerKey,
3874
+ },
3875
+ causation: responseEngine.getTaskRuntimeContext(sessionId)?.causation,
3876
+ });
3877
+ handoffId = created.handoff?.handoff_id;
3878
+ if (!handoffId)
3879
+ return fail('INVALID_ARGS', '--handoff requires a different target session');
3880
+ }
3881
+ catch (error) {
3882
+ return fail('HANDOFF_PREPARE_FAILED', error instanceof Error ? error.message : String(error));
3883
+ }
3884
+ }
3750
3885
  const createOptions = {
3751
3886
  aid: plan.aid,
3752
3887
  project: plan.project,
3753
3888
  baseagent: plan.baseagent,
3754
3889
  owner: plan.owner,
3890
+ owners: plan.owners,
3755
3891
  name: plan.name,
3756
3892
  description: plan.description,
3757
3893
  force: argv.includes('--force'),
3758
3894
  };
3759
- const result = agentApplicationService
3760
- ? await agentApplicationService.create(createOptions)
3761
- : await agentCreateNonInteractive(createOptions);
3762
- return !result.ok
3763
- ? fail(result.code ?? 'INTERNAL', result.error)
3764
- : { ok: true, result: { ...result, configPath: '[redacted]' } };
3895
+ try {
3896
+ const result = agentApplicationService
3897
+ ? await agentApplicationService.create(createOptions)
3898
+ : await agentCreateNonInteractive(createOptions);
3899
+ if (!result.ok) {
3900
+ if (handoffId)
3901
+ handoffRuntime.store.discard(taskAid, handoffId, 'operation_failed');
3902
+ return fail(result.code ?? 'INTERNAL', result.error);
3903
+ }
3904
+ return { ok: true, result: { ...result, configPath: '[redacted]', ...(handoffId ? { handoff_id: handoffId } : {}) } };
3905
+ }
3906
+ catch (error) {
3907
+ if (handoffId)
3908
+ handoffRuntime.store.discard(taskAid, handoffId, 'operation_failed');
3909
+ return fail('INTERNAL', error instanceof Error ? error.message : String(error));
3910
+ }
3765
3911
  }
3766
3912
  if (operation === 'agent.delete') {
3767
3913
  const result = agentApplicationService
package/dist/ipc.js CHANGED
@@ -830,12 +830,21 @@ export class IpcServer {
830
830
  || typeof cmd.toolName !== 'string' || !cmd.toolName) {
831
831
  return { ok: false, applicable: true, decision: 'deny', reason: 'invalid Codex hook request' };
832
832
  }
833
+ // Unmanaged Codex processes carry no credential and are allowed
834
+ // without consulting any runner; see bin/codex-managed-hook.mjs.
835
+ const sessionToken = typeof cmd.sessionToken === 'string' && cmd.sessionToken
836
+ ? cmd.sessionToken
837
+ : undefined;
838
+ if (!sessionToken) {
839
+ return { ok: true, applicable: true, decision: 'allow' };
840
+ }
833
841
  try {
834
842
  return await this.codexPreToolUseExecutor({
835
843
  threadId: cmd.threadId,
836
844
  toolName: cmd.toolName,
837
845
  toolInput: cmd.toolInput,
838
846
  callId: typeof cmd.callId === 'string' ? cmd.callId : undefined,
847
+ sessionToken,
839
848
  signal: requestSignal,
840
849
  });
841
850
  }
@@ -1534,12 +1543,26 @@ export class IpcServer {
1534
1543
  && crypto.timingSafeEqual(provided, this.controlToken);
1535
1544
  }
1536
1545
  }
1537
- /**
1538
- * Query the running EvolCore daemon via Unix socket.
1539
- * Returns null if the service is not running or the socket is unreachable.
1540
- */
1546
+ export class IpcTransportError extends Error {
1547
+ code;
1548
+ endpoint;
1549
+ constructor(code, endpoint) {
1550
+ const hint = code === 'EPERM' || code === 'EACCES'
1551
+ ? '权限被拒绝;Windows 下请检查 daemon 与 CLI 是否使用相同用户和提升权限级别。'
1552
+ : code === 'ETIMEDOUT'
1553
+ ? '等待 daemon 响应超时;请求可能已执行,请先检查状态再重试。'
1554
+ : code === 'ENOENT' || code === 'ECONNREFUSED'
1555
+ ? 'daemon 未运行或 IPC 端点不存在,请先执行 ec start,再重试。'
1556
+ : '请检查 daemon 进程及 IPC 端点状态。';
1557
+ super(`daemon IPC 失败 (${code}): ${endpoint}。${hint}`);
1558
+ this.code = code;
1559
+ this.endpoint = endpoint;
1560
+ this.name = 'IpcTransportError';
1561
+ }
1562
+ }
1563
+ /** Query the daemon; opt in to transport errors instead of the legacy null result. */
1541
1564
  /** Pass `null` as timeoutMs to wait without a client-side deadline. */
1542
- export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1565
+ export async function ipcQuery(socketPath, cmd, timeoutMs = 3000, options = {}) {
1543
1566
  const endpoint = normalizeIpcEndpoint(socketPath);
1544
1567
  let socketCandidates = [endpoint];
1545
1568
  if (isNamedPipe(endpoint)) {
@@ -1547,22 +1570,35 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1547
1570
  if (endpoint === paths.socket)
1548
1571
  socketCandidates = getWindowsInstanceSocketPathCandidates(paths.root);
1549
1572
  }
1550
- if (socketCandidates.length <= 1)
1551
- return ipcQueryOnce(endpoint, cmd, timeoutMs);
1552
- return (async () => {
1553
- const startedAt = Date.now();
1554
- for (const candidate of socketCandidates) {
1555
- const remainingTimeout = timeoutMs === null
1556
- ? null
1557
- : Math.max(0, timeoutMs - (Date.now() - startedAt));
1558
- if (remainingTimeout === 0)
1559
- return null;
1573
+ const startedAt = Date.now();
1574
+ let failure;
1575
+ for (const candidate of socketCandidates) {
1576
+ const remainingTimeout = timeoutMs === null
1577
+ ? null
1578
+ : Math.max(0, timeoutMs - (Date.now() - startedAt));
1579
+ if (remainingTimeout === 0) {
1580
+ failure ??= new IpcTransportError('ETIMEDOUT', candidate);
1581
+ break;
1582
+ }
1583
+ try {
1560
1584
  const response = await ipcQueryOnce(candidate, cmd, remainingTimeout);
1561
1585
  if (response !== null)
1562
1586
  return response;
1563
1587
  }
1564
- return null;
1565
- })();
1588
+ catch (error) {
1589
+ if (!(error instanceof IpcTransportError))
1590
+ throw error;
1591
+ // A missing legacy pipe must not hide a permission failure on the canonical pipe.
1592
+ if (!failure || ['ENOENT', 'ECONNREFUSED'].includes(failure.code))
1593
+ failure = error;
1594
+ // Only connection failures are safe to retry against a legacy endpoint.
1595
+ if (!['ENOENT', 'ECONNREFUSED', 'EPERM', 'EACCES'].includes(error.code))
1596
+ break;
1597
+ }
1598
+ }
1599
+ if (options.throwOnError && failure)
1600
+ throw failure;
1601
+ return null;
1566
1602
  }
1567
1603
  function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1568
1604
  let authenticatedCmd = cmd;
@@ -1587,7 +1623,7 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1587
1623
  ? { daemonRuntimeEpoch: process.env[DAEMON_RUNTIME_EPOCH_ENV] }
1588
1624
  : {}),
1589
1625
  };
1590
- return new Promise((resolve) => {
1626
+ return new Promise((resolve, reject) => {
1591
1627
  const startedAtMono = performance.now();
1592
1628
  const queryId = nextIpcQueryId(request);
1593
1629
  const commandType = ipcLogToken(request.type);
@@ -1606,7 +1642,7 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1606
1642
  settled = true;
1607
1643
  logPhase('timeout', ` timeout_ms=${timeoutMs}`);
1608
1644
  conn.destroy();
1609
- resolve(null);
1645
+ reject(new IpcTransportError('ETIMEDOUT', socketPath));
1610
1646
  }, timeoutMs);
1611
1647
  conn.on('connect', () => {
1612
1648
  if (settled)
@@ -1632,7 +1668,10 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1632
1668
  parse = 'invalid_json';
1633
1669
  }
1634
1670
  logPhase('response', ` bytes=${idx + 1} parse=${parse}`);
1635
- resolve(response);
1671
+ if (parse === 'invalid_json')
1672
+ reject(new IpcTransportError('EPROTO', socketPath));
1673
+ else
1674
+ resolve(response);
1636
1675
  conn.destroy();
1637
1676
  }
1638
1677
  });
@@ -1643,7 +1682,15 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1643
1682
  if (timer)
1644
1683
  clearTimeout(timer);
1645
1684
  logPhase('error', ` code=${error.code ?? 'unknown'}`);
1646
- resolve(null);
1685
+ reject(new IpcTransportError(error.code ?? 'UNKNOWN', socketPath));
1686
+ });
1687
+ conn.on('close', () => {
1688
+ if (settled)
1689
+ return;
1690
+ settled = true;
1691
+ if (timer)
1692
+ clearTimeout(timer);
1693
+ reject(new IpcTransportError('ECONNRESET', socketPath));
1647
1694
  });
1648
1695
  });
1649
1696
  }
package/dist/paths.js CHANGED
@@ -129,11 +129,22 @@ export function agentHandoffDir(aid) {
129
129
  return path.join(agentDataDir(aid), 'handoff');
130
130
  }
131
131
  export function agentOutboxPath(aid) {
132
- return path.join(agentDataDir(aid), 'outbox.jsonl');
132
+ return path.join(resolvePaths().outboxDir, encodeURIComponent(aid), 'outbox.jsonl');
133
133
  }
134
134
  export function agentActivityOutboxPath(aid) {
135
+ return path.join(resolvePaths().outboxDir, encodeURIComponent(aid), 'activity-outbox.jsonl');
136
+ }
137
+ /** Pre-v4 per-Agent outbox paths retained only for compatibility import. */
138
+ export function legacyAgentOutboxPath(aid) {
139
+ return path.join(agentDataDir(aid), 'outbox.jsonl');
140
+ }
141
+ export function legacyAgentActivityOutboxPath(aid) {
135
142
  return path.join(agentDataDir(aid), 'activity-outbox.jsonl');
136
143
  }
144
+ /** Earliest global outbox layout: data/outbox/<aid>.jsonl. */
145
+ export function legacyFlatOutboxPath(aid) {
146
+ return path.join(resolvePaths().outboxDir, `${aid}.jsonl`);
147
+ }
137
148
  /** Directory-safe channel instance key used by connector runtime state. */
138
149
  function encodePathKey(value) {
139
150
  return encodeURIComponent(value);
@@ -283,6 +294,17 @@ function migrateFromAun() {
283
294
  const markerPath = path.join(newRoot, MIGRATION_MARKER);
284
295
  if (fs.existsSync(markerPath))
285
296
  return;
297
+ // Test workers use isolated temporary homes. Copying the developer's
298
+ // machine-wide ~/.aun tree into every fresh fixture is both unnecessary and
299
+ // can make process-boundary tests exceed their timeout. Production roots
300
+ // retain the normal one-time migration behavior.
301
+ if (process.env.EVOLCORE_TEST === '1') {
302
+ try {
303
+ fs.writeFileSync(markerPath, new Date().toISOString(), 'utf-8');
304
+ }
305
+ catch { }
306
+ return;
307
+ }
286
308
  const oldRoot = path.join(os.homedir(), '.aun');
287
309
  if (!fs.existsSync(oldRoot) || oldRoot === newRoot) {
288
310
  try {
@@ -1,11 +1,13 @@
1
1
  /**
2
- * billing.ts — 读 model-prices.jsonl,按 billing_fn 调对应算法函数计算费用。
2
+ * billing.ts — 读 model-prices.jsonl,按模型参数中的 billing_fn 计算费用。
3
3
  * 费用查询时实时计算,不存入 DB(价格可能变动)。
4
4
  */
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { getPackageRoot } from '../paths.js';
8
- // 价格表缓存(进程内,5 分钟 TTL)
8
+ // 模型参数缓存(进程内,5 分钟 TTL)
9
+ let _parameterCache = null;
10
+ let _parameterCacheTs = 0;
9
11
  let _priceCache = null;
10
12
  let _priceCacheTs = 0;
11
13
  const PRICE_CACHE_TTL = 5 * 60 * 1000;
@@ -44,6 +46,14 @@ function _loadJsonlMerged(evolcoreHome, filename) {
44
46
  }
45
47
  return results;
46
48
  }
49
+ function loadModelParameters(evolcoreHome) {
50
+ if (_parameterCache && Date.now() - _parameterCacheTs < PRICE_CACHE_TTL)
51
+ return _parameterCache;
52
+ const params = _loadJsonlMerged(evolcoreHome, 'model-prices.jsonl');
53
+ _parameterCache = params;
54
+ _parameterCacheTs = Date.now();
55
+ return params;
56
+ }
47
57
  let _aliasCache = null;
48
58
  let _aliasCacheTs = 0;
49
59
  function loadAliases(evolcoreHome) {
@@ -64,7 +74,8 @@ function loadPrices(evolcoreHome) {
64
74
  const now = Date.now();
65
75
  if (_priceCache && now - _priceCacheTs < PRICE_CACHE_TTL)
66
76
  return _priceCache;
67
- _priceCache = _loadJsonlMerged(evolcoreHome, 'model-prices.jsonl');
77
+ _priceCache = loadModelParameters(evolcoreHome).filter((record) => (typeof record.billing_fn === 'string'
78
+ && (record.currency === 'USD' || record.currency === 'CNY')));
68
79
  _priceCacheTs = now;
69
80
  return _priceCache;
70
81
  }
@@ -140,13 +151,16 @@ export function calcCost(evolcoreHome, event) {
140
151
  return {};
141
152
  return fn(event, priceRow);
142
153
  }
154
+ // ── Model Specs ──────────────────────────────────────────────────────────────
143
155
  let _specCache = null;
144
156
  let _specCacheTs = 0;
145
157
  function loadSpecs(evolcoreHome) {
146
158
  const now = Date.now();
147
159
  if (_specCache && now - _specCacheTs < PRICE_CACHE_TTL)
148
160
  return _specCache;
149
- _specCache = _loadJsonlMerged(evolcoreHome, 'model-specs.jsonl');
161
+ _specCache = loadModelParameters(evolcoreHome).filter((record) => (typeof record.context_window === 'number'
162
+ && typeof record.max_input_tokens === 'number'
163
+ && typeof record.max_output_tokens === 'number'));
150
164
  _specCacheTs = now;
151
165
  return _specCache;
152
166
  }
@@ -69,8 +69,32 @@ export function atomicWrite(filePath, content) {
69
69
  }
70
70
  }
71
71
  // The final rename atomically replaces the old target without an absent-path
72
- // window.
73
- fs.renameSync(tmp, filePath);
72
+ // window. Windows H-class projections can keep the destination pathname
73
+ // open while a sandbox is starting; replacing that directory entry then
74
+ // fails with EPERM. In that narrow case, preserve the target inode and write
75
+ // in place. The hot backup above still provides the previous full version.
76
+ try {
77
+ fs.renameSync(tmp, filePath);
78
+ }
79
+ catch (error) {
80
+ const code = error.code;
81
+ if (process.platform !== 'win32' || code !== 'EPERM')
82
+ throw error;
83
+ const detachedHot = `${hot}.${process.pid}.fallback.tmp`;
84
+ try {
85
+ // refreshHotBackup may have used a hard link. Detach that backup before
86
+ // the in-place write, otherwise updating the live inode would update the
87
+ // hot backup as well.
88
+ fs.copyFileSync(filePath, detachedHot);
89
+ fs.rmSync(hot, { force: true });
90
+ fs.renameSync(detachedHot, hot);
91
+ stableWrite(filePath, content);
92
+ }
93
+ finally {
94
+ fs.rmSync(detachedHot, { force: true });
95
+ fs.rmSync(tmp, { force: true });
96
+ }
97
+ }
74
98
  }
75
99
  export function atomicWriteJson(filePath, value) {
76
100
  atomicWrite(filePath, JSON.stringify(value, null, 2) + '\n');