evolcore 0.0.17 → 0.0.18
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 +32 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +97 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { groupInfo, groupMembers } from '../../aun/msg/group.js';
|
|
3
3
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
4
4
|
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
5
|
+
import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
5
6
|
import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
|
|
6
7
|
import { readRolesConfig } from '../../config/roles.js';
|
|
7
8
|
import { normalizeMenuError } from './menu-protocol.js';
|
|
@@ -106,8 +107,13 @@ export async function resolveMenuCatalogContext(input) {
|
|
|
106
107
|
throw { code: 'FORBIDDEN', message: 'control catalog context requires a control subject' };
|
|
107
108
|
}
|
|
108
109
|
if (requested === 'process') {
|
|
109
|
-
if (input.channelScope !== 'control' || !subject.fromControlChannel || !subject.isDaemonOwner)
|
|
110
|
-
throw {
|
|
110
|
+
if (input.channelScope !== 'control' || !subject.fromControlChannel || !subject.isDaemonOwner) {
|
|
111
|
+
throw {
|
|
112
|
+
code: 'NOT_ALLOWED',
|
|
113
|
+
message: formatAuthorizationDenial('Process context requires a DaemonOwner control subject.', 'DAEMON_OWNER_REQUIRED'),
|
|
114
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
111
117
|
return makeContext(input, requested, subject, input.channelId);
|
|
112
118
|
}
|
|
113
119
|
if (requested === 'agent') {
|
|
@@ -139,9 +145,18 @@ export async function resolveMenuCatalogContext(input) {
|
|
|
139
145
|
chatType: 'private',
|
|
140
146
|
conversationId: subject.actorId,
|
|
141
147
|
fromControlChannel: subject.fromControlChannel,
|
|
142
|
-
identity: subject.isDaemonOwner ? subject.identity : undefined,
|
|
143
148
|
});
|
|
144
|
-
const
|
|
149
|
+
const processRole = subject.processRole ?? (subject.isDaemonOwner ? 'daemon-owner' : 'none');
|
|
150
|
+
const bound = {
|
|
151
|
+
...rebuilt,
|
|
152
|
+
channel: subject.channel,
|
|
153
|
+
channelId: subject.actorId,
|
|
154
|
+
peerKey: formatPeerKey('aun', subject.actorId),
|
|
155
|
+
processRole,
|
|
156
|
+
isDaemonOwner: processRole === 'daemon-owner',
|
|
157
|
+
fromControlChannel: subject.fromControlChannel,
|
|
158
|
+
allowAccess: processRole !== 'none' || rebuilt.allowAccess,
|
|
159
|
+
};
|
|
145
160
|
const access = authorizeAccess(bound);
|
|
146
161
|
if (!access.allow)
|
|
147
162
|
throw { code: access.code, message: access.reason };
|
|
@@ -201,9 +216,19 @@ export async function resolveMenuCatalogContext(input) {
|
|
|
201
216
|
chatType: 'group',
|
|
202
217
|
conversationId: canonicalGroupId,
|
|
203
218
|
fromControlChannel: subject.fromControlChannel,
|
|
204
|
-
identity: subject.isDaemonOwner ? subject.identity : undefined,
|
|
205
219
|
});
|
|
206
|
-
const
|
|
220
|
+
const processRole = subject.processRole ?? (subject.isDaemonOwner ? 'daemon-owner' : 'none');
|
|
221
|
+
const bound = {
|
|
222
|
+
...rebuilt,
|
|
223
|
+
channel: subject.channel,
|
|
224
|
+
channelId: canonicalGroupId,
|
|
225
|
+
conversationId: canonicalGroupId,
|
|
226
|
+
peerKey,
|
|
227
|
+
processRole,
|
|
228
|
+
isDaemonOwner: processRole === 'daemon-owner',
|
|
229
|
+
fromControlChannel: subject.fromControlChannel,
|
|
230
|
+
allowAccess: processRole !== 'none' || rebuilt.allowAccess,
|
|
231
|
+
};
|
|
207
232
|
const access = authorizeAccess(bound);
|
|
208
233
|
if (!access.allow)
|
|
209
234
|
throw { code: access.code, message: access.reason };
|
|
@@ -278,7 +303,7 @@ async function operationView(binding, context) {
|
|
|
278
303
|
return undefined;
|
|
279
304
|
if (binding.supported === false)
|
|
280
305
|
return { supported: false, allowed: false, reason: { code: 'NOT_SUPPORTED', message: 'operation is not supported' } };
|
|
281
|
-
if (binding.operation === 'group.rulespolicy.update' && context.subject.role !== 'owner'
|
|
306
|
+
if (binding.operation === 'group.rulespolicy.update' && context.subject.role !== 'owner') {
|
|
282
307
|
return { supported: true, allowed: false, reason: { code: 'PERMISSION_DENIED', message: 'rulesPolicy can only be changed by an Agent owner' } };
|
|
283
308
|
}
|
|
284
309
|
const decision = authorizeOperation({ source: 'menu', intent: intentFor(binding, context), subject: context.subject, audit: false });
|
|
@@ -27,6 +27,7 @@ import { isCapabilityType, listCapabilityOptions, queryCapabilityTypes, resolveC
|
|
|
27
27
|
import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArgv, withDefaultRelationContext } from './cli-intent-parser.js';
|
|
28
28
|
import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
|
|
29
29
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
30
|
+
import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
30
31
|
import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
|
|
31
32
|
import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
|
|
32
33
|
import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
|
|
@@ -854,13 +855,21 @@ function gateControlScope(opts) {
|
|
|
854
855
|
if (fromControlChannel)
|
|
855
856
|
return null;
|
|
856
857
|
if (isProcessLevelAction(cmdBase, action)) {
|
|
857
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
error: formatAuthorizationDenial('This daemon-level operation is not available from an Agent channel.', 'DAEMON_OWNER_REQUIRED'),
|
|
860
|
+
code: 'NOT_ALLOWED',
|
|
861
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
862
|
+
};
|
|
858
863
|
}
|
|
859
864
|
const targetAid = args?.aid;
|
|
860
865
|
if (targetAid) {
|
|
861
866
|
const currentAgentAid = this.getOwningAgent?.(channel)?.aid;
|
|
862
867
|
if (targetAid !== currentAgentAid) {
|
|
863
|
-
return {
|
|
868
|
+
return {
|
|
869
|
+
error: formatAuthorizationDenial('An Agent channel may only target its current Agent.', 'TARGET_SELF_ONLY'),
|
|
870
|
+
code: 'NOT_ALLOWED',
|
|
871
|
+
data: authorizationDenialData('TARGET_SELF_ONLY'),
|
|
872
|
+
};
|
|
864
873
|
}
|
|
865
874
|
}
|
|
866
875
|
return null;
|
|
@@ -983,8 +992,12 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
|
|
|
983
992
|
return intent('agent.reload', fromControlChannel ? 'control' : 'agent', { action });
|
|
984
993
|
if (action === 'create')
|
|
985
994
|
return intent('agent.create', 'control', { action });
|
|
986
|
-
if (action === 'delete'
|
|
995
|
+
if (action === 'delete')
|
|
987
996
|
return intent('agent.delete', 'control', { action });
|
|
997
|
+
if (action === 'enable')
|
|
998
|
+
return intent('agent.enable', 'control', { action });
|
|
999
|
+
if (action === 'disable')
|
|
1000
|
+
return intent('agent.disable', 'control', { action });
|
|
988
1001
|
}
|
|
989
1002
|
if (cmdBase === '/trigger') {
|
|
990
1003
|
if (action === 'create' || action === 'set')
|
|
@@ -1088,7 +1101,12 @@ async function authorizeMenuIntent(params) {
|
|
|
1088
1101
|
},
|
|
1089
1102
|
};
|
|
1090
1103
|
}
|
|
1091
|
-
|
|
1104
|
+
const reasonCode = decision.command?.reasonCode;
|
|
1105
|
+
return {
|
|
1106
|
+
error: formatAuthorizationDenial(decision.reason, reasonCode),
|
|
1107
|
+
code: decision.code,
|
|
1108
|
+
...(reasonCode ? { data: authorizationDenialData(reasonCode) } : {}),
|
|
1109
|
+
};
|
|
1092
1110
|
}
|
|
1093
1111
|
return null;
|
|
1094
1112
|
}
|
|
@@ -1136,8 +1154,6 @@ function buildRoleMenuContext(owner, channel, subject) {
|
|
|
1136
1154
|
},
|
|
1137
1155
|
} : {}),
|
|
1138
1156
|
availableBaseagents: Array.from(new Set([
|
|
1139
|
-
...Object.keys(owningAgent?.config?.baseagents ?? {}),
|
|
1140
|
-
...(owningAgent?.baseagent ? [owningAgent.baseagent] : []),
|
|
1141
1157
|
...(owningAgent?.name ? (owner.getAvailableBaseagentsForOwner?.(owningAgent.name) ?? []) : []),
|
|
1142
1158
|
])),
|
|
1143
1159
|
listModels: async (baseagent) => {
|
|
@@ -1187,9 +1203,6 @@ async function executeGroupMenu(owner, params) {
|
|
|
1187
1203
|
return menuResultFailure(error);
|
|
1188
1204
|
}
|
|
1189
1205
|
const authorizationArgs = groupMenuAuthorizationArgs(params.args, params.subject.selfAid, menuIntent.scope === 'relation' ? params.subject.peerKey : undefined);
|
|
1190
|
-
const authorizationSubject = params.subject.isDaemonOwner && params.subject.role !== 'owner'
|
|
1191
|
-
? { ...params.subject, role: 'owner' }
|
|
1192
|
-
: params.subject;
|
|
1193
1206
|
const denied = await authorizeMenuIntent.call(owner, {
|
|
1194
1207
|
intent: {
|
|
1195
1208
|
operation: menuIntent.operation,
|
|
@@ -1198,7 +1211,7 @@ async function executeGroupMenu(owner, params) {
|
|
|
1198
1211
|
args: authorizationArgs,
|
|
1199
1212
|
},
|
|
1200
1213
|
identity: params.identity,
|
|
1201
|
-
subject:
|
|
1214
|
+
subject: params.subject,
|
|
1202
1215
|
session: params.session,
|
|
1203
1216
|
explicitChatType: params.explicitChatType,
|
|
1204
1217
|
channel: params.channel,
|
|
@@ -1541,16 +1554,15 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
|
|
|
1541
1554
|
throw { code: res.code, message: res.error };
|
|
1542
1555
|
return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
|
|
1543
1556
|
}
|
|
1544
|
-
// agent channel
|
|
1557
|
+
// agent channel:直接读取自身,避免先枚举全部 Agent 再过滤。
|
|
1545
1558
|
const selfAid = this.getOwningAgent?.(channel)?.aid;
|
|
1546
1559
|
if (!selfAid)
|
|
1547
1560
|
throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
|
|
1548
|
-
const res = await
|
|
1561
|
+
const res = await execAgentQuery({ aid: selfAid });
|
|
1549
1562
|
if ('error' in res)
|
|
1550
1563
|
throw { code: res.code, message: res.error };
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
|
|
1564
|
+
const ag = res.data;
|
|
1565
|
+
return [{ value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status }];
|
|
1554
1566
|
}
|
|
1555
1567
|
if (cmd === '/capability') {
|
|
1556
1568
|
const type = args?.type;
|
|
@@ -1890,7 +1902,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
|
|
|
1890
1902
|
// 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
|
|
1891
1903
|
if (cmdBase === '/agent') {
|
|
1892
1904
|
if (fromControlChannel) {
|
|
1893
|
-
return await execAgentQuery(args);
|
|
1905
|
+
return args?.aid ? await execAgentQuery(args) : await execAgentOptions(args);
|
|
1894
1906
|
}
|
|
1895
1907
|
const selfAid = this.getOwningAgent?.(channel)?.aid;
|
|
1896
1908
|
if (!selfAid)
|
|
@@ -2727,7 +2739,11 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2727
2739
|
subject: authSubject,
|
|
2728
2740
|
});
|
|
2729
2741
|
if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner) {
|
|
2730
|
-
return {
|
|
2742
|
+
return {
|
|
2743
|
+
error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
2744
|
+
code: 'NOT_ALLOWED',
|
|
2745
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
2746
|
+
};
|
|
2731
2747
|
}
|
|
2732
2748
|
if (cmdBase === '/role') {
|
|
2733
2749
|
const authorized = await authorizeRoleMenu(this, {
|
|
@@ -2808,7 +2824,11 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2808
2824
|
if (cmdBase === '/agent') {
|
|
2809
2825
|
if (fromControlChannel) {
|
|
2810
2826
|
if (!subject.isDaemonOwner) {
|
|
2811
|
-
return {
|
|
2827
|
+
return {
|
|
2828
|
+
error: formatAuthorizationDenial('This Agent lifecycle operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
2829
|
+
code: 'NOT_ALLOWED',
|
|
2830
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
2831
|
+
};
|
|
2812
2832
|
}
|
|
2813
2833
|
}
|
|
2814
2834
|
else {
|
|
@@ -3323,7 +3343,11 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3323
3343
|
if (cmdBase === '/system') {
|
|
3324
3344
|
// D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
|
|
3325
3345
|
if (!subject.isDaemonOwner) {
|
|
3326
|
-
return {
|
|
3346
|
+
return {
|
|
3347
|
+
error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
3348
|
+
code: 'NOT_ALLOWED',
|
|
3349
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
3350
|
+
};
|
|
3327
3351
|
}
|
|
3328
3352
|
if (action === 'restart') {
|
|
3329
3353
|
const suppressRealRestart = shouldSuppressRealRestart();
|
|
@@ -3475,6 +3499,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3475
3499
|
requestId, sessionId: subject.sessionId, agentAid: subject.selfAid,
|
|
3476
3500
|
permissionMode: subject.permissionMode,
|
|
3477
3501
|
channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
|
|
3502
|
+
executed: false, executionState: 'blocked',
|
|
3478
3503
|
reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
|
|
3479
3504
|
name: 'cli', action: 'exec', args: { argv: [...requestedArgv] },
|
|
3480
3505
|
});
|
|
@@ -3534,6 +3559,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3534
3559
|
permissionMode: subject.permissionMode,
|
|
3535
3560
|
channel, channelId, role: identity.role, isDaemonOwner,
|
|
3536
3561
|
fromControlChannel: fromControlChannel ?? false, decision: 'allow',
|
|
3562
|
+
executed: true,
|
|
3563
|
+
executionState: executionData?.exitCode === undefined || executionData.exitCode === 0 ? 'completed' : 'failed',
|
|
3537
3564
|
matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
|
|
3538
3565
|
name: 'cli', action: 'exec',
|
|
3539
3566
|
args: { argv: [...requestedArgv] },
|
|
@@ -3665,7 +3692,6 @@ async function execMenuForSystemControl(payload, context) {
|
|
|
3665
3692
|
channelId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
|
|
3666
3693
|
chatType: 'private',
|
|
3667
3694
|
conversationId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
|
|
3668
|
-
identity: context.localDirect ? { role: 'owner', mode: 'interactive' } : undefined,
|
|
3669
3695
|
processOwners: context.owners,
|
|
3670
3696
|
fromControlChannel: true,
|
|
3671
3697
|
});
|
|
@@ -3788,7 +3814,11 @@ export async function execMenuForEcweb(payload, trusted) {
|
|
|
3788
3814
|
const isProcessLevel = isProcessLevelMenu(name, payload?.cmd) || isProcessContextMenu(payload);
|
|
3789
3815
|
const owners = loadDaemonConfig().owners ?? [];
|
|
3790
3816
|
if (isProcessLevel && owners.length === 0) {
|
|
3791
|
-
return {
|
|
3817
|
+
return menuFailure({ id, ...(name ? { name } : {}) }, {
|
|
3818
|
+
code: 'NOT_ALLOWED',
|
|
3819
|
+
message: formatAuthorizationDenial('Configure daemon.json.owners before using process-level operations.', 'DAEMON_OWNER_REQUIRED'),
|
|
3820
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
3821
|
+
});
|
|
3792
3822
|
}
|
|
3793
3823
|
if (!trusted || (!trusted.localDirect && !trusted.actorAid)) {
|
|
3794
3824
|
return menuFailure({ id, ...(name ? { name } : {}) }, {
|
|
@@ -3797,7 +3827,9 @@ export async function execMenuForEcweb(payload, trusted) {
|
|
|
3797
3827
|
data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
|
|
3798
3828
|
});
|
|
3799
3829
|
}
|
|
3800
|
-
|
|
3830
|
+
// A verified local ECWeb connection acts through the configured human
|
|
3831
|
+
// DaemonOwner identity; it is not an anonymous daemon service principal.
|
|
3832
|
+
const userId = trusted.actorAid ?? (trusted.localDirect ? (owners[0] || 'local-direct') : undefined);
|
|
3801
3833
|
return execMenuForSystemControl.call(this, payload, {
|
|
3802
3834
|
source: 'ecweb',
|
|
3803
3835
|
actorAid: userId || 'local-direct',
|
|
@@ -3819,7 +3851,11 @@ export async function execMenuForControl(payload, peerId) {
|
|
|
3819
3851
|
const isRoleRequest = name === 'role' || compatCmd === '/role';
|
|
3820
3852
|
const isConnectRequest = name === 'connect' || compatCmd === '/connect';
|
|
3821
3853
|
if (!isRoleRequest && !isConnectRequest && !isProcessLevelOwner(peerId, owners)) {
|
|
3822
|
-
return menuFailure({ id, ...(name ? { name } : {}) }, {
|
|
3854
|
+
return menuFailure({ id, ...(name ? { name } : {}) }, {
|
|
3855
|
+
code: 'NOT_ALLOWED',
|
|
3856
|
+
message: formatAuthorizationDenial('Control-channel process operations require DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
|
|
3857
|
+
data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
|
|
3858
|
+
});
|
|
3823
3859
|
}
|
|
3824
3860
|
return execMenuForSystemControl.call(this, payload, {
|
|
3825
3861
|
source: 'control',
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
3
4
|
import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
4
5
|
import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
5
7
|
export const MENU_REQUEST_TYPES = new Set([
|
|
6
8
|
'menu.token.request',
|
|
7
9
|
'menu.list',
|
|
@@ -40,6 +42,51 @@ export function menuCommandForName(name) {
|
|
|
40
42
|
return undefined;
|
|
41
43
|
return MENU_NAME_COMMANDS[name];
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Preserve a valid adapter receive mark, or capture a local monotonic fallback
|
|
47
|
+
* at the earliest boundary available to the caller.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeMenuResponseTiming(timing, clock = {}) {
|
|
50
|
+
const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
|
|
51
|
+
? timing.receivedAtMono
|
|
52
|
+
: undefined;
|
|
53
|
+
const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
|
|
54
|
+
? timing.receivedAt
|
|
55
|
+
: undefined;
|
|
56
|
+
if (receivedAtMono !== undefined) {
|
|
57
|
+
return { ...(receivedAt !== undefined ? { receivedAt } : {}), receivedAtMono };
|
|
58
|
+
}
|
|
59
|
+
if (receivedAt !== undefined)
|
|
60
|
+
return { receivedAt };
|
|
61
|
+
return {
|
|
62
|
+
receivedAt: clock.wallNow ?? Date.now(),
|
|
63
|
+
receivedAtMono: clock.monotonicNow ?? performance.now(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Finalize a Menu response at an external transport boundary.
|
|
68
|
+
*
|
|
69
|
+
* Keeping this outside menuSuccess/menuFailure is intentional: dedupe caches
|
|
70
|
+
* response drafts, while every inbound request (including a replay) must get
|
|
71
|
+
* its own inbound-to-outbound processing time.
|
|
72
|
+
*/
|
|
73
|
+
export function withMenuProcessingTime(response, timing, clock = {}) {
|
|
74
|
+
const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
|
|
75
|
+
? timing.receivedAtMono
|
|
76
|
+
: undefined;
|
|
77
|
+
const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
|
|
78
|
+
? timing.receivedAt
|
|
79
|
+
: undefined;
|
|
80
|
+
const elapsed = receivedAtMono !== undefined
|
|
81
|
+
? (clock.monotonicNow ?? performance.now()) - receivedAtMono
|
|
82
|
+
: receivedAt !== undefined
|
|
83
|
+
? (clock.wallNow ?? Date.now()) - receivedAt
|
|
84
|
+
: 0;
|
|
85
|
+
return {
|
|
86
|
+
...response,
|
|
87
|
+
processing_ms: Math.max(0, Math.round(Number.isFinite(elapsed) ? elapsed : 0)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
43
90
|
export function evaluateEvolMenuVersionGate(input) {
|
|
44
91
|
const minimum = loadDaemonConfig().aun?.minEvolVersion;
|
|
45
92
|
if (!minimum)
|
|
@@ -346,3 +393,152 @@ export class MenuDiagnosticLimiter {
|
|
|
346
393
|
return { log: false, suppressed: previous.suppressed };
|
|
347
394
|
}
|
|
348
395
|
}
|
|
396
|
+
const MAX_LOG_DEPTH = 8;
|
|
397
|
+
const MAX_LOG_KEYS = 100;
|
|
398
|
+
const MAX_LOG_ARRAY_ITEMS = 100;
|
|
399
|
+
const MAX_LOG_STRING_LENGTH = 4096;
|
|
400
|
+
const SENSITIVE_LOG_KEY = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret(?:[_-]?key)?|password|passwd|credentials?|authorization|cookie|private[_-]?key|seed)(?:$|[_-])/i;
|
|
401
|
+
const SENSITIVE_LOG_ARG = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret|password|passwd|credential|authorization|cookie|private[_-]?key|seed)/i;
|
|
402
|
+
function isSensitiveLogKey(key) {
|
|
403
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
|
|
404
|
+
return SENSITIVE_LOG_KEY.test(normalized);
|
|
405
|
+
}
|
|
406
|
+
function sanitizeInlineLogSecrets(value) {
|
|
407
|
+
return value
|
|
408
|
+
.replace(/\b(authorization\s*:\s*Bearer\s+)[^\s"',;]+/gi, '$1[REDACTED]')
|
|
409
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
410
|
+
.replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|password|passwd|secret|credential|authorization|cookie)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, '$1$2[REDACTED]')
|
|
411
|
+
.replace(/(--(?:api-key|access-token|refresh-token|menu-token|token|password|secret|credential))(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/gi, '$1 [REDACTED]');
|
|
412
|
+
}
|
|
413
|
+
function sanitizeLogString(value) {
|
|
414
|
+
let sanitized = value;
|
|
415
|
+
const trimmed = value.trim();
|
|
416
|
+
if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && trimmed.length <= 64 * 1024) {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(trimmed);
|
|
419
|
+
if (parsed && typeof parsed === 'object')
|
|
420
|
+
sanitized = JSON.stringify(sanitizeMenuLogValue(parsed));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Ordinary strings may start with JSON punctuation; keep their original shape.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
sanitized = sanitizeInlineLogSecrets(sanitized);
|
|
427
|
+
if (sanitized.length <= MAX_LOG_STRING_LENGTH)
|
|
428
|
+
return sanitized;
|
|
429
|
+
return `${sanitized.slice(0, MAX_LOG_STRING_LENGTH)}...[TRUNCATED ${sanitized.length - MAX_LOG_STRING_LENGTH} chars]`;
|
|
430
|
+
}
|
|
431
|
+
function sanitizeLogArgv(value, depth, seen) {
|
|
432
|
+
const output = [];
|
|
433
|
+
let redactNext = false;
|
|
434
|
+
for (const item of value.slice(0, MAX_LOG_ARRAY_ITEMS)) {
|
|
435
|
+
if (typeof item !== 'string') {
|
|
436
|
+
output.push(sanitizeMenuLogValue(item, '', depth + 1, seen));
|
|
437
|
+
redactNext = false;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (redactNext) {
|
|
441
|
+
output.push('[REDACTED]');
|
|
442
|
+
redactNext = false;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const sanitized = sanitizeLogString(item);
|
|
446
|
+
output.push(sanitized);
|
|
447
|
+
const token = item.replace(/^--?/, '').replace(/=.*$/, '');
|
|
448
|
+
redactNext = SENSITIVE_LOG_ARG.test(token) && !item.includes('=');
|
|
449
|
+
}
|
|
450
|
+
if (value.length > MAX_LOG_ARRAY_ITEMS)
|
|
451
|
+
output.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
|
|
452
|
+
return output;
|
|
453
|
+
}
|
|
454
|
+
export function sanitizeMenuLogValue(value, key = '', depth = 0, seen = new WeakSet()) {
|
|
455
|
+
if (isSensitiveLogKey(key))
|
|
456
|
+
return '[REDACTED]';
|
|
457
|
+
if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean')
|
|
458
|
+
return value;
|
|
459
|
+
if (typeof value === 'bigint')
|
|
460
|
+
return value.toString();
|
|
461
|
+
if (typeof value === 'string')
|
|
462
|
+
return sanitizeLogString(value);
|
|
463
|
+
if (typeof value !== 'object')
|
|
464
|
+
return String(value);
|
|
465
|
+
if (depth >= MAX_LOG_DEPTH)
|
|
466
|
+
return '[MAX_DEPTH]';
|
|
467
|
+
if (seen.has(value))
|
|
468
|
+
return '[CIRCULAR]';
|
|
469
|
+
seen.add(value);
|
|
470
|
+
if (Array.isArray(value)) {
|
|
471
|
+
const items = value.slice(0, MAX_LOG_ARRAY_ITEMS).map(item => sanitizeMenuLogValue(item, '', depth + 1, seen));
|
|
472
|
+
if (value.length > MAX_LOG_ARRAY_ITEMS)
|
|
473
|
+
items.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
|
|
474
|
+
return items;
|
|
475
|
+
}
|
|
476
|
+
const output = {};
|
|
477
|
+
const entries = Object.entries(value);
|
|
478
|
+
for (const [entryKey, entryValue] of entries.slice(0, MAX_LOG_KEYS)) {
|
|
479
|
+
output[entryKey] = entryKey === 'argv' && Array.isArray(entryValue)
|
|
480
|
+
? sanitizeLogArgv(entryValue, depth + 1, seen)
|
|
481
|
+
: sanitizeMenuLogValue(entryValue, entryKey, depth + 1, seen);
|
|
482
|
+
}
|
|
483
|
+
if (entries.length > MAX_LOG_KEYS)
|
|
484
|
+
output.$truncated_keys = entries.length - MAX_LOG_KEYS;
|
|
485
|
+
return output;
|
|
486
|
+
}
|
|
487
|
+
function menuRequestId(request) {
|
|
488
|
+
return typeof request.id === 'string' && request.id.trim() ? request.id : undefined;
|
|
489
|
+
}
|
|
490
|
+
function menuFlowBaseRecord(request, context) {
|
|
491
|
+
return {
|
|
492
|
+
source: context.source,
|
|
493
|
+
requestId: menuRequestId(request),
|
|
494
|
+
messageId: context.messageId,
|
|
495
|
+
selfAid: context.selfAid,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function menuRequestBusinessFields(request) {
|
|
499
|
+
return sanitizeMenuLogValue({
|
|
500
|
+
type: request.type,
|
|
501
|
+
name: request.name,
|
|
502
|
+
agent: request.agent,
|
|
503
|
+
action: request.action,
|
|
504
|
+
cmd: request.cmd,
|
|
505
|
+
args: request.args,
|
|
506
|
+
value: request.value,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function writeMenuFlowRecord(record) {
|
|
510
|
+
try {
|
|
511
|
+
logger.menu(record);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Observability must never affect Menu execution.
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
export function logMenuRequestReceived(request, context) {
|
|
518
|
+
writeMenuFlowRecord({
|
|
519
|
+
...menuFlowBaseRecord(request, context),
|
|
520
|
+
event: 'menu.request.received',
|
|
521
|
+
request: menuRequestBusinessFields(request),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
export function logMenuRequestCompleted(request, response, context, options) {
|
|
525
|
+
const error = response && 'error' in response ? response.error : undefined;
|
|
526
|
+
const transportError = options.transportError instanceof Error
|
|
527
|
+
? { message: sanitizeLogString(options.transportError.message) }
|
|
528
|
+
: options.transportError === undefined
|
|
529
|
+
? undefined
|
|
530
|
+
: { message: sanitizeLogString(String(options.transportError)) };
|
|
531
|
+
const processingMs = response && typeof response.processing_ms === 'number'
|
|
532
|
+
? response.processing_ms
|
|
533
|
+
: undefined;
|
|
534
|
+
writeMenuFlowRecord({
|
|
535
|
+
...menuFlowBaseRecord(request, context),
|
|
536
|
+
event: options.delivery === 'failed' ? 'menu.request.failed' : 'menu.request.completed',
|
|
537
|
+
delivery: options.delivery,
|
|
538
|
+
processingMs,
|
|
539
|
+
reason: options.reason,
|
|
540
|
+
result: response && 'data' in response ? sanitizeMenuLogValue(response.data) : undefined,
|
|
541
|
+
warning: response && 'warning' in response ? sanitizeMenuLogValue(response.warning) : undefined,
|
|
542
|
+
error: error ? sanitizeMenuLogValue(error) : transportError,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 支持的命令列表
|
|
2
|
-
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
2
|
+
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
3
3
|
const deprecatedCommands = ['/clear'];
|
|
4
|
+
const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop']);
|
|
4
5
|
// 命令别名映射
|
|
5
6
|
const aliases = {
|
|
6
7
|
'/s': '/session',
|
|
@@ -9,7 +10,7 @@ const aliases = {
|
|
|
9
10
|
'/base': '/baseagent',
|
|
10
11
|
};
|
|
11
12
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
12
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
13
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
13
14
|
/**
|
|
14
15
|
* 计算两个字符串的 Levenshtein 距离(编辑距离)
|
|
15
16
|
*/
|
|
@@ -39,7 +40,9 @@ function levenshteinDistance(str1, str2) {
|
|
|
39
40
|
return matrix[len1][len2];
|
|
40
41
|
}
|
|
41
42
|
export function isQuickCommand(content) {
|
|
42
|
-
return content === '/s' || quickCommandPrefixes.some(cmd =>
|
|
43
|
+
return content === '/s' || quickCommandPrefixes.some(cmd => exactBoundaryCommands.has(cmd)
|
|
44
|
+
? content === cmd || content.startsWith(cmd + ' ')
|
|
45
|
+
: content.startsWith(cmd));
|
|
43
46
|
}
|
|
44
47
|
export function normalizeSlashContent(content) {
|
|
45
48
|
for (const [alias, full] of Object.entries(aliases)) {
|
|
@@ -50,7 +53,9 @@ export function normalizeSlashContent(content) {
|
|
|
50
53
|
return content;
|
|
51
54
|
}
|
|
52
55
|
export function isRecognizedSlashCommand(content) {
|
|
53
|
-
return commands.some(cmd =>
|
|
56
|
+
return commands.some(cmd => exactBoundaryCommands.has(cmd)
|
|
57
|
+
? content === cmd || content.startsWith(cmd + ' ')
|
|
58
|
+
: content.startsWith(cmd)) ||
|
|
54
59
|
deprecatedCommands.some(cmd => content === cmd || content.startsWith(cmd + ' '));
|
|
55
60
|
}
|
|
56
61
|
export function guardThreadCommand(content, threadId) {
|
|
@@ -70,7 +75,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
|
|
|
70
75
|
// visitor/member 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
|
|
71
76
|
const userGroupCommands = [
|
|
72
77
|
'/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
|
|
73
|
-
'/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
|
|
78
|
+
'/model', '/effort', '/baseagent', '/perm', '/activity', '/stop', '/pause',
|
|
74
79
|
'/resume', '/trigger', '/file',
|
|
75
80
|
];
|
|
76
81
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
@@ -107,6 +112,8 @@ export async function guardIdleCommand(opts) {
|
|
|
107
112
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
108
113
|
const threadSession = await opts.sessionManager.getThreadSession(opts.channel, opts.channelId, opts.threadId);
|
|
109
114
|
if (threadSession) {
|
|
115
|
+
if (opts.isSessionPaused?.(threadSession.id))
|
|
116
|
+
return undefined;
|
|
110
117
|
let hasActiveStream = false;
|
|
111
118
|
try {
|
|
112
119
|
hasActiveStream = opts.getAgentForSession(threadSession).hasActiveStream(threadSession.id);
|
|
@@ -123,6 +130,8 @@ export async function guardIdleCommand(opts) {
|
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
else if (opts.activeSession) {
|
|
133
|
+
if (opts.isSessionPaused?.(opts.activeSession.id))
|
|
134
|
+
return undefined;
|
|
126
135
|
const isBusy = (opts.activeAgent?.hasActiveStream(opts.activeSession.id) ?? false) ||
|
|
127
136
|
opts.messageQueue?.isProcessing(opts.activeSession.id) ||
|
|
128
137
|
(opts.messageQueue?.getQueueLength(opts.activeSession.id) ?? 0) > 0;
|