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.
- package/CHANGELOG.md +74 -14
- package/README.md +4 -0
- package/bin/codex-managed-acl.ps1 +160 -0
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +384 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +30 -2
- package/dist/agents/claude-runner.js +179 -240
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +369 -0
- package/dist/agents/codex-runner.js +839 -192
- package/dist/agents/ecagent-runner.js +27 -34
- package/dist/agents/gemini-runner.js +62 -8
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +43 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +57 -5
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +7 -1
- package/dist/channels/aun.js +178 -77
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wechat.js +81 -10
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +108 -24
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +289 -61
- package/dist/cli/index.js +22 -2
- package/dist/cli/init.js +2 -10
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +75 -18
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +14 -2
- package/dist/core/auth/remote-access-store.js +136 -0
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +128 -30
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +6 -1
- package/dist/core/command/menu-handler.js +410 -98
- package/dist/core/command/menu-protocol.js +123 -0
- package/dist/core/command/slash-gate.js +4 -6
- package/dist/core/command/slash-handler.js +199 -70
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +147 -94
- package/dist/core/model/catalog-service.js +310 -0
- package/dist/core/model/model-catalog.js +134 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +5 -2
- package/dist/core/permission/approval-gateway.js +183 -32
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +257 -31
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +214 -57
- package/dist/ipc.js +191 -102
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +16 -2
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +70 -15
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +16 -4
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
/**
|
|
4048
|
-
*
|
|
4049
|
-
*
|
|
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
|
-
|
|
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:
|
|
4063
|
-
message:
|
|
4064
|
-
data:
|
|
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:
|
|
4381
|
+
actorAid: managementActor,
|
|
4070
4382
|
localDirect: false,
|
|
4071
4383
|
canApprovePersistentFullAccess: false,
|
|
4072
4384
|
owners,
|