evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -79,6 +79,7 @@ const ENTRIES = [
79
79
  { id: 'chatmode.agent.nothuman', type: 'setting', contexts: ['agent'], name: 'chatmode', label: 'Agent 非人类对话模式', section: 'agent', order: 43, query: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.current', scope: 'agent' }, options: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.current', scope: 'agent' }, update: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.update', scope: 'agent' }, control: 'select' },
80
80
  { id: 'activity.agent', type: 'setting', contexts: ['agent'], name: 'activity', label: 'Agent 中间输出', section: 'agent', order: 50, query: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, options: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, update: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.update', scope: 'agent' }, control: 'select' },
81
81
  { id: 'observable.agent', type: 'setting', contexts: ['agent'], name: 'observable', label: '观察模式', section: 'agent', order: 60, query: { cmd: '/observable', operation: 'observable.current', scope: 'agent' }, update: { cmd: '/observable', operation: 'observable.update', scope: 'agent' }, valueSchema: { type: 'boolean' }, control: 'toggle' },
82
+ { id: 'aid.avatar', type: 'action', contexts: ['agent'], name: 'aid', label: '更换头像', section: 'agent', order: 70, action: { cmd: '/aid', action: 'avatar', operation: 'ec.aid.avatar', scope: 'agent' }, valueSchema: { type: 'object', properties: { data: { type: 'string' }, url: { type: 'string', format: 'uri' } }, oneOf: [{ required: ['data'] }, { required: ['url'] }] }, control: 'action' },
82
83
  { id: 'system.status', type: 'list', contexts: ['process'], name: 'system', label: '系统状态', section: 'process', order: 10, query: { cmd: '/system', operation: 'system.status', scope: 'process' }, control: 'list' },
83
84
  { id: 'system.monitor', type: 'list', contexts: ['process'], name: 'system', label: '运行监控快照', section: 'process', order: 11, query: { cmd: '/system', args: { view: 'monitor' }, operation: 'system.status', scope: 'process' }, control: 'list' },
84
85
  { id: 'system.logs', type: 'list', contexts: ['process'], name: 'system', label: 'Daemon 日志', section: 'process', order: 12, query: { cmd: '/system', args: { view: 'logs' }, operation: 'system.status', scope: 'process' }, options: { cmd: '/system', args: { option: 'logs' }, operation: 'system.status', scope: 'process', mode: 'dynamic' }, control: 'list' },
@@ -86,6 +87,7 @@ const ENTRIES = [
86
87
  { id: 'baseagent.models', type: 'list', contexts: ['process'], name: 'baseagent-models', label: 'Baseagent 模型目录', section: 'process', order: 41, query: { cmd: '/baseagent-models', args: { baseagent: 'claude' }, operation: 'baseagent.models.list', scope: 'process' }, control: 'list' },
87
88
  { id: 'system.restart', type: 'action', contexts: ['process'], name: 'system', label: '重启服务', section: 'process', order: 20, action: { cmd: '/system', action: 'restart', operation: 'system.restart', scope: 'process' } },
88
89
  { id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '升级服务', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', operation: 'system.upgrade', scope: 'process' } },
90
+ { id: 'system.paircode', type: 'action', contexts: ['process'], name: 'system', label: '获取配对码', section: 'process', order: 35, action: { cmd: '/system', action: 'paircode', operation: 'system.paircode', scope: 'process' } },
89
91
  { id: 'agent.list', type: 'list', contexts: ['process'], name: 'agent', label: 'Agent 列表', section: 'process', order: 40, options: { cmd: '/agent', args: { options: 'all' }, operation: 'agent.list', scope: 'control', mode: 'dynamic' }, control: 'list' },
90
92
  { id: 'agent.create', type: 'action', contexts: ['process'], name: 'agent', label: '创建 Agent', section: 'process', order: 50, action: { cmd: '/agent', action: 'create', operation: 'agent.create', scope: 'control' } },
91
93
  { id: 'agent.reload', type: 'action', contexts: ['process'], name: 'agent', label: '重载 Agent', section: 'process', order: 60, action: { cmd: '/agent', action: 'reload', operation: 'agent.reload', scope: 'control' } },
@@ -303,11 +305,18 @@ export async function resolveExplicitMenuExecutionContext(input) {
303
305
  }
304
306
  function entryVisible(entry, context) { return entry.contexts.includes(context.kind); }
305
307
  function intentFor(binding, context) { return { operation: binding.operation, scope: binding.scope, source: 'menu', args: { ...bindingArgs(binding, context) } }; }
306
- async function operationView(binding, context) {
308
+ async function operationView(binding, context, options = {}) {
307
309
  if (!binding)
308
310
  return undefined;
309
311
  if (binding.supported === false)
310
312
  return { supported: false, allowed: false, reason: { code: 'NOT_SUPPORTED', message: 'operation is not supported' } };
313
+ if (binding.operation === 'system.paircode' && options.paircodeAllowed === false) {
314
+ return {
315
+ supported: true,
316
+ allowed: false,
317
+ reason: { code: 'NOT_ALLOWED', message: 'Pairing code issuance requires encrypted DaemonOwner authentication' },
318
+ };
319
+ }
311
320
  if (binding.operation === 'group.rulespolicy.update' && context.subject.role !== 'owner') {
312
321
  return { supported: true, allowed: false, reason: { code: 'PERMISSION_DENIED', message: 'rulesPolicy can only be changed by an Agent owner' } };
313
322
  }
@@ -330,16 +339,17 @@ function wireBinding(binding, context) {
330
339
  export async function getMenuCatalog(input) {
331
340
  const context = await resolveMenuCatalogContext(input);
332
341
  const entries = [];
342
+ const paircodeAllowed = input.paircodeAllowed ?? input.channelScope !== 'control';
333
343
  for (const entry of ENTRIES.filter(candidate => entryVisible(candidate, context)).sort((a, b) => a.order - b.order)) {
334
344
  const operations = {};
335
345
  for (const [key, binding] of [['query', entry.query], ['options', entry.options], ['update', entry.update], ['action', entry.action]]) {
336
- const view = await operationView(binding, context);
346
+ const view = await operationView(binding, context, { paircodeAllowed });
337
347
  if (view)
338
348
  operations[key] = view;
339
349
  }
340
350
  entries.push({ id: entry.id, type: entry.type, name: entry.name, label: entry.label, section: entry.section, order: entry.order, target: { scope: context.kind === 'agent' ? 'agent' : context.kind === 'process' ? 'process' : 'relation', ...(context.self ? { self: context.self } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}) }, valueSchema: entry.valueSchema ?? { type: 'object' }, operations, wire: { name: entry.name, operations: { ...(wireBinding(entry.query, context) ? { query: wireBinding(entry.query, context) } : {}), ...(wireBinding(entry.options, context) ? { options: wireBinding(entry.options, context) } : {}), ...(wireBinding(entry.update, context) ? { update: wireBinding(entry.update, context) } : {}), ...(wireBinding(entry.action, context) ? { action: wireBinding(entry.action, context) } : {}) } }, ui: { control: entry.control ?? 'action' } });
341
351
  }
342
- const policyRevision = hash({ role: context.subject.role, self: context.subject.selfAid, peerKey: context.subject.peerKey, groupMembershipRole: context.groupMembershipRole, baselineRole: context.baselineRole, roles: readRolesConfig(context.subject.selfAid) });
352
+ const policyRevision = hash({ role: context.subject.role, self: context.subject.selfAid, peerKey: context.subject.peerKey, groupMembershipRole: context.groupMembershipRole, baselineRole: context.baselineRole, paircodeAllowed, roles: readRolesConfig(context.subject.selfAid) });
343
353
  const data = { $schema_version: 1, kind: 'menu.catalog', complete: true, catalogRevision: hash({ entries, context: context.contextKey, policyRevision }), policyRevision, generatedAt: Date.now(), context: { contextKey: context.contextKey, kind: context.kind, channelScope: context.channelScope, transport: context.transport, chatType: context.chatType, ...(context.self ? { self: context.self } : {}), ...(context.actor ? { actor: context.actor } : {}), ...(context.groupId ? { groupId: context.groupId } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}), requesterRole: context.requesterRole, ...(context.groupMembershipRole ? { groupMembershipRole: context.groupMembershipRole } : {}), ...(context.baselineRole !== undefined ? { baselineRole: context.baselineRole } : {}) }, features: { batchValuesV1: true, verifiedExplicitGroupContext: context.kind === 'group_relation' }, entries };
344
354
  return data;
345
355
  }
@@ -19,7 +19,7 @@ import { scanInstances } from '../../utils/instance-registry.js';
19
19
  import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
20
20
  import { WEB_PACKAGE_NAME } from '../../product.js';
21
21
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
22
- import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './agent-control.js';
22
+ import { execAgentAction, execAgentQuery, execAgentOptions, execAidAvatar, resolveProjectPath } from './agent-control.js';
23
23
  import { gatewayList, gatewayUpdate, gatewayDelete, gatewayTest, gatewayModels, gatewaySetPrice, gatewaySyncEnv } from '../../config/gateway-config.js';
24
24
  import { displaySessionTitle } from '../session/session-title.js';
25
25
  import { buildSessionTurnList } from '../session/session-turns.js';
@@ -40,7 +40,7 @@ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
40
40
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
41
41
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
42
42
  import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
43
- import { validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
43
+ import { remoteAccessService, validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
44
44
  import { listDiagnosticLogOptions, listRawConfigResources, queryDiagnosticLogs, readRawConfigResource, } from './daemon-diagnostics.js';
45
45
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
46
46
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
@@ -1015,6 +1015,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1015
1015
  return intent('system.upgrade', 'process', { action });
1016
1016
  if (action === 'check')
1017
1017
  return intent('system.status', 'process', { action });
1018
+ if (action === 'paircode')
1019
+ return intent('system.paircode', 'process', { action });
1018
1020
  }
1019
1021
  if (cmdBase === '/agent') {
1020
1022
  if (action === 'reload')
@@ -1028,6 +1030,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1028
1030
  if (action === 'disable')
1029
1031
  return intent('agent.disable', 'control', { action });
1030
1032
  }
1033
+ if (cmdBase === '/aid' && action === 'avatar')
1034
+ return intent('ec.aid.avatar', 'agent', { action });
1031
1035
  if (cmdBase === '/trigger') {
1032
1036
  if (action === 'create' || action === 'set')
1033
1037
  return intent('trigger.create', 'relation', { action });
@@ -1287,7 +1291,11 @@ function menuResultFailure(error) {
1287
1291
  return { error: normalized.message, code: normalized.code, ...(normalized.data !== undefined ? { data: normalized.data } : {}) };
1288
1292
  }
1289
1293
  function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, required) {
1290
- const requestedAid = typeof payload?.agent === 'string' ? payload.agent.trim() : '';
1294
+ const requestedAid = typeof payload?.agent === 'string' && payload.agent.trim()
1295
+ ? payload.agent.trim()
1296
+ : typeof payload?.args?.aid === 'string'
1297
+ ? payload.args.aid.trim()
1298
+ : '';
1291
1299
  if (!requestedAid) {
1292
1300
  return required
1293
1301
  ? { error: '缺少顶层 agent 参数', code: 'MISSING_AID' }
@@ -1761,7 +1769,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1761
1769
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1762
1770
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1763
1771
  if (models.length > 0)
1764
- return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '') + (catalog.freshness === 'stale' ? ' [stale]' : ''), selected: modelMatches(agent, m, currentModel) }));
1772
+ return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : ''), selected: modelMatches(agent, m, currentModel) }));
1765
1773
  }
1766
1774
  return null;
1767
1775
  }
@@ -2366,6 +2374,24 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2366
2374
  }
2367
2375
  if (cmdBase === '/system') {
2368
2376
  const view = args?.view ?? 'status';
2377
+ if (view === 'version') {
2378
+ const data = {};
2379
+ try {
2380
+ const pkgPath = path.join(getPackageRoot(), 'package.json');
2381
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
2382
+ if (pkg?.version)
2383
+ data.version = pkg.version;
2384
+ }
2385
+ catch { }
2386
+ try {
2387
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
2388
+ const fp2 = JSON.parse(fs.readFileSync(fp, 'utf-8'));
2389
+ if (fp2?.version)
2390
+ data.fastaunVersion = fp2.version;
2391
+ }
2392
+ catch { }
2393
+ return { data };
2394
+ }
2369
2395
  if (view === 'monitor') {
2370
2396
  if (typeof this.daemonMonitorProvider !== 'function') {
2371
2397
  return { error: 'Daemon monitor snapshot is unavailable', code: 'DEPENDENCY_UNAVAILABLE' };
@@ -3061,7 +3087,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3061
3087
  return menuResultFailure(error);
3062
3088
  }
3063
3089
  }
3064
- if (cmdBase === '/agent' && !fromControlChannel && !args?.aid) {
3090
+ if ((cmdBase === '/agent' || cmdBase === '/aid') && !args?.aid) {
3065
3091
  const selfAid = this.getOwningAgent?.(channel)?.aid;
3066
3092
  if (selfAid)
3067
3093
  args = { ...(args ?? {}), aid: selfAid };
@@ -3096,6 +3122,30 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3096
3122
  return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
3097
3123
  }
3098
3124
  const isMenuAdmin = isManagementRole(authIdentity.role);
3125
+ // ── /aid avatar ──
3126
+ // The AID namespace is the only avatar mutation surface.
3127
+ if (cmdBase === '/aid') {
3128
+ if (action !== 'avatar')
3129
+ return { error: `不支持 aid action: ${action}`, code: 'NOT_SUPPORTED' };
3130
+ const result = await execAidAvatar(args);
3131
+ if (!('error' in result)) {
3132
+ const data = result.data;
3133
+ if (data?.avatar) {
3134
+ this.eventBus.publish({
3135
+ type: 'agent:avatar_updated',
3136
+ aid: data.aid,
3137
+ avatar: data.avatar,
3138
+ ...(data.sha256 !== undefined ? { sha256: data.sha256 } : {}),
3139
+ ...(data.mime !== undefined ? { mime: data.mime } : {}),
3140
+ ...(data.width !== undefined ? { width: data.width } : {}),
3141
+ ...(data.height !== undefined ? { height: data.height } : {}),
3142
+ ...(data.aspectRatio !== undefined ? { aspectRatio: data.aspectRatio } : {}),
3143
+ timestamp: Date.now(),
3144
+ });
3145
+ }
3146
+ }
3147
+ return result;
3148
+ }
3099
3149
  // ── /agent action ──
3100
3150
  // 控制 channel:验 evolcore.owners,可执行进程级(create/delete/enable/disable)+ 自管理(update/reload)。
3101
3151
  // agent channel:闸门已挡掉进程级 + 跨 agent,仅 update/reload 能到此;reload 允许 owner/admin,update 仅 owner。
@@ -3855,6 +3905,13 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3855
3905
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
3856
3906
  return { data: { action: 'restart', success: true } };
3857
3907
  }
3908
+ if (action === 'paircode') {
3909
+ // Keep Menu pairing on the same daemon-owned store as /pair, ECWeb, and
3910
+ // the local IPC endpoint. The store reuses an unexpired code and creates
3911
+ // a new one only after the shared TTL has elapsed.
3912
+ const pair = remoteAccessService.getPairingCode(subject.actorId);
3913
+ return { data: { code: pair.code, expiresAt: pair.expiresAt } };
3914
+ }
3858
3915
  if (action === 'check') {
3859
3916
  const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3860
3917
  // delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
@@ -4157,9 +4214,9 @@ async function execMenuForSystemControl(payload, context) {
4157
4214
  const isRoleRequest = name === 'role' || compatCmd === '/role';
4158
4215
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
4159
4216
  const requiresAgentTarget = isRoleRequest || isConnectRequest || [
4160
- 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group',
4217
+ 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group', 'aid',
4161
4218
  ].includes(name)
4162
- || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group'].includes(compatCmd ?? '')
4219
+ || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group', '/aid'].includes(compatCmd ?? '')
4163
4220
  || (!isProcessLevel && requestedContextKind !== undefined && requestedContextKind !== 'process');
4164
4221
  const target = isProcessLevel
4165
4222
  ? { channel: SYSTEM_CONTROL_CHANNEL }
@@ -4169,6 +4226,12 @@ async function execMenuForSystemControl(payload, context) {
4169
4226
  const targetChannel = target.channel;
4170
4227
  const targetAid = this.getOwningAgent?.(targetChannel)?.aid
4171
4228
  ?? this.getDaemonTriggerOwnerAid?.(targetChannel);
4229
+ if (name === 'aid' || compatCmd === '/aid') {
4230
+ const requestedAid = typeof payload?.args?.aid === 'string' ? payload.args.aid.trim().replace(/^@/, '') : '';
4231
+ if (requestedAid && targetAid && requestedAid !== targetAid) {
4232
+ return ecwebErr(id, name, 'INVALID_ARGUMENT', 'args.aid 必须与目标 Agent 一致');
4233
+ }
4234
+ }
4172
4235
  const trustedSubject = buildAuthSubject({
4173
4236
  selfAid: targetAid,
4174
4237
  actorId: context.actorAid,
@@ -4185,6 +4248,12 @@ async function execMenuForSystemControl(payload, context) {
4185
4248
  });
4186
4249
  const trustedIdentity = trustedSubject.identity;
4187
4250
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
4251
+ if (payload?.type === 'menu.action'
4252
+ && menuCommandToken(cmd) === '/system'
4253
+ && payload.action === 'paircode'
4254
+ && (context.authMethod !== 'daemon-owner' || (context.source === 'control' && !context.encrypted))) {
4255
+ return ecwebErr(id, name, 'NOT_ALLOWED', 'Pairing code issuance requires encrypted DaemonOwner authentication');
4256
+ }
4188
4257
  try {
4189
4258
  if (menuCommandToken(cmd) === '/baseagent-models') {
4190
4259
  if (payload.type !== 'menu.query')
@@ -4203,6 +4272,8 @@ async function execMenuForSystemControl(payload, context) {
4203
4272
  channelScope: 'control',
4204
4273
  channelId: targetChannel,
4205
4274
  request: payload.args.context,
4275
+ paircodeAllowed: context.authMethod === 'daemon-owner'
4276
+ && (context.source !== 'control' || context.encrypted),
4206
4277
  });
4207
4278
  return menuSuccess({ id, ...(name ? { name } : {}) }, data);
4208
4279
  }
@@ -4307,15 +4378,6 @@ export async function execMenuForEcweb(payload, trusted) {
4307
4378
  const validationError = validateMenuRequest(payload ?? {});
4308
4379
  if (validationError)
4309
4380
  return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
4310
- const isProcessLevel = isProcessLevelMenu(name, payload?.cmd) || isProcessContextMenu(payload);
4311
- const owners = loadDaemonConfig().owners ?? [];
4312
- if (isProcessLevel && owners.length === 0) {
4313
- return menuFailure({ id, ...(name ? { name } : {}) }, {
4314
- code: 'NOT_ALLOWED',
4315
- message: formatAuthorizationDenial('Configure daemon.json.owners before using process-level operations.', 'DAEMON_OWNER_REQUIRED'),
4316
- data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4317
- });
4318
- }
4319
4381
  if (!trusted || (!trusted.localDirect && trusted.authenticated !== true)) {
4320
4382
  return menuFailure({ id, ...(name ? { name } : {}) }, {
4321
4383
  code: 'PERMISSION_DENIED',
@@ -4323,29 +4385,22 @@ export async function execMenuForEcweb(payload, trusted) {
4323
4385
  data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
4324
4386
  });
4325
4387
  }
4326
- // A verified ECWeb connection has daemon-owner management authority. If the
4327
- // bearer token has no explicit owner provenance (the normal pairing path),
4328
- // use the first currently configured owner as the canonical management
4329
- // principal. This keeps audit/Trigger approval attribution deterministic
4330
- // without requiring owner-bound pairing metadata.
4388
+ // ECWeb authentication grants daemon-owner authority independently of the
4389
+ // configured owners. Prefer an owner for audit/Trigger attribution when
4390
+ // available; otherwise keep local and remote management actors distinct.
4391
+ const owners = loadDaemonConfig().owners ?? [];
4331
4392
  const userId = (trusted.actorAid && owners.includes(trusted.actorAid))
4332
4393
  ? trusted.actorAid
4333
- : owners[0] ?? (trusted.localDirect ? 'local-direct' : undefined);
4334
- if (!userId) {
4335
- return menuFailure({ id, ...(name ? { name } : {}) }, {
4336
- code: 'PERMISSION_DENIED',
4337
- message: 'ECWeb operation requires configured daemon owners',
4338
- data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
4339
- });
4340
- }
4394
+ : owners[0] ?? (trusted.localDirect ? 'local-direct' : 'ecweb-remote');
4341
4395
  return execMenuForSystemControl.call(this, payload, {
4342
4396
  source: 'ecweb',
4343
4397
  actorAid: userId,
4398
+ authMethod: trusted.localDirect ? 'daemon-owner' : 'remote-access-token',
4399
+ encrypted: trusted.localDirect,
4344
4400
  localDirect: trusted.localDirect,
4345
4401
  // The ECWeb server/IPC control-token gate authenticates this management
4346
4402
  // request. All authenticated ECWeb connections are equally eligible to
4347
- // approve persistent fullaccess; actorAid is used only when it identifies
4348
- // a current configured owner.
4403
+ // approve persistent fullaccess without owner-bound token metadata.
4349
4404
  canApprovePersistentFullAccess: true,
4350
4405
  owners,
4351
4406
  });
@@ -4379,6 +4434,8 @@ export async function execMenuForControl(payload, peerId, auth = {}) {
4379
4434
  return execMenuForSystemControl.call(this, payload, {
4380
4435
  source: 'control',
4381
4436
  actorAid: managementActor,
4437
+ authMethod: authorization.allowed ? authorization.authMethod : 'daemon-owner',
4438
+ encrypted: auth.encrypted === true,
4382
4439
  localDirect: false,
4383
4440
  canApprovePersistentFullAccess: false,
4384
4441
  owners,
@@ -120,6 +120,7 @@ export const MENU_NAME_COMMANDS = Object.freeze({
120
120
  system: '/system',
121
121
  cli: '/cli',
122
122
  agent: '/agent',
123
+ aid: '/aid',
123
124
  trigger: '/trigger',
124
125
  file: '/file',
125
126
  gateway: '/gateway',
@@ -566,6 +567,11 @@ function sanitizeLogArgv(value, depth, seen) {
566
567
  export function sanitizeMenuLogValue(value, key = '', depth = 0, seen = new WeakSet()) {
567
568
  if (isSensitiveLogKey(key))
568
569
  return '[REDACTED]';
570
+ // Pairing codes are intentionally returned as `code` on the wire for
571
+ // compatibility with /pair and the ECWeb API. Do not persist six-digit
572
+ // credentials in menu-protocol.log.
573
+ if (key === 'code' && typeof value === 'string' && /^\d{6}$/.test(value))
574
+ return '[REDACTED]';
569
575
  if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean')
570
576
  return value;
571
577
  if (typeof value === 'bigint')
@@ -4,6 +4,20 @@ import { agentDataDir } from '../../paths.js';
4
4
  import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
5
5
  export const DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS = 30 * 24 * 60 * 60;
6
6
  export const MAX_MENU_TOKENS_PER_AGENT = 1_000;
7
+ function validInstallationId(value) {
8
+ return value === undefined || (typeof value === 'string' && value.length <= 256
9
+ && !/[\u0000-\u001f\u007f]/.test(value));
10
+ }
11
+ function normalizeInstallation(value) {
12
+ if (!value || typeof value !== 'object' || !validInstallationId(value.deviceId)
13
+ || !validInstallationId(value.slotId))
14
+ return null;
15
+ const deviceId = value.deviceId?.trim() || '';
16
+ const slotId = value.slotId?.trim() || '';
17
+ if (slotId && !deviceId)
18
+ return null;
19
+ return { deviceId, slotId };
20
+ }
7
21
  function hashToken(token) {
8
22
  return createHash('sha256').update(token).digest('hex');
9
23
  }
@@ -13,6 +27,9 @@ function isTokenRecord(value) {
13
27
  const record = value;
14
28
  return typeof record.peerAid === 'string'
15
29
  && record.peerAid.trim().length > 0
30
+ && validInstallationId(record.peerDeviceId)
31
+ && validInstallationId(record.peerSlotId)
32
+ && (!record.peerSlotId?.trim() || !!record.peerDeviceId?.trim())
16
33
  && typeof record.tokenHash === 'string'
17
34
  && /^[a-f0-9]{64}$/.test(record.tokenHash)
18
35
  && typeof record.requestedAt === 'number'
@@ -28,6 +45,10 @@ function isTokenRecord(value) {
28
45
  function isExpired(record, now) {
29
46
  return now - record.lastUsedAt >= record.idleTimeout * 1000;
30
47
  }
48
+ function installationMatches(record, installation) {
49
+ return (record.peerDeviceId?.trim() || '') === (installation.deviceId?.trim() || '')
50
+ && (record.peerSlotId?.trim() || '') === (installation.slotId?.trim() || '');
51
+ }
31
52
  function hashesEqual(left, right) {
32
53
  const leftBytes = Buffer.from(left, 'hex');
33
54
  const rightBytes = Buffer.from(right, 'hex');
@@ -41,10 +62,14 @@ export class MenuTokenStore {
41
62
  constructor(maxRecords = MAX_MENU_TOKENS_PER_AGENT) {
42
63
  this.maxRecords = maxRecords;
43
64
  }
44
- issue(selfAid, peerAid, metadata = {}, now = Date.now()) {
65
+ issue(selfAid, peerAid, metadata = {}, installationOrNow = {}, now = Date.now()) {
66
+ const installation = normalizeInstallation(typeof installationOrNow === 'number' ? {} : installationOrNow);
67
+ if (!installation)
68
+ return null;
69
+ const issuedAt = typeof installationOrNow === 'number' ? installationOrNow : now;
45
70
  const file = this.read(selfAid);
46
71
  const idleTimeout = DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS;
47
- const tokens = file.tokens.filter(record => record.peerAid !== peerAid && !isExpired(record, now));
72
+ const tokens = file.tokens.filter(record => (record.peerAid !== peerAid || !installationMatches(record, installation)) && !isExpired(record, issuedAt));
48
73
  if (tokens.length >= this.maxRecords) {
49
74
  if (tokens.length !== file.tokens.length)
50
75
  this.write(selfAid, tokens);
@@ -53,9 +78,11 @@ export class MenuTokenStore {
53
78
  const menuToken = `mt_${randomBytes(32).toString('base64url')}`;
54
79
  tokens.push({
55
80
  peerAid,
81
+ ...(installation.deviceId?.trim() ? { peerDeviceId: installation.deviceId.trim() } : {}),
82
+ ...(installation.slotId?.trim() ? { peerSlotId: installation.slotId.trim() } : {}),
56
83
  tokenHash: hashToken(menuToken),
57
- requestedAt: now,
58
- lastUsedAt: now,
84
+ requestedAt: issuedAt,
85
+ lastUsedAt: issuedAt,
59
86
  idleTimeout,
60
87
  ...(metadata.clientName ? { clientName: metadata.clientName } : {}),
61
88
  ...(metadata.clientVersion ? { clientVersion: metadata.clientVersion } : {}),
@@ -64,21 +91,27 @@ export class MenuTokenStore {
64
91
  this.write(selfAid, tokens);
65
92
  return { menuToken, idleTimeout };
66
93
  }
67
- use(selfAid, peerAid, menuToken, now = Date.now()) {
94
+ use(selfAid, peerAid, menuToken, installationOrNow = {}, now = Date.now()) {
95
+ const installation = normalizeInstallation(typeof installationOrNow === 'number' ? {} : installationOrNow);
96
+ if (!installation)
97
+ return false;
98
+ const usedAt = typeof installationOrNow === 'number' ? installationOrNow : now;
68
99
  const file = this.read(selfAid);
69
- const activeTokens = file.tokens.filter(record => !isExpired(record, now));
100
+ const activeTokens = file.tokens.filter(record => !isExpired(record, usedAt));
70
101
  const tokenHash = typeof menuToken === 'string' && menuToken.length > 0 && menuToken.length <= 256
71
102
  ? hashToken(menuToken)
72
103
  : undefined;
73
104
  const matchingIndex = tokenHash
74
- ? activeTokens.findIndex(record => record.peerAid === peerAid && hashesEqual(record.tokenHash, tokenHash))
105
+ ? activeTokens.findIndex(record => record.peerAid === peerAid
106
+ && installationMatches(record, installation)
107
+ && hashesEqual(record.tokenHash, tokenHash))
75
108
  : -1;
76
109
  if (matchingIndex < 0) {
77
110
  if (activeTokens.length !== file.tokens.length)
78
111
  this.write(selfAid, activeTokens);
79
112
  return false;
80
113
  }
81
- activeTokens[matchingIndex] = { ...activeTokens[matchingIndex], lastUsedAt: now };
114
+ activeTokens[matchingIndex] = { ...activeTokens[matchingIndex], lastUsedAt: usedAt };
82
115
  this.write(selfAid, activeTokens);
83
116
  return true;
84
117
  }
@@ -45,7 +45,8 @@ export function probeSessionActivity(evidence, probes) {
45
45
  // 支持的命令列表
46
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', '/pair']);
48
+ commands.push('/diag');
49
+ const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop', '/pair', '/diag']);
49
50
  /** Parse the execution command without treating it as a quick/result command. */
50
51
  export function parseFullAccessCommand(content) {
51
52
  const match = content.match(/^\s*(\/fa|\/fullaccess)(?=\s|$)([\s\S]*)$/);
@@ -64,8 +65,37 @@ const aliases = {
64
65
  '/rw': '/rewind',
65
66
  '/base': '/baseagent',
66
67
  };
68
+ const GROUP_READ_ONLY_COMMANDS = new Set(['/mentionmode', '/model', '/effort', '/perm', '/baseagent']);
69
+ const GROUP_TASK_COMMANDS = new Set([
70
+ '/help', '/evolhelp', '/status', '/check', '/ask', '/pause', '/resume', '/stop', '/compact', '/renew',
71
+ ]);
72
+ /** Group slash policy applies to every role, including old command-card clicks. */
73
+ export function guardGroupSlashCommand(content, chatType) {
74
+ if (chatType !== 'group' || !content.trim().startsWith('/'))
75
+ return undefined;
76
+ const [inputCommand, ...args] = content.trim().split(/\s+/);
77
+ const command = aliases[inputCommand] ?? inputCommand;
78
+ if (GROUP_READ_ONLY_COMMANDS.has(command)) {
79
+ if (!args.length || (command === '/model' && args.length === 1 && ['current', 'list', 'check'].includes(args[0]))) {
80
+ return undefined;
81
+ }
82
+ return {
83
+ kind: 'command.error',
84
+ text: `❌ 群聊中 ${command} 仅支持查询,请在私聊或管理界面操作`,
85
+ reason: 'GROUP_COMMAND_READ_ONLY',
86
+ };
87
+ }
88
+ if (GROUP_TASK_COMMANDS.has(command))
89
+ return undefined;
90
+ return {
91
+ kind: 'command.error',
92
+ text: `❌ 群聊中不支持 ${inputCommand},输入 /help 查看可用命令`,
93
+ reason: 'GROUP_COMMAND_UNSUPPORTED',
94
+ };
95
+ }
67
96
  // 命令快速路径前缀(所有命令都不进入消息队列)
68
97
  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'];
98
+ quickCommandPrefixes.push('/diag');
69
99
  /**
70
100
  * 计算两个字符串的 Levenshtein 距离(编辑距离)
71
101
  */
@@ -95,7 +125,7 @@ function levenshteinDistance(str1, str2) {
95
125
  return matrix[len1][len2];
96
126
  }
97
127
  export function isQuickCommand(content) {
98
- return content === '/s' || quickCommandPrefixes.some(cmd => exactBoundaryCommands.has(cmd)
128
+ return content === '/s' || content === '/base' || quickCommandPrefixes.some(cmd => exactBoundaryCommands.has(cmd)
99
129
  ? content === cmd || content.startsWith(cmd + ' ')
100
130
  : content.startsWith(cmd));
101
131
  }
@@ -131,7 +161,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
131
161
  const userGroupCommands = [
132
162
  '/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
133
163
  '/model', '/effort', '/baseagent', '/perm', '/activity', '/stop', '/pause',
134
- '/resume', '/trigger', '/file',
164
+ '/resume', '/trigger', '/file', '/ask',
135
165
  ];
136
166
  const userCommands = activeChatType === 'group' && !isAdmin
137
167
  ? userGroupCommands
@@ -146,7 +176,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
146
176
  return {
147
177
  kind: 'command.error',
148
178
  text: activeChatType === 'group'
149
- ? '❌ 无权限:当前群聊仅支持 /status 和 /help'
179
+ ? '❌ 无权限:此命令仅限管理员使用,输入 /help 查看可用命令'
150
180
  : '❌ 无权限:此命令仅限管理员使用',
151
181
  };
152
182
  }