evolcore 0.0.21 → 0.0.23

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/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  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 +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -1,11 +1,11 @@
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, resolveCatalogModel, assertCatalogSelectable } 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
6
  import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
8
7
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
8
+ import { inspectDataMigrationRequirement } from '../data-migration.js';
9
9
  import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
10
10
  import path from 'path';
11
11
  import fs from 'fs';
@@ -39,6 +39,9 @@ import { logger } from '../../utils/logger.js';
39
39
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
40
40
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
41
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';
42
45
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
43
46
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
44
47
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
@@ -76,7 +79,7 @@ function getBaseagentVersion(cmd) {
76
79
  return null;
77
80
  }
78
81
  }
79
- const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
82
+ const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
80
83
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
81
84
  /** menu file: fetch 文件大小上限(与 /file 一致) */
82
85
  const FILE_FETCH_MAX_SIZE = 10 * 1024 * 1024;
@@ -393,14 +396,8 @@ function tokenizeArgv(line) {
393
396
  }
394
397
  return out;
395
398
  }
396
- function getAvailableEfforts(agent, model) {
397
- if (agent.name === 'claude') {
398
- return allEfforts;
399
- }
400
- if (agent.name === 'codex') {
401
- return getCodexEfforts(model);
402
- }
403
- return [];
399
+ async function getAvailableEfforts(agent, model) {
400
+ return (await agent.getAvailableEfforts?.(model) ?? []);
404
401
  }
405
402
  function modelDisplayLabel(agent, model) {
406
403
  const full = agent.resolveModelId?.(model);
@@ -942,6 +939,10 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
942
939
  return intent('trigger.list', 'relation');
943
940
  }
944
941
  if (verb === 'options') {
942
+ if (cmdBase === '/system')
943
+ return intent('system.status', 'process');
944
+ if (cmdBase === '/config')
945
+ return intent('config.read', args?.scope === 'agent' ? 'agent' : 'process');
945
946
  if (cmdBase === '/agent')
946
947
  return intent(fromControlChannel ? 'agent.list' : 'agent.show', fromControlChannel ? 'control' : 'agent');
947
948
  if (cmdBase === '/capability')
@@ -1283,8 +1284,14 @@ function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, requir
1283
1284
  : { channel: fallbackChannel };
1284
1285
  }
1285
1286
  const agent = owner.agentRegistry?.get?.(requestedAid) ?? null;
1286
- if (!agent)
1287
+ if (!agent) {
1288
+ // The daemon trigger owner is a virtual control identity and therefore is
1289
+ // intentionally absent from the normal Agent registry.
1290
+ const daemonAid = owner.getDaemonTriggerOwnerAid?.();
1291
+ if (daemonAid && requestedAid === daemonAid)
1292
+ return { channel: 'daemon' };
1287
1293
  return { error: `未找到 Agent: ${requestedAid}`, code: 'NOT_FOUND' };
1294
+ }
1288
1295
  const channel = agent.channelInstanceNames?.()[0];
1289
1296
  if (!channel)
1290
1297
  return { error: `Agent 无可用 channel: ${requestedAid}`, code: 'NOT_FOUND' };
@@ -1470,6 +1477,9 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1470
1477
  ...(isControlScope && canManageProcess && canOperation('system.restart', 'process') ? [
1471
1478
  { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1472
1479
  ] : []),
1480
+ ...(isControlScope && canManageProcess && canOperation('system.status', 'process') ? [
1481
+ { cmd: '/system', label: 'Daemon 诊断', desc: '查询 daemon 状态、监控快照和日志' },
1482
+ ] : []),
1473
1483
  ...(canUseFile ? [
1474
1484
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1475
1485
  ] : []),
@@ -1566,6 +1576,18 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1566
1576
  });
1567
1577
  if (optionsAuthDenied)
1568
1578
  throw { code: optionsAuthDenied.code, message: optionsAuthDenied.error, data: optionsAuthDenied.data };
1579
+ if (cmdBase0 === '/system') {
1580
+ if (args?.option !== 'logs')
1581
+ throw { code: 'NOT_SUPPORTED', message: `Unsupported system option: ${String(args?.option ?? '')}` };
1582
+ const options = listDiagnosticLogOptions();
1583
+ return [
1584
+ ...options.sources.map(source => ({ value: source.id, label: source.label, desc: `${source.fileCount} file(s), ${source.size} bytes` })),
1585
+ ...options.levels.map(level => ({ value: `level:${level}`, label: level, desc: 'minimum log level' })),
1586
+ ];
1587
+ }
1588
+ if (cmdBase0 === '/config' && args?.option === 'rawFile') {
1589
+ return listRawConfigResources();
1590
+ }
1569
1591
  // ── /agent list(只读) ──
1570
1592
  // 控制 channel:返回全量;agent channel:仅返回自身单条。
1571
1593
  if (cmd === '/agent') {
@@ -1606,7 +1628,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1606
1628
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1607
1629
  const scope = args?.options === 'all' ? 'all' : 'enabled';
1608
1630
  const role = optionsIdentity.role;
1609
- const isAdmin = isManagementRole(role);
1631
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
1610
1632
  if (!triggerScheduler)
1611
1633
  return [];
1612
1634
  const list = triggerScheduler.list({ all: scope === 'all' });
@@ -1710,13 +1732,15 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1710
1732
  if ('error' in target)
1711
1733
  throw { code: target.code, message: target.error };
1712
1734
  const agent = this.getAgent(channel, target.baseagent);
1713
- if (hasModelSwitcher(agent) && agent.listModels) {
1714
- const rawModels = await agent.listModels() ?? [];
1715
- const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, agent.resolveModelId?.bind(agent));
1735
+ if (hasModelSwitcher(agent)) {
1736
+ const catalog = await getCatalog(target.sel.self, target.baseagent);
1737
+ assertCatalogSelectable(catalog);
1738
+ const rawModels = catalog.models.map(m => m.id);
1739
+ const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, model => resolveCatalogModel(catalog, model), target.sel.self);
1716
1740
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1717
1741
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1718
1742
  if (models.length > 0)
1719
- return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m), selected: modelMatches(agent, m, currentModel) }));
1743
+ 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) }));
1720
1744
  }
1721
1745
  return null;
1722
1746
  }
@@ -1773,7 +1797,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1773
1797
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1774
1798
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1775
1799
  const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
1776
- const efforts = getAvailableEfforts(agent, currentModel);
1800
+ const efforts = await getAvailableEfforts(agent, currentModel);
1777
1801
  const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent).value ?? 'auto';
1778
1802
  const allItems = [...efforts, 'auto'];
1779
1803
  return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
@@ -1949,9 +1973,25 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1949
1973
  }
1950
1974
  // ── /config 查询(只读,查询各层配置) ──
1951
1975
  if (cmdBase === '/config') {
1976
+ if (args?.view === 'rawFile') {
1977
+ return { data: readRawConfigResource(args, subject.actorId ?? '') };
1978
+ }
1952
1979
  const scope = args?.scope || 'process';
1953
1980
  if (scope === 'process') {
1954
1981
  const cfg = loadDaemonConfig();
1982
+ if (args?.fields !== undefined) {
1983
+ if (!Array.isArray(args.fields) || args.fields.length === 0
1984
+ || args.fields.some((field) => typeof field !== 'string' || !field.trim())) {
1985
+ return { error: 'fields must be a non-empty string array', code: 'INVALID_ARGUMENT' };
1986
+ }
1987
+ const selected = {};
1988
+ for (const rawField of args.fields) {
1989
+ const field = rawField.trim();
1990
+ if (Object.prototype.hasOwnProperty.call(cfg, field))
1991
+ selected[field] = cfg[field];
1992
+ }
1993
+ return { data: { scope: 'process', fields: args.fields, config: selected } };
1994
+ }
1955
1995
  return { data: { scope: 'process', config: cfg } };
1956
1996
  }
1957
1997
  if (scope === 'defaults') {
@@ -2283,6 +2323,23 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2283
2323
  };
2284
2324
  }
2285
2325
  if (cmdBase === '/system') {
2326
+ const view = args?.view ?? 'status';
2327
+ if (view === 'monitor') {
2328
+ if (typeof this.daemonMonitorProvider !== 'function') {
2329
+ return { error: 'Daemon monitor snapshot is unavailable', code: 'DEPENDENCY_UNAVAILABLE' };
2330
+ }
2331
+ return { data: await this.daemonMonitorProvider() };
2332
+ }
2333
+ if (view === 'logs') {
2334
+ try {
2335
+ return { data: queryDiagnosticLogs(args ?? {}, subject.actorId ?? '') };
2336
+ }
2337
+ catch (error) {
2338
+ return { error: error?.message ?? String(error), code: error?.code ?? 'INTERNAL_ERROR' };
2339
+ }
2340
+ }
2341
+ if (view !== 'status')
2342
+ return { error: `Unsupported system view: ${String(view)}`, code: 'NOT_SUPPORTED' };
2286
2343
  const data = {
2287
2344
  aid: loadDaemonConfig().aid ?? null,
2288
2345
  pid: process.pid,
@@ -2459,7 +2516,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2459
2516
  }
2460
2517
  if (!patch?.nameOrId)
2461
2518
  return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2462
- const isAdmin = isManagementRole(identity.role);
2519
+ const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner;
2463
2520
  if (!isAdmin && !userId)
2464
2521
  return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2465
2522
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
@@ -2553,13 +2610,16 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2553
2610
  const agent = this.getAgent(channel, target.baseagent);
2554
2611
  let targetModel = arg;
2555
2612
  if (hasModelSwitcher(agent)) {
2556
- const models = (await agent.listModels?.()) ?? [];
2613
+ const catalog = await getCatalog(target.sel.self, target.baseagent);
2614
+ assertCatalogSelectable(catalog);
2615
+ const models = catalog.models.map(m => m.id);
2557
2616
  const decision = validateModelSelectionForRole({
2558
2617
  role: identity.role,
2559
2618
  baseagent: target.baseagent,
2560
2619
  requestedModel: arg,
2561
2620
  models,
2562
- resolveModelId: agent.resolveModelId?.bind(agent),
2621
+ selfAid: target.sel.self,
2622
+ resolveModelId: model => resolveCatalogModel(catalog, model),
2563
2623
  });
2564
2624
  if (!decision.ok)
2565
2625
  return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
@@ -2609,7 +2669,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2609
2669
  const agent = this.getAgent(channel, target.baseagent);
2610
2670
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
2611
2671
  const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name;
2612
- const validEfforts = getAvailableEfforts(agent, currentModel);
2672
+ const validEfforts = await getAvailableEfforts(agent, currentModel);
2613
2673
  const allValid = [...validEfforts, 'auto'];
2614
2674
  if (!allValid.includes(arg)) {
2615
2675
  return { error: `无效推理强度: ${arg},可选: ${allValid.join(' / ')}`, code: 'INVALID_VALUE' };
@@ -2994,7 +3054,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2994
3054
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
2995
3055
  if (cmdBase === '/trigger') {
2996
3056
  const role = identity.role;
2997
- const isAdmin = isManagementRole(role);
3057
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
2998
3058
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
2999
3059
  if (!triggerScheduler)
3000
3060
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
@@ -3563,6 +3623,14 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3563
3623
  : 'AUN_OUTBOUND_ROUTE_REQUIRED',
3564
3624
  };
3565
3625
  }
3626
+ const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
3627
+ if (migrationRequirement.required) {
3628
+ return {
3629
+ error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3630
+ code: 'DATA_MIGRATION_REQUIRED',
3631
+ data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
3632
+ };
3633
+ }
3566
3634
  if (!suppressRealRestart) {
3567
3635
  const restartInfo = {
3568
3636
  channel,
@@ -3810,6 +3878,48 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3810
3878
  }
3811
3879
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
3812
3880
  }
3881
+ export function resolveControlMenuAuthorization(input) {
3882
+ const headers = parseMenuClientProtectedHeaders(input.auth.protectedHeaders);
3883
+ const wireAuthMethod = headers.auth_method ?? 'daemon-owner';
3884
+ const authMethod = wireAuthMethod === 'ecweb-token' ? 'remote-access-token' : wireAuthMethod;
3885
+ const remoteToken = headers.remote_access_token ?? headers.ecweb_token;
3886
+ if (authMethod !== 'daemon-owner' && authMethod !== 'remote-access-token') {
3887
+ return { allowed: false, code: 'PERMISSION_DENIED', message: 'Unsupported Control Menu authorization method' };
3888
+ }
3889
+ if (authMethod === 'daemon-owner') {
3890
+ if (remoteToken) {
3891
+ return { allowed: false, code: 'PERMISSION_DENIED', message: 'remote_access_token requires auth_method=remote-access-token' };
3892
+ }
3893
+ if (!isProcessLevelOwner(input.peerId, input.owners)) {
3894
+ return {
3895
+ allowed: false,
3896
+ code: 'NOT_ALLOWED',
3897
+ message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
3898
+ data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
3899
+ };
3900
+ }
3901
+ return { allowed: true, actorAid: input.peerId, authMethod };
3902
+ }
3903
+ if (!input.auth.encrypted || !remoteToken) {
3904
+ return { allowed: false, code: 'REMOTE_ACCESS_TOKEN_INVALID', message: 'Remote access token authorization requires an encrypted protected header' };
3905
+ }
3906
+ const checked = (input.validateToken ?? validateAndRenewRemoteAccessToken)(remoteToken);
3907
+ if (!checked.valid) {
3908
+ return { allowed: false, code: 'REMOTE_ACCESS_TOKEN_INVALID', message: 'Remote access token is invalid or expired' };
3909
+ }
3910
+ const actorAid = checked.authorizedBy && input.owners.includes(checked.authorizedBy)
3911
+ ? checked.authorizedBy
3912
+ : input.owners[0];
3913
+ if (!actorAid) {
3914
+ return {
3915
+ allowed: false,
3916
+ code: 'NOT_ALLOWED',
3917
+ message: formatAuthorizationDenial('Configure daemon.json.owners before using Control Menu operations.', 'DAEMON_OWNER_REQUIRED'),
3918
+ data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
3919
+ };
3920
+ }
3921
+ return { allowed: true, actorAid, authMethod };
3922
+ }
3813
3923
  function menuCommandToken(cmd) {
3814
3924
  if (typeof cmd !== 'string')
3815
3925
  return undefined;
@@ -3862,7 +3972,8 @@ async function execMenuForSystemControl(payload, context) {
3862
3972
  if ('error' in target)
3863
3973
  return ecwebErr(id, name, target.code, target.error);
3864
3974
  const targetChannel = target.channel;
3865
- const targetAid = this.getOwningAgent?.(targetChannel)?.aid;
3975
+ const targetAid = this.getOwningAgent?.(targetChannel)?.aid
3976
+ ?? this.getDaemonTriggerOwnerAid?.(targetChannel);
3866
3977
  const trustedSubject = buildAuthSubject({
3867
3978
  selfAid: targetAid,
3868
3979
  actorId: context.actorAid,
@@ -4035,10 +4146,10 @@ export async function execMenuForEcweb(payload, trusted) {
4035
4146
  owners,
4036
4147
  });
4037
4148
  }
4038
- /** 控制 AID channel 专用入口:peerId 必须 ∈ evolcore.owners。
4039
- * 全量权限(进程级 + 跨 agent + 关系级),fromControlChannel=true 放行闸门。
4040
- * 与 ECWeb 入口的区别:鉴权主体是真实 peerId(而非注入 owner),按 evolcore.owners 校验。 */
4041
- export async function execMenuForControl(payload, peerId) {
4149
+ /** Control AID entry: daemon-owner and valid ECWeb token modes share the same
4150
+ * process authorization path. The authenticated envelope peerId remains the
4151
+ * requester; token mode maps to a configured owner as its management actor. */
4152
+ export async function execMenuForControl(payload, peerId, auth = {}) {
4042
4153
  const id = payload?.id ?? '';
4043
4154
  const name = payload?.name;
4044
4155
  const validationError = validateMenuRequest(payload ?? {});
@@ -4048,16 +4159,22 @@ export async function execMenuForControl(payload, peerId) {
4048
4159
  const compatCmd = menuCommandToken(payload?.cmd);
4049
4160
  const isRoleRequest = name === 'role' || compatCmd === '/role';
4050
4161
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
4051
- if (!isRoleRequest && !isConnectRequest && !isProcessLevelOwner(peerId, owners)) {
4162
+ const authorization = resolveControlMenuAuthorization({ peerId, owners, auth });
4163
+ const hasExplicitAuthMethod = auth.protectedHeaders?.auth_method !== undefined;
4164
+ if (!authorization.allowed && (hasExplicitAuthMethod || (!isRoleRequest && !isConnectRequest))) {
4052
4165
  return menuFailure({ id, ...(name ? { name } : {}) }, {
4053
- code: 'NOT_ALLOWED',
4054
- message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
4055
- data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4166
+ code: authorization.code,
4167
+ message: authorization.message,
4168
+ ...(authorization.data !== undefined ? { data: authorization.data } : {}),
4056
4169
  });
4057
4170
  }
4171
+ // Existing role/connect self-inspection remains available without process
4172
+ // authority. Token-authorized and daemon-owner requests use one canonical
4173
+ // management actor for the common process authorization path.
4174
+ const managementActor = authorization.allowed ? authorization.actorAid : peerId;
4058
4175
  return execMenuForSystemControl.call(this, payload, {
4059
4176
  source: 'control',
4060
- actorAid: peerId,
4177
+ actorAid: managementActor,
4061
4178
  localDirect: false,
4062
4179
  canApprovePersistentFullAccess: false,
4063
4180
  owners,
@@ -12,6 +12,93 @@ export const MENU_REQUEST_TYPES = new Set([
12
12
  'menu.update',
13
13
  'menu.action',
14
14
  ]);
15
+ export function buildMenuClientProtectedHeaders(input) {
16
+ const clientName = input.clientName ?? 'evol';
17
+ if (clientName === 'ec-menu-client') {
18
+ const clientVersion = input.clientVersion?.trim();
19
+ if (!parseStableSemver(clientVersion)) {
20
+ throw Object.assign(new Error('ec-menu-client requires a stable X.Y.Z client version'), { code: 'INVALID_CLIENT' });
21
+ }
22
+ return {
23
+ client_name: clientName,
24
+ client_version: clientVersion,
25
+ ...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
26
+ };
27
+ }
28
+ return {
29
+ ...(input.evolVersion ? { evol_version: input.evolVersion } : {}),
30
+ ...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
31
+ };
32
+ }
33
+ export function parseMenuClientProtectedHeaders(value) {
34
+ if (!value)
35
+ return {};
36
+ const normalized = {};
37
+ for (const [key, raw] of Object.entries(value)) {
38
+ if (typeof raw !== 'string')
39
+ continue;
40
+ normalized[key.trim().toLowerCase()] = raw;
41
+ }
42
+ const result = {};
43
+ if (normalized.client_name === 'evol' || normalized.client_name === 'ec-menu-client')
44
+ result.client_name = normalized.client_name;
45
+ else if (normalized.client_name !== undefined)
46
+ result.client_name = normalized.client_name;
47
+ if (normalized.client_version !== undefined)
48
+ result.client_version = normalized.client_version;
49
+ if (normalized.evol_version !== undefined)
50
+ result.evol_version = normalized.evol_version;
51
+ if (normalized.auth_method === 'daemon-owner' || normalized.auth_method === 'remote-access-token' || normalized.auth_method === 'ecweb-token')
52
+ result.auth_method = normalized.auth_method;
53
+ else if (normalized.auth_method !== undefined)
54
+ result.auth_method = normalized.auth_method;
55
+ if (normalized.remote_access_token !== undefined)
56
+ result.remote_access_token = normalized.remote_access_token;
57
+ if (normalized.ecweb_token !== undefined)
58
+ result.ecweb_token = normalized.ecweb_token;
59
+ return result;
60
+ }
61
+ /** Validate client identity separately from authorization. These fields never grant access. */
62
+ export function evaluateMenuClientCompatibility(input) {
63
+ const headers = parseMenuClientProtectedHeaders(input.protectedHeaders);
64
+ const clientName = headers.client_name ?? 'evol';
65
+ if (clientName === 'ec-menu-client') {
66
+ if (!input.encrypted || !parseStableSemver(headers.client_version)) {
67
+ return {
68
+ code: 'INVALID_CLIENT',
69
+ message: 'ec-menu-client requires encrypted protected headers and a stable X.Y.Z client_version',
70
+ };
71
+ }
72
+ return null;
73
+ }
74
+ if (clientName !== 'evol') {
75
+ return { code: 'INVALID_CLIENT', message: `Unsupported Menu client: ${String(clientName)}` };
76
+ }
77
+ return evaluateEvolMenuVersionGate(input);
78
+ }
79
+ export function parseMenuResponse(value) {
80
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
81
+ throw Object.assign(new Error('Menu response must be an object'), { code: 'INVALID_RESPONSE' });
82
+ }
83
+ const response = value;
84
+ if (response.type !== 'menu.response' || typeof response.id !== 'string' || !response.id.trim()) {
85
+ throw Object.assign(new Error('Invalid Menu response type or id'), { code: 'INVALID_RESPONSE' });
86
+ }
87
+ const hasData = Object.prototype.hasOwnProperty.call(response, 'data');
88
+ const hasError = Object.prototype.hasOwnProperty.call(response, 'error');
89
+ if (hasData === hasError) {
90
+ throw Object.assign(new Error('Menu response must contain exactly one of data or error'), { code: 'INVALID_RESPONSE' });
91
+ }
92
+ if (hasError) {
93
+ const error = response.error;
94
+ if (!error || typeof error !== 'object' || Array.isArray(error)
95
+ || typeof error.code !== 'string'
96
+ || typeof error.message !== 'string') {
97
+ throw Object.assign(new Error('Invalid Menu response error'), { code: 'INVALID_RESPONSE' });
98
+ }
99
+ }
100
+ return value;
101
+ }
15
102
  export const MENU_NAME_COMMANDS = Object.freeze({
16
103
  pwd: '/pwd',
17
104
  session: '/session',
@@ -259,6 +346,25 @@ export function menuFailure(header, error) {
259
346
  },
260
347
  };
261
348
  }
349
+ export function enforceMenuResponseSize(response, maxBytes = 128 * 1024) {
350
+ let bytes;
351
+ try {
352
+ bytes = Buffer.byteLength(JSON.stringify(response), 'utf8');
353
+ }
354
+ catch {
355
+ return menuFailure({ id: response.id, ...(response.name ? { name: response.name } : {}) }, {
356
+ code: 'INTERNAL_ERROR',
357
+ message: 'Menu response is not serializable',
358
+ });
359
+ }
360
+ if (bytes <= maxBytes)
361
+ return response;
362
+ return menuFailure({ id: response.id, ...(response.name ? { name: response.name } : {}) }, {
363
+ code: 'RESPONSE_TOO_LARGE',
364
+ message: `Menu response exceeds the ${maxBytes}-byte limit`,
365
+ data: { bytes, maxBytes },
366
+ });
367
+ }
262
368
  const INVALID_ARGUMENT_CODES = new Set([
263
369
  'ARGUMENT_MISMATCH', 'FILE_TOO_LARGE', 'INVALID_ARGS', 'INVALID_DELEGATION',
264
370
  'INVALID_FIELD', 'INVALID_REVISION', 'INVALID_SCOPE', 'INVALID_TYPE', 'INVALID_VALUE', 'MISSING_AID',
@@ -272,7 +378,9 @@ const STABLE_CODES = new Set([
272
378
  'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
273
379
  'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
274
380
  'INVALID_CONTEXT', 'SCHEMA_VERSION_UNSUPPORTED', 'DEPENDENCY_UNAVAILABLE',
275
- 'RATE_LIMITED', 'CATALOG_STALE',
381
+ 'RATE_LIMITED', 'CATALOG_STALE', 'DATA_MIGRATION_REQUIRED',
382
+ 'INVALID_CLIENT', 'REMOTE_ACCESS_TOKEN_INVALID', 'ECWEB_TOKEN_INVALID', 'CLIENT_BUSY', 'INVALID_RESPONSE', 'TIMEOUT',
383
+ 'RESPONSE_TOO_LARGE',
276
384
  ]);
277
385
  export function normalizeMenuError(error) {
278
386
  const source = error && typeof error === 'object' ? error : {};
@@ -493,6 +601,9 @@ function menuFlowBaseRecord(request, context) {
493
601
  requestId: menuRequestId(request),
494
602
  messageId: context.messageId,
495
603
  selfAid: context.selfAid,
604
+ requesterAid: context.requesterAid,
605
+ authMethod: context.authMethod,
606
+ clientName: context.clientName,
496
607
  };
497
608
  }
498
609
  function menuRequestBusinessFields(request) {
@@ -531,11 +642,19 @@ export function logMenuRequestCompleted(request, response, context, options) {
531
642
  const processingMs = response && typeof response.processing_ms === 'number'
532
643
  ? response.processing_ms
533
644
  : undefined;
645
+ let responseBytes;
646
+ if (response) {
647
+ try {
648
+ responseBytes = Buffer.byteLength(JSON.stringify(response), 'utf8');
649
+ }
650
+ catch { }
651
+ }
534
652
  writeMenuFlowRecord({
535
653
  ...menuFlowBaseRecord(request, context),
536
654
  event: options.delivery === 'failed' ? 'menu.request.failed' : 'menu.request.completed',
537
655
  delivery: options.delivery,
538
656
  processingMs,
657
+ responseBytes,
539
658
  reason: options.reason,
540
659
  result: response && 'data' in response ? sanitizeMenuLogValue(response.data) : undefined,
541
660
  warning: response && 'warning' in response ? sanitizeMenuLogValue(response.warning) : undefined,
@@ -43,9 +43,9 @@ export function probeSessionActivity(evidence, probes) {
43
43
  return evaluateSessionActivity(collected);
44
44
  }
45
45
  // 支持的命令列表
46
- const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/renew', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
46
+ const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/renew', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair'];
47
47
  const deprecatedCommands = ['/clear'];
48
- const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop']);
48
+ const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop', '/pair']);
49
49
  /** Parse the execution command without treating it as a quick/result command. */
50
50
  export function parseFullAccessCommand(content) {
51
51
  const match = content.match(/^\s*(\/fa|\/fullaccess)(?=\s|$)([\s\S]*)$/);
@@ -65,7 +65,7 @@ const aliases = {
65
65
  '/base': '/baseagent',
66
66
  };
67
67
  // 命令快速路径前缀(所有命令都不进入消息队列)
68
- const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
68
+ const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair'];
69
69
  /**
70
70
  * 计算两个字符串的 Levenshtein 距离(编辑距离)
71
71
  */