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.
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +24 -14
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/install-codex-managed-hooks.mjs +373 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +30 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
- 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 +154 -0
- package/dist/agents/codex-runner.js +407 -139
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -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 +51 -4
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +31 -1
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +142 -45
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/authorization-audit.js +4 -0
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/command/command-handler.js +41 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +141 -33
- package/dist/core/command/menu-protocol.js +119 -0
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +42 -22
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/response-engine.js +74 -73
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -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 +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- 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 +244 -25
- package/dist/core/runtime-lock.js +13 -8
- package/dist/index.js +40 -29
- package/dist/ipc.js +123 -81
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/npm-ops.js +47 -15
- 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/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- 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
|
-
|
|
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)
|
|
1715
|
-
const
|
|
1716
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
4048
|
-
*
|
|
4049
|
-
*
|
|
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
|
-
|
|
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:
|
|
4063
|
-
message:
|
|
4064
|
-
data:
|
|
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:
|
|
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
|
*/
|