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.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. 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 { code: 'FORBIDDEN', message: 'process context requires a daemon owner control subject' };
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 bound = { ...rebuilt, channel: subject.channel, channelId: subject.actorId, peerKey: formatPeerKey('aun', subject.actorId), isDaemonOwner: subject.isDaemonOwner || rebuilt.isDaemonOwner, fromControlChannel: subject.fromControlChannel };
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 bound = { ...rebuilt, channel: subject.channel, channelId: canonicalGroupId, conversationId: canonicalGroupId, peerKey, isDaemonOwner: subject.isDaemonOwner || rebuilt.isDaemonOwner, fromControlChannel: subject.fromControlChannel };
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' && !context.subject.isDaemonOwner) {
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 { error: '此操作仅允许通过控制 AID channel 执行', code: 'FORBIDDEN' };
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 { error: '跨 agent 操作仅允许通过控制 AID channel 执行', code: 'FORBIDDEN' };
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' || action === 'disable')
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
- return { error: decision.reason, code: decision.code };
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: authorizationSubject,
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 execAgentOptions(args);
1561
+ const res = await execAgentQuery({ aid: selfAid });
1549
1562
  if ('error' in res)
1550
1563
  throw { code: res.code, message: res.error };
1551
- return res.data.agents
1552
- .filter(ag => ag.aid === selfAid)
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 { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
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 { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
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 { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
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 { type: 'menu.response', id, name, error: { code: 'FORBIDDEN', message: '请在 daemon.json 配置 owners 后使用进程级操作' } };
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
- const userId = trusted.actorAid ?? (trusted.localDirect ? (isProcessLevel ? owners[0] : 'local-direct') : undefined);
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 } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: '控制 channel 操作需要 owner 权限' });
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 => content.startsWith(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 => content.startsWith(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;