evolcore 0.0.22 → 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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  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 +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -1,10 +1,8 @@
1
1
  import { normalizePeer, writeScope } from '../model/config-scope.js';
2
2
  import { formatPeerKey } from '../relation/peer-identity.js';
3
- import { modelMatches } from '../model/model-catalog.js';
3
+ import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
4
4
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
5
5
  import { hasModelSwitcher } from '../../agents/runner-types.js';
6
- import { getCodexEfforts } from '../../agents/codex-runner.js';
7
- import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
8
6
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
9
7
  import { inspectDataMigrationRequirement } from '../data-migration.js';
10
8
  import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
@@ -12,11 +10,12 @@ import path from 'path';
12
10
  import fs from 'fs';
13
11
  import os from 'os';
14
12
  import crypto from 'crypto';
15
- import { execFileSync } from 'child_process';
16
13
  import { CronExpressionParser } from 'cron-parser';
17
14
  import { parseDuration } from '../../trigger/parser.js';
18
15
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
19
- import { commandExists, spawnDetachedNode } from '../../utils/cross-platform.js';
16
+ import { commandExists, resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
17
+ import { cachedCliVersion } from '../../utils/cross-platform.js';
18
+ import { scanInstances } from '../../utils/instance-registry.js';
20
19
  import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
21
20
  import { WEB_PACKAGE_NAME } from '../../product.js';
22
21
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
@@ -40,6 +39,9 @@ import { logger } from '../../utils/logger.js';
40
39
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
41
40
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
42
41
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
42
+ import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
43
+ import { validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
44
+ import { listDiagnosticLogOptions, listRawConfigResources, queryDiagnosticLogs, readRawConfigResource, } from './daemon-diagnostics.js';
43
45
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
44
46
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
45
47
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
@@ -50,34 +52,9 @@ import { probeSessionActivity } from './slash-gate.js';
50
52
  * 失败返回 null(命令不存在或执行失败)。
51
53
  */
52
54
  function getBaseagentVersion(cmd) {
53
- try {
54
- const executable = cmd === 'codex' ? resolveCodexCliPath() : cmd;
55
- if (!executable)
56
- return null;
57
- const output = cmd === 'codex'
58
- ? execCodexCliSync(['--version'], {
59
- encoding: 'utf-8',
60
- timeout: 3000,
61
- stdio: ['ignore', 'pipe', 'pipe'],
62
- })
63
- : execFileSync(executable, ['--version'], {
64
- encoding: 'utf-8',
65
- timeout: 3000,
66
- stdio: ['ignore', 'pipe', 'pipe'],
67
- windowsHide: process.platform === 'win32',
68
- });
69
- const versionOutput = String(output).trim();
70
- // claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
71
- // gemini: "0.38.0" → 直接返回
72
- // codex: "codex-cli 0.142.0" → 提取 "0.142.0"
73
- const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
74
- return match ? match[1] : null;
75
- }
76
- catch {
77
- return null;
78
- }
55
+ return cachedCliVersion(cmd);
79
56
  }
80
- const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
57
+ const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
81
58
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
82
59
  /** menu file: fetch 文件大小上限(与 /file 一致) */
83
60
  const FILE_FETCH_MAX_SIZE = 10 * 1024 * 1024;
@@ -394,14 +371,15 @@ function tokenizeArgv(line) {
394
371
  }
395
372
  return out;
396
373
  }
397
- function getAvailableEfforts(agent, model) {
398
- if (agent.name === 'claude') {
399
- return allEfforts;
400
- }
401
- if (agent.name === 'codex') {
402
- return getCodexEfforts(model);
403
- }
404
- return [];
374
+ async function getAvailableEfforts(agent, model) {
375
+ const dynamic = await agent.getAvailableEfforts?.(model);
376
+ if (Array.isArray(dynamic))
377
+ return dynamic;
378
+ // Keep Menu usable with legacy/embedded runner implementations that do not
379
+ // expose the optional catalog method. Full runners still take the dynamic
380
+ // path above, so this fallback cannot broaden their advertised capabilities.
381
+ const baseagent = agent?.baseagent || agent?.name;
382
+ return (TRANSPORT_EFFORTS[baseagent] ?? []);
405
383
  }
406
384
  function modelDisplayLabel(agent, model) {
407
385
  const full = agent.resolveModelId?.(model);
@@ -414,6 +392,29 @@ function menuStringArg(args, key) {
414
392
  const trimmed = value.trim();
415
393
  return trimmed || undefined;
416
394
  }
395
+ function hostCatalogArgs(args = {}) {
396
+ const invalid = (message) => { throw { code: 'INVALID_ARGUMENT', message }; };
397
+ if (!args || typeof args !== 'object' || Array.isArray(args))
398
+ invalid('Baseagent catalog args must be an object');
399
+ if (Object.keys(args).some(key => !['baseagent', 'ba', 'refresh', 'scope', 'context'].includes(key))) {
400
+ invalid('Baseagent catalog only accepts baseagent, ba, refresh and process context');
401
+ }
402
+ if (args.scope !== undefined && args.scope !== 'process')
403
+ invalid('Baseagent catalog requires process scope');
404
+ if (args.context !== undefined && (!args.context || typeof args.context !== 'object'
405
+ || Array.isArray(args.context) || args.context.kind !== 'process'
406
+ || Object.keys(args.context).some(key => key !== 'kind'))) {
407
+ invalid('Baseagent catalog requires process context');
408
+ }
409
+ for (const key of ['baseagent', 'ba']) {
410
+ if (args[key] !== undefined && (typeof args[key] !== 'string' || !args[key].trim())) {
411
+ invalid(`${key} must be a non-empty string`);
412
+ }
413
+ }
414
+ if (args.refresh !== undefined && typeof args.refresh !== 'boolean')
415
+ invalid('refresh must be a boolean');
416
+ return { baseagent: menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba') ?? 'claude', refresh: args.refresh === true };
417
+ }
417
418
  const LEGACY_SESSION_CONFIG_ARG_KEYS = [
418
419
  'session',
419
420
  'sessionId',
@@ -856,6 +857,8 @@ export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
856
857
  /** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
857
858
  * /system 全部进程级;/agent 仅 create/delete/enable/disable 进程级;其余关系级。 */
858
859
  export function isProcessLevelAction(cmdBase, action) {
860
+ if (cmdBase === '/baseagent-models')
861
+ return true;
859
862
  if (cmdBase === '/system')
860
863
  return true;
861
864
  if (cmdBase === '/gateway')
@@ -904,6 +907,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
904
907
  });
905
908
  const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
906
909
  if (verb === 'query') {
910
+ if (cmdBase === '/baseagent-models')
911
+ return intent('baseagent.models.list', 'process');
907
912
  if (cmdBase === '/pwd')
908
913
  return intent('project.current', 'relation');
909
914
  if (cmdBase === '/session' || cmdBase === '/s')
@@ -943,6 +948,10 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
943
948
  return intent('trigger.list', 'relation');
944
949
  }
945
950
  if (verb === 'options') {
951
+ if (cmdBase === '/system')
952
+ return intent('system.status', 'process');
953
+ if (cmdBase === '/config')
954
+ return intent('config.read', args?.scope === 'agent' ? 'agent' : 'process');
946
955
  if (cmdBase === '/agent')
947
956
  return intent(fromControlChannel ? 'agent.list' : 'agent.show', fromControlChannel ? 'control' : 'agent');
948
957
  if (cmdBase === '/capability')
@@ -1107,6 +1116,7 @@ async function authorizeMenuIntent(params) {
1107
1116
  intent.source = source;
1108
1117
  const decision = await authorizeOperation({ source, intent, subject });
1109
1118
  if (!decision.allow) {
1119
+ logger.warn(`[MenuAuth] denied operation=${intent.operation} scope=${intent.scope} source=${source} selfAid=${subject.selfAid ?? '<none>'} actor=${subject.actorId ?? '<none>'} role=${subject.role} processRole=${subject.processRole} dataScope=${subject.dataScope} daemonOwner=${subject.isDaemonOwner} control=${subject.fromControlChannel} reason=${decision.reason}`);
1110
1120
  if (intent.operation.startsWith('role.') || intent.operation.startsWith('connect.')) {
1111
1121
  const namespace = intent.operation.split('.', 1)[0];
1112
1122
  return {
@@ -1284,8 +1294,14 @@ function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, requir
1284
1294
  : { channel: fallbackChannel };
1285
1295
  }
1286
1296
  const agent = owner.agentRegistry?.get?.(requestedAid) ?? null;
1287
- if (!agent)
1297
+ if (!agent) {
1298
+ // The daemon trigger owner is a virtual control identity and therefore is
1299
+ // intentionally absent from the normal Agent registry.
1300
+ const daemonAid = owner.getDaemonTriggerOwnerAid?.();
1301
+ if (daemonAid && requestedAid === daemonAid)
1302
+ return { channel: 'daemon' };
1288
1303
  return { error: `未找到 Agent: ${requestedAid}`, code: 'NOT_FOUND' };
1304
+ }
1289
1305
  const channel = agent.channelInstanceNames?.()[0];
1290
1306
  if (!channel)
1291
1307
  return { error: `Agent 无可用 channel: ${requestedAid}`, code: 'NOT_FOUND' };
@@ -1471,6 +1487,9 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1471
1487
  ...(isControlScope && canManageProcess && canOperation('system.restart', 'process') ? [
1472
1488
  { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1473
1489
  ] : []),
1490
+ ...(isControlScope && canManageProcess && canOperation('system.status', 'process') ? [
1491
+ { cmd: '/system', label: 'Daemon 诊断', desc: '查询 daemon 状态、监控快照和日志' },
1492
+ ] : []),
1474
1493
  ...(canUseFile ? [
1475
1494
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1476
1495
  ] : []),
@@ -1501,6 +1520,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1501
1520
  const gated0 = gateControlScope.call(this, { cmdBase: cmdBase0, args, channel, fromControlChannel });
1502
1521
  if (gated0)
1503
1522
  throw { code: gated0.code, message: gated0.error };
1523
+ if (cmdBase0 === '/baseagent-models')
1524
+ throw { code: 'NOT_SUPPORTED', message: 'Baseagent catalog only supports menu.query' };
1504
1525
  if (cmdBase0 === '/role') {
1505
1526
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1506
1527
  const authorized = await authorizeRoleMenu(this, {
@@ -1567,6 +1588,18 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1567
1588
  });
1568
1589
  if (optionsAuthDenied)
1569
1590
  throw { code: optionsAuthDenied.code, message: optionsAuthDenied.error, data: optionsAuthDenied.data };
1591
+ if (cmdBase0 === '/system') {
1592
+ if (args?.option !== 'logs')
1593
+ throw { code: 'NOT_SUPPORTED', message: `Unsupported system option: ${String(args?.option ?? '')}` };
1594
+ const options = listDiagnosticLogOptions();
1595
+ return [
1596
+ ...options.sources.map(source => ({ value: source.id, label: source.label, desc: `${source.fileCount} file(s), ${source.size} bytes` })),
1597
+ ...options.levels.map(level => ({ value: `level:${level}`, label: level, desc: 'minimum log level' })),
1598
+ ];
1599
+ }
1600
+ if (cmdBase0 === '/config' && args?.option === 'rawFile') {
1601
+ return listRawConfigResources();
1602
+ }
1570
1603
  // ── /agent list(只读) ──
1571
1604
  // 控制 channel:返回全量;agent channel:仅返回自身单条。
1572
1605
  if (cmd === '/agent') {
@@ -1607,7 +1640,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1607
1640
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1608
1641
  const scope = args?.options === 'all' ? 'all' : 'enabled';
1609
1642
  const role = optionsIdentity.role;
1610
- const isAdmin = isManagementRole(role);
1643
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
1611
1644
  if (!triggerScheduler)
1612
1645
  return [];
1613
1646
  const list = triggerScheduler.list({ all: scope === 'all' });
@@ -1710,14 +1743,25 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1710
1743
  });
1711
1744
  if ('error' in target)
1712
1745
  throw { code: target.code, message: target.error };
1713
- const agent = this.getAgent(channel, target.baseagent);
1714
- if (hasModelSwitcher(agent) && agent.listModels) {
1715
- const rawModels = await agent.listModels() ?? [];
1716
- const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, agent.resolveModelId?.bind(agent));
1746
+ let agent;
1747
+ try {
1748
+ agent = this.getAgent(channel, target.baseagent);
1749
+ }
1750
+ catch (error) {
1751
+ if (subject.processRole === 'daemon-service') {
1752
+ return [{ value: target.baseagent, label: target.baseagent, selected: true }];
1753
+ }
1754
+ throw error;
1755
+ }
1756
+ if (hasModelSwitcher(agent)) {
1757
+ const catalog = await getCatalog(target.sel.self, target.baseagent);
1758
+ assertCatalogSelectable(catalog);
1759
+ const rawModels = catalog.models.map(m => m.id);
1760
+ const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, model => resolveCatalogModel(catalog, model), target.sel.self);
1717
1761
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1718
1762
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1719
1763
  if (models.length > 0)
1720
- return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m), selected: modelMatches(agent, m, currentModel) }));
1764
+ return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '') + (catalog.freshness === 'stale' ? ' [stale]' : ''), selected: modelMatches(agent, m, currentModel) }));
1721
1765
  }
1722
1766
  return null;
1723
1767
  }
@@ -1770,11 +1814,20 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1770
1814
  });
1771
1815
  if ('error' in target)
1772
1816
  throw { code: target.code, message: target.error };
1773
- const agent = this.getAgent(channel, target.baseagent);
1817
+ let agent;
1818
+ try {
1819
+ agent = this.getAgent(channel, target.baseagent);
1820
+ }
1821
+ catch (error) {
1822
+ if (subject.processRole === 'daemon-service') {
1823
+ return ['low', 'medium', 'high', 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1824
+ }
1825
+ throw error;
1826
+ }
1774
1827
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1775
1828
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1776
1829
  const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
1777
- const efforts = getAvailableEfforts(agent, currentModel);
1830
+ const efforts = await getAvailableEfforts(agent, currentModel);
1778
1831
  const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent).value ?? 'auto';
1779
1832
  const allItems = [...efforts, 'auto'];
1780
1833
  return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
@@ -1924,6 +1977,18 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1924
1977
  });
1925
1978
  if (authDenied)
1926
1979
  return authDenied;
1980
+ if (cmdBase === '/baseagent-models') {
1981
+ try {
1982
+ const query = hostCatalogArgs(args);
1983
+ return { data: await getBaseagentCatalog(query.baseagent, { refresh: query.refresh }) };
1984
+ }
1985
+ catch (error) {
1986
+ if (error?.code === 'INVALID_ARGUMENT')
1987
+ return menuResultFailure(error);
1988
+ logger.error('Failed to query host baseagent catalog');
1989
+ return { error: 'Unable to query host baseagent catalog', code: 'INTERNAL_ERROR' };
1990
+ }
1991
+ }
1927
1992
  // ── /agent 查询(只读) ──
1928
1993
  // 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
1929
1994
  if (cmdBase === '/agent') {
@@ -1950,9 +2015,25 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1950
2015
  }
1951
2016
  // ── /config 查询(只读,查询各层配置) ──
1952
2017
  if (cmdBase === '/config') {
2018
+ if (args?.view === 'rawFile') {
2019
+ return { data: readRawConfigResource(args, subject.actorId ?? '') };
2020
+ }
1953
2021
  const scope = args?.scope || 'process';
1954
2022
  if (scope === 'process') {
1955
2023
  const cfg = loadDaemonConfig();
2024
+ if (args?.fields !== undefined) {
2025
+ if (!Array.isArray(args.fields) || args.fields.length === 0
2026
+ || args.fields.some((field) => typeof field !== 'string' || !field.trim())) {
2027
+ return { error: 'fields must be a non-empty string array', code: 'INVALID_ARGUMENT' };
2028
+ }
2029
+ const selected = {};
2030
+ for (const rawField of args.fields) {
2031
+ const field = rawField.trim();
2032
+ if (Object.prototype.hasOwnProperty.call(cfg, field))
2033
+ selected[field] = cfg[field];
2034
+ }
2035
+ return { data: { scope: 'process', fields: args.fields, config: selected } };
2036
+ }
1956
2037
  return { data: { scope: 'process', config: cfg } };
1957
2038
  }
1958
2039
  if (scope === 'defaults') {
@@ -2284,6 +2365,23 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2284
2365
  };
2285
2366
  }
2286
2367
  if (cmdBase === '/system') {
2368
+ const view = args?.view ?? 'status';
2369
+ if (view === 'monitor') {
2370
+ if (typeof this.daemonMonitorProvider !== 'function') {
2371
+ return { error: 'Daemon monitor snapshot is unavailable', code: 'DEPENDENCY_UNAVAILABLE' };
2372
+ }
2373
+ return { data: await this.daemonMonitorProvider() };
2374
+ }
2375
+ if (view === 'logs') {
2376
+ try {
2377
+ return { data: queryDiagnosticLogs(args ?? {}, subject.actorId ?? '') };
2378
+ }
2379
+ catch (error) {
2380
+ return { error: error?.message ?? String(error), code: error?.code ?? 'INTERNAL_ERROR' };
2381
+ }
2382
+ }
2383
+ if (view !== 'status')
2384
+ return { error: `Unsupported system view: ${String(view)}`, code: 'NOT_SUPPORTED' };
2287
2385
  const data = {
2288
2386
  aid: loadDaemonConfig().aid ?? null,
2289
2387
  pid: process.pid,
@@ -2352,7 +2450,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2352
2450
  return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
2353
2451
  }
2354
2452
  /** menu.update — 写入新值。 */
2355
- export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
2453
+ export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu', transportReplyContext) {
2356
2454
  const cmdBase = cmd.trim().split(' ')[0];
2357
2455
  if (!cmdBase)
2358
2456
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -2368,10 +2466,27 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2368
2466
  const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
2369
2467
  if (gated)
2370
2468
  return gated;
2469
+ const loadedContext = await this.loadMenuContext(channel, channelId);
2470
+ let session = loadedContext.session;
2471
+ const evolagent = loadedContext.evolagent;
2472
+ if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
2473
+ try {
2474
+ const topicSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId);
2475
+ if (!topicSession) {
2476
+ return { error: '话题会话不存在,拒绝回退到主会话', code: 'TOPIC_SESSION_NOT_FOUND' };
2477
+ }
2478
+ session = topicSession;
2479
+ }
2480
+ catch (error) {
2481
+ logger.debug('[MenuHandler] topic session lookup failed for menu update: thread=' + transportReplyContext.threadId + ': ' + (error instanceof Error ? error.message : String(error)));
2482
+ return { error: '无法读取话题会话,拒绝回退到主会话', code: 'TOPIC_SESSION_LOOKUP_FAILED' };
2483
+ }
2484
+ }
2485
+ if (cmdBase === '/baseagent-models')
2486
+ return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2371
2487
  const arg = value.trim();
2372
2488
  if (!arg)
2373
2489
  return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2374
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2375
2490
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2376
2491
  const isAdmin = isManagementRole(identity.role);
2377
2492
  const subject = buildMenuAuthSubject(this, {
@@ -2460,7 +2575,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2460
2575
  }
2461
2576
  if (!patch?.nameOrId)
2462
2577
  return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2463
- const isAdmin = isManagementRole(identity.role);
2578
+ const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
2464
2579
  if (!isAdmin && !userId)
2465
2580
  return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2466
2581
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
@@ -2522,21 +2637,112 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2522
2637
  return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
2523
2638
  }
2524
2639
  const previousBaseagent = evolagent.baseagent;
2640
+ if (session?.threadId) {
2641
+ if (transportReplyContext?.threadId !== session.threadId) {
2642
+ return { error: '话题路由上下文不匹配,拒绝切换', code: 'THREAD_CONTEXT_MISMATCH' };
2643
+ }
2644
+ if (typeof this.messageQueue.withSessionBarrier !== 'function') {
2645
+ return { error: '当前运行环境不支持安全话题切换', code: 'NOT_SUPPORTED' };
2646
+ }
2647
+ const sessionKey = this.getQueueKey(session, channel, channelId);
2648
+ const currentAgent = this.getAgent(channel, session.baseagent);
2649
+ const outcome = await this.messageQueue.withSessionBarrier(sessionKey, async (snapshot) => {
2650
+ const latest = await this.sessionManager.getSessionById?.(session.id);
2651
+ if (!latest || latest.threadId !== session.threadId) {
2652
+ return { error: '话题会话不存在或归属已变化', code: 'NOT_FOUND' };
2653
+ }
2654
+ const busy = !!latest.processingState
2655
+ || !!latest.metadata?.turnState?.active
2656
+ || snapshot.active
2657
+ || snapshot.queued > 0
2658
+ || snapshot.dispatching
2659
+ || this.messageQueue.isProcessing?.(sessionKey)
2660
+ || (this.messageQueue.getQueueLength?.(sessionKey) ?? 0) > 0
2661
+ || currentAgent.hasActiveStream(sessionKey);
2662
+ if (busy) {
2663
+ return { error: '当前话题会话忙碌,无法切换 baseagent', code: 'BUSY' };
2664
+ }
2665
+ try {
2666
+ evolagent.setActiveBaseagent(arg);
2667
+ await this.sessionManager.rotateTopicBackend({
2668
+ sessionId: latest.id,
2669
+ expectedThreadId: latest.threadId,
2670
+ baseagent: arg,
2671
+ });
2672
+ return { ok: true };
2673
+ }
2674
+ catch (error) {
2675
+ try {
2676
+ evolagent.setActiveBaseagent(previousBaseagent);
2677
+ }
2678
+ catch { }
2679
+ return { error: error instanceof Error ? error.message : String(error), code: 'EXEC_FAILED' };
2680
+ }
2681
+ });
2682
+ if ('error' in outcome)
2683
+ return outcome;
2684
+ this.eventBus.publish({
2685
+ type: 'agent:baseagent-changed',
2686
+ aid: evolagent.aid,
2687
+ baseagent: arg,
2688
+ previousBaseagent,
2689
+ scope: 'agent',
2690
+ timestamp: Date.now(),
2691
+ });
2692
+ return { data: {
2693
+ baseagent: arg,
2694
+ previousBaseagent,
2695
+ baseagentChanged: previousBaseagent !== arg,
2696
+ scope: 'agent',
2697
+ sessionId: session.id,
2698
+ threadId: session.threadId,
2699
+ sessionIdChanged: false,
2700
+ notice: '已切换 Baseagent: ' + previousBaseagent + ' → ' + arg + ',话题会话 ID 保持不变',
2701
+ } };
2702
+ }
2525
2703
  try {
2526
2704
  evolagent.setActiveBaseagent(arg);
2527
2705
  }
2528
2706
  catch (error) {
2529
2707
  return { error: error instanceof Error ? error.message : String(error), code: 'INVALID_VALUE' };
2530
2708
  }
2531
- this.eventBus.publish({
2532
- type: 'agent:baseagent-changed',
2533
- aid: evolagent.aid,
2534
- baseagent: arg,
2535
- previousBaseagent,
2536
- scope: 'agent',
2537
- timestamp: Date.now(),
2538
- });
2539
- return { data: { baseagent: arg, scope: 'agent' } };
2709
+ try {
2710
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, evolagent.projectPath, undefined, arg, session?.metadata, { previousBaseagent, baseagentChanged: previousBaseagent !== arg });
2711
+ if (session) {
2712
+ try {
2713
+ const previousAgent = this.getAgent(channel, session.baseagent);
2714
+ await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2715
+ await previousAgent.closeSession(session.id);
2716
+ }
2717
+ catch (error) {
2718
+ // The new Evolcore session is already active; backend cleanup is best effort.
2719
+ logger.warn(`[Menu] baseagent switch cleanup failed: session=${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2720
+ }
2721
+ }
2722
+ this.eventBus.publish({
2723
+ type: 'agent:baseagent-changed',
2724
+ aid: evolagent.aid,
2725
+ baseagent: arg,
2726
+ previousBaseagent,
2727
+ scope: 'agent',
2728
+ timestamp: Date.now(),
2729
+ });
2730
+ return { data: {
2731
+ baseagent: arg,
2732
+ previousBaseagent,
2733
+ baseagentChanged: previousBaseagent !== arg,
2734
+ scope: 'agent',
2735
+ sessionId: newSession.id,
2736
+ notice: `已切换 Baseagent: ${previousBaseagent} → ${arg},已创建新的主会话`,
2737
+ } };
2738
+ }
2739
+ catch (error) {
2740
+ try {
2741
+ evolagent.setActiveBaseagent(previousBaseagent);
2742
+ }
2743
+ catch { }
2744
+ return { error: `创建新会话失败:${error instanceof Error ? error.message : String(error)}`, code: 'EXEC_FAILED' };
2745
+ }
2540
2746
  }
2541
2747
  if (cmdBase === '/model') {
2542
2748
  const target = resolveMenuModelTarget.call(this, {
@@ -2554,13 +2760,16 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2554
2760
  const agent = this.getAgent(channel, target.baseagent);
2555
2761
  let targetModel = arg;
2556
2762
  if (hasModelSwitcher(agent)) {
2557
- const models = (await agent.listModels?.()) ?? [];
2763
+ const catalog = await getCatalog(target.sel.self, target.baseagent);
2764
+ assertCatalogSelectable(catalog);
2765
+ const models = catalog.models.map(m => m.id);
2558
2766
  const decision = validateModelSelectionForRole({
2559
2767
  role: identity.role,
2560
2768
  baseagent: target.baseagent,
2561
2769
  requestedModel: arg,
2562
2770
  models,
2563
- resolveModelId: agent.resolveModelId?.bind(agent),
2771
+ selfAid: target.sel.self,
2772
+ resolveModelId: model => resolveCatalogModel(catalog, model),
2564
2773
  });
2565
2774
  if (!decision.ok)
2566
2775
  return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
@@ -2610,7 +2819,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2610
2819
  const agent = this.getAgent(channel, target.baseagent);
2611
2820
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
2612
2821
  const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name;
2613
- const validEfforts = getAvailableEfforts(agent, currentModel);
2822
+ const validEfforts = await getAvailableEfforts(agent, currentModel);
2614
2823
  const allValid = [...validEfforts, 'auto'];
2615
2824
  if (!allValid.includes(arg)) {
2616
2825
  return { error: `无效推理强度: ${arg},可选: ${allValid.join(' / ')}`, code: 'INVALID_VALUE' };
@@ -2757,8 +2966,9 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2757
2966
  };
2758
2967
  }
2759
2968
  if (cmdBase === '/observable') {
2760
- if (identity.role !== 'owner')
2969
+ if (identity.role !== 'owner' && subject.processRole !== 'daemon-service' && !subject.isDaemonOwner) {
2761
2970
  return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
2971
+ }
2762
2972
  if (arg !== 'true' && arg !== 'false')
2763
2973
  return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
2764
2974
  if (!evolagent)
@@ -2778,8 +2988,25 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2778
2988
  const gated = gateControlScope.call(this, { cmdBase, action, args, channel, fromControlChannel });
2779
2989
  if (gated)
2780
2990
  return gated;
2781
- const { session: authSession } = await this.loadMenuContext(channel, channelId);
2991
+ const loadedContext = await this.loadMenuContext(channel, channelId);
2992
+ // Menu callbacks can arrive from a topic while the channel's active
2993
+ // session still points at the main conversation. Prefer the explicitly
2994
+ // transported topic route for session-scoped actions and authorization.
2995
+ let authSession = loadedContext.session;
2996
+ if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
2997
+ try {
2998
+ authSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId) ?? authSession;
2999
+ }
3000
+ catch (error) {
3001
+ logger.debug(`[MenuHandler] topic session lookup failed for menu action: thread=${transportReplyContext.threadId}: ${error instanceof Error ? error.message : String(error)}`);
3002
+ }
3003
+ }
3004
+ const session = authSession;
3005
+ const evolagent = loadedContext.evolagent;
3006
+ if (cmdBase === '/baseagent-models')
3007
+ return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2782
3008
  const authIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(authSession));
3009
+ const identity = authIdentity;
2783
3010
  const subject = buildMenuAuthSubject(this, {
2784
3011
  identity: authIdentity,
2785
3012
  session: authSession,
@@ -2791,7 +3018,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2791
3018
  fromControlChannel,
2792
3019
  subject: authSubject,
2793
3020
  });
2794
- if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner) {
3021
+ if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
2795
3022
  return {
2796
3023
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
2797
3024
  code: 'NOT_ALLOWED',
@@ -2868,15 +3095,13 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2868
3095
  return await gatewaySyncEnv(args);
2869
3096
  return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
2870
3097
  }
2871
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2872
- const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2873
- const isMenuAdmin = isManagementRole(identity.role);
3098
+ const isMenuAdmin = isManagementRole(authIdentity.role);
2874
3099
  // ── /agent action ──
2875
3100
  // 控制 channel:验 evolcore.owners,可执行进程级(create/delete/enable/disable)+ 自管理(update/reload)。
2876
3101
  // agent channel:闸门已挡掉进程级 + 跨 agent,仅 update/reload 能到此;reload 允许 owner/admin,update 仅 owner。
2877
3102
  if (cmdBase === '/agent') {
2878
3103
  if (fromControlChannel) {
2879
- if (!subject.isDaemonOwner) {
3104
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
2880
3105
  return {
2881
3106
  error: formatAuthorizationDenial('This Agent lifecycle operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
2882
3107
  code: 'NOT_ALLOWED',
@@ -2995,7 +3220,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2995
3220
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
2996
3221
  if (cmdBase === '/trigger') {
2997
3222
  const role = identity.role;
2998
- const isAdmin = isManagementRole(role);
3223
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
2999
3224
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
3000
3225
  if (!triggerScheduler)
3001
3226
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
@@ -3076,7 +3301,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3076
3301
  if (action === 'enable' && trigger.execution.permissionMode === 'fullaccess') {
3077
3302
  if (!isFullAccessEnabled())
3078
3303
  return { error: 'fullaccess 功能未启用', code: 'NOT_SUPPORTED' };
3079
- if (!subject.isDaemonOwner)
3304
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service')
3080
3305
  return { error: '只有 daemon-owner 可以启用 fullaccess Trigger', code: 'FORBIDDEN' };
3081
3306
  if (subject.canApprovePersistentFullAccess === false) {
3082
3307
  return { error: '当前认证通道不能批准持久 fullaccess Trigger', code: 'FORBIDDEN' };
@@ -3454,7 +3679,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3454
3679
  }
3455
3680
  if (action === 'new') {
3456
3681
  const name = (args?.name ?? '').toString().trim();
3457
- return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject });
3682
+ return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3458
3683
  }
3459
3684
  if (action === 'rename') {
3460
3685
  const newName = getRenameName(args);
@@ -3497,33 +3722,33 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3497
3722
  const target = (args?.target ?? '').toString().trim();
3498
3723
  if (!target)
3499
3724
  return { error: '缺少 args.target', code: 'MISSING_VALUE' };
3500
- return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject });
3725
+ return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3501
3726
  }
3502
3727
  if (action === 'switch') {
3503
3728
  const target = (args?.target ?? '').toString().trim();
3504
3729
  if (!target)
3505
3730
  return { error: '缺少 args.target', code: 'MISSING_VALUE' };
3506
- return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject });
3731
+ return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3507
3732
  }
3508
3733
  if (action === 'compact') {
3509
3734
  const need = this.requireSession(session);
3510
3735
  if (need)
3511
3736
  return need;
3512
- return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject });
3737
+ return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3513
3738
  }
3514
3739
  if (action === 'fork') {
3515
3740
  const need = this.requireSession(session);
3516
3741
  if (need)
3517
3742
  return need;
3518
3743
  const name = (args?.name ?? '').toString().trim();
3519
- return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject });
3744
+ return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3520
3745
  }
3521
3746
  return { error: `不支持的 session action: ${action}`, code: 'NOT_SUPPORTED' };
3522
3747
  }
3523
3748
  // ── /system 系列 ──
3524
3749
  if (cmdBase === '/system') {
3525
3750
  // D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
3526
- if (!subject.isDaemonOwner) {
3751
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
3527
3752
  return {
3528
3753
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
3529
3754
  code: 'NOT_ALLOWED',
@@ -3567,17 +3792,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3567
3792
  const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
3568
3793
  if (migrationRequirement.required) {
3569
3794
  return {
3570
- error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3795
+ error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启;本次仅检查,未停止当前服务。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3571
3796
  code: 'DATA_MIGRATION_REQUIRED',
3572
3797
  data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
3573
3798
  };
3574
3799
  }
3575
3800
  if (!suppressRealRestart) {
3801
+ // 这条路径同样以 daemon 自杀收尾,入口缺失必须在写 pending 和自杀之前拦下。
3802
+ const restartEntry = resolveCliEntry(getPackageRoot());
3803
+ if (!fs.existsSync(restartEntry.entry)) {
3804
+ return {
3805
+ error: `找不到重启入口:${restartEntry.entry}。请先执行 npm run build 重建产物。`,
3806
+ code: 'RESTART_ENTRY_MISSING',
3807
+ data: { reasonCode: 'RESTART_ENTRY_MISSING', entry: restartEntry.entry },
3808
+ };
3809
+ }
3576
3810
  const restartInfo = {
3577
3811
  channel,
3578
3812
  channelId,
3579
3813
  timestamp: Date.now(),
3814
+ ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
3580
3815
  ...(restartDelivery ? { delivery: restartDelivery } : {}),
3816
+ ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
3581
3817
  };
3582
3818
  const controlDir = daemonControlDir();
3583
3819
  fs.mkdirSync(controlDir, { recursive: true });
@@ -3586,23 +3822,41 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3586
3822
  // the version check and automatic upgrades before replacing the daemon.
3587
3823
  // Do not inherit managed-session credentials: otherwise cmdRestart can
3588
3824
  // delegate back to this daemon and recursively enqueue another restart.
3589
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart'], {
3825
+ //
3826
+ // `ec restart` owns the monitor liveness check, but it can itself die on
3827
+ // load (tsx + a broken source tree), and this path kills the daemon
3828
+ // afterwards — so confirm the CLI process survived before proceeding.
3829
+ const restartLaunch = await launchMonitorAndConfirm([...restartEntry.args, 'restart'], {
3590
3830
  EVOLCORE_HOME: resolvePaths().root,
3591
3831
  EVOLCORE_SESSION_ID: '',
3592
3832
  [AGENT_DELEGATION_TOKEN_ENV]: '',
3593
- });
3833
+ },
3834
+ // `ec restart` does not register itself; staying alive is the signal.
3835
+ () => false, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid), 8_000);
3836
+ if (!restartLaunch.ok) {
3837
+ try {
3838
+ fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
3839
+ }
3840
+ catch { }
3841
+ logger.error(`[System] Refusing menu restart: launcher did not survive (${restartLaunch.reason})`);
3842
+ for (const line of (restartLaunch.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
3843
+ logger.error(`[System] | ${line}`);
3844
+ }
3845
+ return {
3846
+ error: `重启已中止:启动器未能运行(${restartLaunch.reason})。服务未停止,仍在运行当前版本。`,
3847
+ code: 'RESTART_LAUNCH_FAILED',
3848
+ data: { reasonCode: 'RESTART_LAUNCH_FAILED', reason: restartLaunch.reason },
3849
+ };
3850
+ }
3594
3851
  }
3595
3852
  else {
3596
3853
  logger.info('[System] Suppressed real menu restart in test runtime');
3597
3854
  }
3598
3855
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
3599
- if (!suppressRealRestart) {
3600
- setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
3601
- }
3602
3856
  return { data: { action: 'restart', success: true } };
3603
3857
  }
3604
3858
  if (action === 'check') {
3605
- const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject });
3859
+ const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3606
3860
  // delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
3607
3861
  return r;
3608
3862
  }
@@ -3819,6 +4073,48 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3819
4073
  }
3820
4074
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
3821
4075
  }
4076
+ export function resolveControlMenuAuthorization(input) {
4077
+ const headers = parseMenuClientProtectedHeaders(input.auth.protectedHeaders);
4078
+ const wireAuthMethod = headers.auth_method ?? 'daemon-owner';
4079
+ const authMethod = wireAuthMethod === 'ecweb-token' ? 'remote-access-token' : wireAuthMethod;
4080
+ const remoteToken = headers.remote_access_token ?? headers.ecweb_token;
4081
+ if (authMethod !== 'daemon-owner' && authMethod !== 'remote-access-token') {
4082
+ return { allowed: false, code: 'PERMISSION_DENIED', message: 'Unsupported Control Menu authorization method' };
4083
+ }
4084
+ if (authMethod === 'daemon-owner') {
4085
+ if (remoteToken) {
4086
+ return { allowed: false, code: 'PERMISSION_DENIED', message: 'remote_access_token requires auth_method=remote-access-token' };
4087
+ }
4088
+ if (!isProcessLevelOwner(input.peerId, input.owners)) {
4089
+ return {
4090
+ allowed: false,
4091
+ code: 'NOT_ALLOWED',
4092
+ message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
4093
+ data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4094
+ };
4095
+ }
4096
+ return { allowed: true, actorAid: input.peerId, authMethod };
4097
+ }
4098
+ if (!input.auth.encrypted || !remoteToken) {
4099
+ return { allowed: false, code: 'REMOTE_ACCESS_TOKEN_INVALID', message: 'Remote access token authorization requires an encrypted protected header' };
4100
+ }
4101
+ const checked = (input.validateToken ?? validateAndRenewRemoteAccessToken)(remoteToken);
4102
+ if (!checked.valid) {
4103
+ return { allowed: false, code: 'REMOTE_ACCESS_TOKEN_INVALID', message: 'Remote access token is invalid or expired' };
4104
+ }
4105
+ const actorAid = checked.authorizedBy && input.owners.includes(checked.authorizedBy)
4106
+ ? checked.authorizedBy
4107
+ : input.owners[0];
4108
+ if (!actorAid) {
4109
+ return {
4110
+ allowed: false,
4111
+ code: 'NOT_ALLOWED',
4112
+ message: formatAuthorizationDenial('Configure daemon.json.owners before using Control Menu operations.', 'DAEMON_OWNER_REQUIRED'),
4113
+ data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4114
+ };
4115
+ }
4116
+ return { allowed: true, actorAid, authMethod };
4117
+ }
3822
4118
  function menuCommandToken(cmd) {
3823
4119
  if (typeof cmd !== 'string')
3824
4120
  return undefined;
@@ -3826,8 +4122,8 @@ function menuCommandToken(cmd) {
3826
4122
  }
3827
4123
  function isProcessLevelMenu(name, cmd) {
3828
4124
  const cmdToken = menuCommandToken(cmd);
3829
- return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
3830
- || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config';
4125
+ return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models'
4126
+ || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models';
3831
4127
  }
3832
4128
  function isProcessContextMenu(payload) {
3833
4129
  const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
@@ -3871,7 +4167,8 @@ async function execMenuForSystemControl(payload, context) {
3871
4167
  if ('error' in target)
3872
4168
  return ecwebErr(id, name, target.code, target.error);
3873
4169
  const targetChannel = target.channel;
3874
- const targetAid = this.getOwningAgent?.(targetChannel)?.aid;
4170
+ const targetAid = this.getOwningAgent?.(targetChannel)?.aid
4171
+ ?? this.getDaemonTriggerOwnerAid?.(targetChannel);
3875
4172
  const trustedSubject = buildAuthSubject({
3876
4173
  selfAid: targetAid,
3877
4174
  actorId: context.actorAid,
@@ -3884,10 +4181,18 @@ async function execMenuForSystemControl(payload, context) {
3884
4181
  processOwners: context.owners,
3885
4182
  fromControlChannel: true,
3886
4183
  canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
4184
+ trustedProcessRole: context.source === 'ecweb' ? 'daemon-owner' : undefined,
3887
4185
  });
3888
4186
  const trustedIdentity = trustedSubject.identity;
3889
4187
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
3890
4188
  try {
4189
+ if (menuCommandToken(cmd) === '/baseagent-models') {
4190
+ if (payload.type !== 'menu.query')
4191
+ return ecwebErr(id, name, 'NOT_SUPPORTED', 'Baseagent catalog only supports menu.query');
4192
+ if (payload.agent !== undefined)
4193
+ return ecwebErr(id, name, 'INVALID_ARGUMENT', 'Baseagent catalog does not accept an Agent target');
4194
+ hostCatalogArgs(payload.args);
4195
+ }
3891
4196
  switch (payload?.type) {
3892
4197
  case 'menu.list':
3893
4198
  if (payload?.args?.view === 'catalog') {
@@ -3991,6 +4296,7 @@ async function execMenuForSystemControl(payload, context) {
3991
4296
  }
3992
4297
  }
3993
4298
  catch (e) {
4299
+ logger.error(`[ECWebMenu] failed type=${payload?.type ?? '<none>'} name=${name ?? '<none>'} cmd=${cmd ?? '<none>'} agent=${payload?.agent ?? '<none>'} actor=${context.actorAid ?? '<none>'}: ${e?.stack || e?.message || String(e)}`);
3994
4300
  return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError(e));
3995
4301
  }
3996
4302
  }
@@ -4044,10 +4350,10 @@ export async function execMenuForEcweb(payload, trusted) {
4044
4350
  owners,
4045
4351
  });
4046
4352
  }
4047
- /** 控制 AID channel 专用入口:peerId 必须 ∈ evolcore.owners。
4048
- * 全量权限(进程级 + 跨 agent + 关系级),fromControlChannel=true 放行闸门。
4049
- * 与 ECWeb 入口的区别:鉴权主体是真实 peerId(而非注入 owner),按 evolcore.owners 校验。 */
4050
- export async function execMenuForControl(payload, peerId) {
4353
+ /** Control AID entry: daemon-owner and valid ECWeb token modes share the same
4354
+ * process authorization path. The authenticated envelope peerId remains the
4355
+ * requester; token mode maps to a configured owner as its management actor. */
4356
+ export async function execMenuForControl(payload, peerId, auth = {}) {
4051
4357
  const id = payload?.id ?? '';
4052
4358
  const name = payload?.name;
4053
4359
  const validationError = validateMenuRequest(payload ?? {});
@@ -4057,16 +4363,22 @@ export async function execMenuForControl(payload, peerId) {
4057
4363
  const compatCmd = menuCommandToken(payload?.cmd);
4058
4364
  const isRoleRequest = name === 'role' || compatCmd === '/role';
4059
4365
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
4060
- if (!isRoleRequest && !isConnectRequest && !isProcessLevelOwner(peerId, owners)) {
4366
+ const authorization = resolveControlMenuAuthorization({ peerId, owners, auth });
4367
+ const hasExplicitAuthMethod = auth.protectedHeaders?.auth_method !== undefined;
4368
+ if (!authorization.allowed && (hasExplicitAuthMethod || (!isRoleRequest && !isConnectRequest))) {
4061
4369
  return menuFailure({ id, ...(name ? { name } : {}) }, {
4062
- code: 'NOT_ALLOWED',
4063
- message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
4064
- data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4370
+ code: authorization.code,
4371
+ message: authorization.message,
4372
+ ...(authorization.data !== undefined ? { data: authorization.data } : {}),
4065
4373
  });
4066
4374
  }
4375
+ // Existing role/connect self-inspection remains available without process
4376
+ // authority. Token-authorized and daemon-owner requests use one canonical
4377
+ // management actor for the common process authorization path.
4378
+ const managementActor = authorization.allowed ? authorization.actorAid : peerId;
4067
4379
  return execMenuForSystemControl.call(this, payload, {
4068
4380
  source: 'control',
4069
- actorAid: peerId,
4381
+ actorAid: managementActor,
4070
4382
  localDirect: false,
4071
4383
  canApprovePersistentFullAccess: false,
4072
4384
  owners,