evolcore 0.0.22 → 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 (75) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +24 -14
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/install-codex-managed-hooks.mjs +373 -95
  6. package/dist/agents/baseagent-availability.js +48 -0
  7. package/dist/agents/baseagent.js +1 -1
  8. package/dist/agents/claude-runner.js +30 -226
  9. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
  10. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  11. package/dist/agents/codex/catalog.js +70 -0
  12. package/dist/agents/codex/diagnostics.js +154 -0
  13. package/dist/agents/codex-runner.js +407 -139
  14. package/dist/agents/ecagent-runner.js +10 -31
  15. package/dist/agents/gemini-runner.js +2 -6
  16. package/dist/aun/aid/store.js +2 -0
  17. package/dist/aun/menu-client.js +272 -0
  18. package/dist/aun/msg/group.js +65 -4
  19. package/dist/aun/msg/index.js +1 -1
  20. package/dist/aun/msg/managed-operation.js +51 -4
  21. package/dist/aun/rpc/client.js +8 -1
  22. package/dist/channels/aun.js +31 -1
  23. package/dist/channels/wechat.js +81 -10
  24. package/dist/cli/aun-commands.js +48 -4
  25. package/dist/cli/command-log.js +21 -2
  26. package/dist/cli/daemon-commands.js +142 -45
  27. package/dist/cli/index.js +20 -0
  28. package/dist/cli/init.js +2 -10
  29. package/dist/cli/menu-client.js +95 -0
  30. package/dist/cli/model.js +23 -8
  31. package/dist/cli/task-context.js +12 -2
  32. package/dist/cli/trigger-command.js +9 -1
  33. package/dist/config/schema-migration.js +14 -2
  34. package/dist/core/auth/agent-delegation.js +10 -7
  35. package/dist/core/auth/authorization-audit.js +4 -0
  36. package/dist/core/auth/operation-catalog.js +4 -0
  37. package/dist/core/auth/remote-access-store.js +135 -0
  38. package/dist/core/command/command-handler.js +41 -5
  39. package/dist/core/command/daemon-diagnostics.js +448 -0
  40. package/dist/core/command/menu-catalog.js +3 -0
  41. package/dist/core/command/menu-handler.js +141 -33
  42. package/dist/core/command/menu-protocol.js +119 -0
  43. package/dist/core/command/slash-gate.js +3 -3
  44. package/dist/core/command/slash-handler.js +42 -22
  45. package/dist/core/message/message-queue.js +20 -15
  46. package/dist/core/message/response-engine.js +74 -73
  47. package/dist/core/model/catalog-service.js +308 -0
  48. package/dist/core/model/model-catalog.js +124 -334
  49. package/dist/core/model/model-diagnostics.js +4 -5
  50. package/dist/core/model/model-fallback.js +9 -16
  51. package/dist/core/model/model-permission.js +1 -1
  52. package/dist/core/permission/approval-gateway.js +16 -1
  53. package/dist/core/permission/protected-paths.js +63 -4
  54. package/dist/core/permission/sandbox-runtime.js +4 -2
  55. package/dist/core/permission/tool-policy.js +244 -25
  56. package/dist/core/runtime-lock.js +13 -8
  57. package/dist/index.js +40 -29
  58. package/dist/ipc.js +123 -81
  59. package/dist/trigger/manager.js +3 -1
  60. package/dist/trigger/parser.js +77 -13
  61. package/dist/trigger/patch.js +104 -1
  62. package/dist/utils/ecweb-supervisor.js +11 -2
  63. package/dist/utils/ecweb-utils.js +8 -26
  64. package/dist/utils/error-utils.js +7 -0
  65. package/dist/utils/npm-ops.js +47 -15
  66. package/kits/docs/evolcore/INDEX.md +1 -0
  67. package/kits/docs/evolcore/agent-create.md +13 -0
  68. package/kits/docs/evolcore/group-collaboration.md +7 -2
  69. package/kits/docs/evolcore/menu-client.md +34 -0
  70. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  71. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  72. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  73. package/kits/templates/roles/visitor.json +1 -0
  74. package/kits/templates/system-fragments/bootstrap.md +8 -1
  75. package/package.json +11 -3
@@ -1,9 +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, 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';
9
8
  import { inspectDataMigrationRequirement } from '../data-migration.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';
@@ -77,7 +79,7 @@ function getBaseagentVersion(cmd) {
77
79
  return null;
78
80
  }
79
81
  }
80
- const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
82
+ const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
81
83
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
82
84
  /** menu file: fetch 文件大小上限(与 /file 一致) */
83
85
  const FILE_FETCH_MAX_SIZE = 10 * 1024 * 1024;
@@ -394,14 +396,8 @@ function tokenizeArgv(line) {
394
396
  }
395
397
  return out;
396
398
  }
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 [];
399
+ async function getAvailableEfforts(agent, model) {
400
+ return (await agent.getAvailableEfforts?.(model) ?? []);
405
401
  }
406
402
  function modelDisplayLabel(agent, model) {
407
403
  const full = agent.resolveModelId?.(model);
@@ -943,6 +939,10 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
943
939
  return intent('trigger.list', 'relation');
944
940
  }
945
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');
946
946
  if (cmdBase === '/agent')
947
947
  return intent(fromControlChannel ? 'agent.list' : 'agent.show', fromControlChannel ? 'control' : 'agent');
948
948
  if (cmdBase === '/capability')
@@ -1284,8 +1284,14 @@ function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, requir
1284
1284
  : { channel: fallbackChannel };
1285
1285
  }
1286
1286
  const agent = owner.agentRegistry?.get?.(requestedAid) ?? null;
1287
- 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' };
1288
1293
  return { error: `未找到 Agent: ${requestedAid}`, code: 'NOT_FOUND' };
1294
+ }
1289
1295
  const channel = agent.channelInstanceNames?.()[0];
1290
1296
  if (!channel)
1291
1297
  return { error: `Agent 无可用 channel: ${requestedAid}`, code: 'NOT_FOUND' };
@@ -1471,6 +1477,9 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1471
1477
  ...(isControlScope && canManageProcess && canOperation('system.restart', 'process') ? [
1472
1478
  { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1473
1479
  ] : []),
1480
+ ...(isControlScope && canManageProcess && canOperation('system.status', 'process') ? [
1481
+ { cmd: '/system', label: 'Daemon 诊断', desc: '查询 daemon 状态、监控快照和日志' },
1482
+ ] : []),
1474
1483
  ...(canUseFile ? [
1475
1484
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1476
1485
  ] : []),
@@ -1567,6 +1576,18 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1567
1576
  });
1568
1577
  if (optionsAuthDenied)
1569
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
+ }
1570
1591
  // ── /agent list(只读) ──
1571
1592
  // 控制 channel:返回全量;agent channel:仅返回自身单条。
1572
1593
  if (cmd === '/agent') {
@@ -1607,7 +1628,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1607
1628
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1608
1629
  const scope = args?.options === 'all' ? 'all' : 'enabled';
1609
1630
  const role = optionsIdentity.role;
1610
- const isAdmin = isManagementRole(role);
1631
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
1611
1632
  if (!triggerScheduler)
1612
1633
  return [];
1613
1634
  const list = triggerScheduler.list({ all: scope === 'all' });
@@ -1711,13 +1732,15 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1711
1732
  if ('error' in target)
1712
1733
  throw { code: target.code, message: target.error };
1713
1734
  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));
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);
1717
1740
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1718
1741
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1719
1742
  if (models.length > 0)
1720
- 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) }));
1721
1744
  }
1722
1745
  return null;
1723
1746
  }
@@ -1774,7 +1797,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1774
1797
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1775
1798
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1776
1799
  const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
1777
- const efforts = getAvailableEfforts(agent, currentModel);
1800
+ const efforts = await getAvailableEfforts(agent, currentModel);
1778
1801
  const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent).value ?? 'auto';
1779
1802
  const allItems = [...efforts, 'auto'];
1780
1803
  return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
@@ -1950,9 +1973,25 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1950
1973
  }
1951
1974
  // ── /config 查询(只读,查询各层配置) ──
1952
1975
  if (cmdBase === '/config') {
1976
+ if (args?.view === 'rawFile') {
1977
+ return { data: readRawConfigResource(args, subject.actorId ?? '') };
1978
+ }
1953
1979
  const scope = args?.scope || 'process';
1954
1980
  if (scope === 'process') {
1955
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
+ }
1956
1995
  return { data: { scope: 'process', config: cfg } };
1957
1996
  }
1958
1997
  if (scope === 'defaults') {
@@ -2284,6 +2323,23 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2284
2323
  };
2285
2324
  }
2286
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' };
2287
2343
  const data = {
2288
2344
  aid: loadDaemonConfig().aid ?? null,
2289
2345
  pid: process.pid,
@@ -2460,7 +2516,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2460
2516
  }
2461
2517
  if (!patch?.nameOrId)
2462
2518
  return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2463
- const isAdmin = isManagementRole(identity.role);
2519
+ const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner;
2464
2520
  if (!isAdmin && !userId)
2465
2521
  return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2466
2522
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
@@ -2554,13 +2610,16 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2554
2610
  const agent = this.getAgent(channel, target.baseagent);
2555
2611
  let targetModel = arg;
2556
2612
  if (hasModelSwitcher(agent)) {
2557
- 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);
2558
2616
  const decision = validateModelSelectionForRole({
2559
2617
  role: identity.role,
2560
2618
  baseagent: target.baseagent,
2561
2619
  requestedModel: arg,
2562
2620
  models,
2563
- resolveModelId: agent.resolveModelId?.bind(agent),
2621
+ selfAid: target.sel.self,
2622
+ resolveModelId: model => resolveCatalogModel(catalog, model),
2564
2623
  });
2565
2624
  if (!decision.ok)
2566
2625
  return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
@@ -2610,7 +2669,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2610
2669
  const agent = this.getAgent(channel, target.baseagent);
2611
2670
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
2612
2671
  const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name;
2613
- const validEfforts = getAvailableEfforts(agent, currentModel);
2672
+ const validEfforts = await getAvailableEfforts(agent, currentModel);
2614
2673
  const allValid = [...validEfforts, 'auto'];
2615
2674
  if (!allValid.includes(arg)) {
2616
2675
  return { error: `无效推理强度: ${arg},可选: ${allValid.join(' / ')}`, code: 'INVALID_VALUE' };
@@ -2995,7 +3054,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2995
3054
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
2996
3055
  if (cmdBase === '/trigger') {
2997
3056
  const role = identity.role;
2998
- const isAdmin = isManagementRole(role);
3057
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
2999
3058
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
3000
3059
  if (!triggerScheduler)
3001
3060
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
@@ -3819,6 +3878,48 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3819
3878
  }
3820
3879
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
3821
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
+ }
3822
3923
  function menuCommandToken(cmd) {
3823
3924
  if (typeof cmd !== 'string')
3824
3925
  return undefined;
@@ -3871,7 +3972,8 @@ async function execMenuForSystemControl(payload, context) {
3871
3972
  if ('error' in target)
3872
3973
  return ecwebErr(id, name, target.code, target.error);
3873
3974
  const targetChannel = target.channel;
3874
- const targetAid = this.getOwningAgent?.(targetChannel)?.aid;
3975
+ const targetAid = this.getOwningAgent?.(targetChannel)?.aid
3976
+ ?? this.getDaemonTriggerOwnerAid?.(targetChannel);
3875
3977
  const trustedSubject = buildAuthSubject({
3876
3978
  selfAid: targetAid,
3877
3979
  actorId: context.actorAid,
@@ -4044,10 +4146,10 @@ export async function execMenuForEcweb(payload, trusted) {
4044
4146
  owners,
4045
4147
  });
4046
4148
  }
4047
- /** 控制 AID channel 专用入口:peerId 必须 ∈ evolcore.owners。
4048
- * 全量权限(进程级 + 跨 agent + 关系级),fromControlChannel=true 放行闸门。
4049
- * 与 ECWeb 入口的区别:鉴权主体是真实 peerId(而非注入 owner),按 evolcore.owners 校验。 */
4050
- 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 = {}) {
4051
4153
  const id = payload?.id ?? '';
4052
4154
  const name = payload?.name;
4053
4155
  const validationError = validateMenuRequest(payload ?? {});
@@ -4057,16 +4159,22 @@ export async function execMenuForControl(payload, peerId) {
4057
4159
  const compatCmd = menuCommandToken(payload?.cmd);
4058
4160
  const isRoleRequest = name === 'role' || compatCmd === '/role';
4059
4161
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
4060
- 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))) {
4061
4165
  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'),
4166
+ code: authorization.code,
4167
+ message: authorization.message,
4168
+ ...(authorization.data !== undefined ? { data: authorization.data } : {}),
4065
4169
  });
4066
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;
4067
4175
  return execMenuForSystemControl.call(this, payload, {
4068
4176
  source: 'control',
4069
- actorAid: peerId,
4177
+ actorAid: managementActor,
4070
4178
  localDirect: false,
4071
4179
  canApprovePersistentFullAccess: false,
4072
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',
@@ -273,6 +379,8 @@ const STABLE_CODES = new Set([
273
379
  'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
274
380
  'INVALID_CONTEXT', 'SCHEMA_VERSION_UNSUPPORTED', 'DEPENDENCY_UNAVAILABLE',
275
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
  */