evolcore 0.0.9 → 0.0.11

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 (64) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/contact-book-store.js +1 -1
  20. package/dist/config/gateway-config.js +26 -10
  21. package/dist/core/agent-reload-coordinator.js +53 -0
  22. package/dist/core/auth/operation-authorizer.js +32 -147
  23. package/dist/core/auth/operation-catalog.js +80 -0
  24. package/dist/core/bootstrap-messages.js +50 -0
  25. package/dist/core/bootstrap-service.js +85 -10
  26. package/dist/core/channel-loader.js +23 -6
  27. package/dist/core/command/agent-control.js +14 -11
  28. package/dist/core/command/menu-handler.js +67 -76
  29. package/dist/core/command/slash-handler.js +4 -4
  30. package/dist/core/data-migration.js +79 -27
  31. package/dist/core/evolagent-registry.js +125 -35
  32. package/dist/core/evolagent.js +8 -3
  33. package/dist/core/inference/text-inference.js +38 -4
  34. package/dist/core/message/message-bridge.js +1 -1
  35. package/dist/core/message/message-log.js +22 -0
  36. package/dist/core/message/message-queue.js +19 -4
  37. package/dist/core/model/model-catalog.js +143 -24
  38. package/dist/core/model/model-diagnostics.js +28 -10
  39. package/dist/core/permission/index.js +1 -0
  40. package/dist/core/permission/readonly-shell-query.js +532 -0
  41. package/dist/core/permission/shell-environment.js +46 -0
  42. package/dist/core/permission/tool-policy.js +231 -93
  43. package/dist/core/protected-paths.js +10 -7
  44. package/dist/core/runner-reload-transaction.js +57 -0
  45. package/dist/index.js +262 -84
  46. package/dist/ipc.js +29 -11
  47. package/dist/utils/aid-bind.js +3 -8
  48. package/dist/utils/log-writer.js +6 -10
  49. package/dist/utils/logger.js +5 -5
  50. package/kits/docs/evolcore/msg.md +13 -0
  51. package/kits/rules/01-overview.md +9 -0
  52. package/kits/schemas/agent-config.schema.3.json +1 -1
  53. package/kits/schemas/agent-config.schema.4.json +1 -1
  54. package/kits/schemas/relation-config.schema.2.json +1 -1
  55. package/kits/schemas/role-config.schema.1.json +1 -1
  56. package/kits/templates/roles/admin.json +5 -0
  57. package/kits/templates/roles/member.json +17 -0
  58. package/kits/templates/roles/visitor.json +8 -0
  59. package/kits/templates/system-fragments/bootstrap.md +12 -6
  60. package/kits/templates/system-fragments/channel.md +6 -0
  61. package/kits/templates/system-fragments/session.md +2 -0
  62. package/package.json +2 -1
  63. package/skills/eclink/SKILL.md +15 -3
  64. package/skills/eclink/agents/openai.yaml +3 -3
@@ -9,6 +9,36 @@ import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecy
9
9
  import { renderTemplate } from '../eck/manifest-engine.js';
10
10
  import { activeBaseagent } from './model/config-scope.js';
11
11
  import { buildEnvelope } from './message/message-utils.js';
12
+ import { BOOTSTRAP_MESSAGE_TTL_MS, bootstrapInitialMessageOperationId } from './bootstrap-messages.js';
13
+ /** Coordinate the write-ahead completion welcome with the lifecycle commit. */
14
+ export async function completeBootstrapWithWelcome(service, aid, welcome) {
15
+ const lifecycle = service.lifecycleOf(aid);
16
+ if (lifecycle === 'bootstrapping') {
17
+ if (!welcome) {
18
+ return {
19
+ ok: false,
20
+ code: 'WELCOME_CHANNEL_UNAVAILABLE',
21
+ error: `No AUN channel available to persist the post-bootstrap welcome for ${aid}`,
22
+ reloaded: false,
23
+ };
24
+ }
25
+ if (!await welcome.preparePostBootstrapWelcome()) {
26
+ return {
27
+ ok: false,
28
+ code: 'WELCOME_PREPARE_FAILED',
29
+ error: `Failed to persist the post-bootstrap welcome for ${aid}`,
30
+ reloaded: false,
31
+ };
32
+ }
33
+ }
34
+ const result = service.completeBootstrap(aid);
35
+ if (!result.ok)
36
+ return { ...result, reloaded: false };
37
+ if (!welcome)
38
+ return { ...result, reloaded: true, welcomeQueued: false };
39
+ const welcomeQueued = await welcome.reconcilePostBootstrapWelcome();
40
+ return { ...result, reloaded: true, welcomeQueued };
41
+ }
12
42
  export class BootstrapService {
13
43
  agentRegistry;
14
44
  eventBus;
@@ -29,23 +59,25 @@ export class BootstrapService {
29
59
  return false;
30
60
  this.inFlight.add(key);
31
61
  let lifecycleStarted = false;
32
- const loadedConfig = agent?.config || loadAgent(aid);
62
+ const loadedConfig = loadAgent(aid) || agent?.config;
33
63
  if (!loadedConfig) {
34
64
  this.inFlight.delete(key);
35
65
  return false;
36
66
  }
37
67
  const config = normalizeAgentLifecycle(loadedConfig);
38
- if (config.lifecycle !== 'created') {
68
+ if (config.lifecycle === 'active') {
39
69
  this.inFlight.delete(key);
40
70
  return false;
41
71
  }
72
+ const starting = config.lifecycle === 'created';
42
73
  const channelType = ctx.channelType || this.channelTypeFromKey(ctx.channelKey);
43
- const recipientId = ctx.recipientId || this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
74
+ const configuredRecipient = this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
75
+ const recipientId = ctx.recipientId || configuredRecipient;
44
76
  if (!recipientId) {
45
77
  this.inFlight.delete(key);
46
78
  return false;
47
79
  }
48
- if (ctx.recipientId && recipientId !== ctx.recipientId) {
80
+ if (ctx.recipientId && configuredRecipient !== ctx.recipientId) {
49
81
  this.inFlight.delete(key);
50
82
  return false;
51
83
  }
@@ -57,9 +89,11 @@ export class BootstrapService {
57
89
  try {
58
90
  const agentName = this.resolveAgentDisplayName(aid);
59
91
  const baseagent = this.resolveBaseagent(agent, aid);
60
- await this.publishAgentMdIfSupported(ctx.adapter, aid, agentName);
61
- this.setLifecycle(agent, aid, 'bootstrapping');
62
- lifecycleStarted = true;
92
+ if (starting) {
93
+ await this.publishAgentMdIfSupported(ctx.adapter, aid, agentName);
94
+ this.setLifecycle(agent, aid, 'bootstrapping');
95
+ lifecycleStarted = true;
96
+ }
63
97
  const text = this.renderWelcome({
64
98
  agentAid: aid,
65
99
  agentName,
@@ -72,9 +106,23 @@ export class BootstrapService {
72
106
  channel: ctx.adapter.channelKey || ctx.adapter.channelName,
73
107
  channelId,
74
108
  agentName: aid,
109
+ replyContext: {
110
+ metadata: {
111
+ source: 'daemon',
112
+ persistRequired: true,
113
+ operationId: bootstrapInitialMessageOperationId(aid),
114
+ outboxTtl: BOOTSTRAP_MESSAGE_TTL_MS,
115
+ criticalDelivery: true,
116
+ },
117
+ },
75
118
  }), { kind: 'result.text', text, isFinal: true });
76
- this.eventBus.publish({ type: 'agent:bootstrap-started', aid, channel: channelType || ctx.channelKey, timestamp: Date.now() });
77
- logger.info(`[Bootstrap] Started for ${aid} via ${ctx.channelKey} (${ctx.source})`);
119
+ if (starting) {
120
+ this.eventBus.publish({ type: 'agent:bootstrap-started', aid, channel: channelType || ctx.channelKey, timestamp: Date.now() });
121
+ logger.info(`[Bootstrap] Started for ${aid} via ${ctx.channelKey} (${ctx.source})`);
122
+ }
123
+ else {
124
+ logger.info(`[Bootstrap] Reconciled initial message for ${aid} via ${ctx.channelKey} (${ctx.source})`);
125
+ }
78
126
  return true;
79
127
  }
80
128
  catch (e) {
@@ -93,6 +141,33 @@ export class BootstrapService {
93
141
  this.inFlight.delete(key);
94
142
  }
95
143
  }
144
+ completeBootstrap(aid) {
145
+ const agent = this.agentRegistry.get(aid);
146
+ const loadedConfig = loadAgent(aid) || agent?.config;
147
+ if (!loadedConfig)
148
+ return { ok: false, error: `Agent "${aid}" not found` };
149
+ const lifecycle = normalizeAgentLifecycle(loadedConfig).lifecycle;
150
+ if (lifecycle === 'active')
151
+ return { ok: true, aid, transitioned: false };
152
+ if (lifecycle !== 'bootstrapping') {
153
+ return {
154
+ ok: false,
155
+ code: 'INVALID_LIFECYCLE',
156
+ error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready`,
157
+ };
158
+ }
159
+ this.setLifecycle(agent, aid, 'active');
160
+ this.eventBus.publish({ type: 'agent:bootstrap-complete', aid, timestamp: Date.now() });
161
+ logger.info(`[Bootstrap] Completed for ${aid} (previous lifecycle=${lifecycle})`);
162
+ return { ok: true, aid, transitioned: true };
163
+ }
164
+ lifecycleOf(aid) {
165
+ const agent = this.agentRegistry.get(aid);
166
+ const loadedConfig = loadAgent(aid) || agent?.config;
167
+ return loadedConfig
168
+ ? normalizeAgentLifecycle(loadedConfig).lifecycle
169
+ : null;
170
+ }
96
171
  setLifecycle(agent, aid, lifecycle) {
97
172
  if (agent?.setLifecycle) {
98
173
  agent.setLifecycle(lifecycle);
@@ -100,7 +175,7 @@ export class BootstrapService {
100
175
  }
101
176
  const cfg = loadAgent(aid);
102
177
  if (!cfg)
103
- return;
178
+ throw new Error(`Agent "${aid}" disappeared while updating lifecycle`);
104
179
  saveAgent(withLifecycleForWrite(cfg, lifecycle));
105
180
  }
106
181
  resolveConfiguredRecipient(config, _channelKey, _channelType) {
@@ -177,10 +177,14 @@ export function isValidChannelName(name) {
177
177
  return typeof name === 'string' && name.length > 0 && !name.includes(SEP);
178
178
  }
179
179
  export function buildReloadHooks(deps) {
180
- const { channelLoader, channelInstances, registerChannelInstance, unregisterChannelInstance, messageQueue, handoffRuntime, onChannelStarted, onChannelConnected } = deps;
180
+ const { channelLoader, channelInstances, registerChannelInstance, unregisterChannelInstance, messageQueue, handoffRuntime, onChannelStarted, onChannelConnected, onAgentReloaded, stageAgentRunners, } = deps;
181
181
  const drainDelayMs = deps.drainDelayMs ?? 500;
182
182
  const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
183
183
  return {
184
+ stageAgentRunners,
185
+ async afterReload(agent) {
186
+ await onAgentReloaded?.(agent);
187
+ },
184
188
  async prepareHandoffReload(aid) {
185
189
  if (!handoffRuntime)
186
190
  return;
@@ -284,11 +288,24 @@ export function buildReloadHooks(deps) {
284
288
  return;
285
289
  }
286
290
  registerChannelInstance(newInst);
287
- onChannelStarted?.(newInst);
288
- await newInst.connect();
289
- channelInstances.push(newInst);
290
- await onChannelConnected?.(newInst);
291
- logger.info(`[Reload] Started channel: ${channelName}`);
291
+ try {
292
+ onChannelStarted?.(newInst);
293
+ await newInst.connect();
294
+ channelInstances.push(newInst);
295
+ await onChannelConnected?.(newInst);
296
+ logger.info(`[Reload] Started channel: ${channelName}`);
297
+ }
298
+ catch (error) {
299
+ try {
300
+ await newInst.disconnect();
301
+ }
302
+ catch { }
303
+ unregisterChannelInstance?.(channelName);
304
+ const index = channelInstances.indexOf(newInst);
305
+ if (index >= 0)
306
+ channelInstances.splice(index, 1);
307
+ throw error;
308
+ }
292
309
  },
293
310
  };
294
311
  }
@@ -4,7 +4,7 @@ import { resolvePaths, resolveRoot } from '../../paths.js';
4
4
  import path from 'path';
5
5
  import { ConfigTarget, ensureFile as cfgEnsure, read as cfgRead, write as cfgWrite, } from '../../config/config-manager.js';
6
6
  import { CreateStatusWriter, readCreateStatus } from '../message/create-status.js';
7
- import { deriveAgentProjectPath } from '../../utils/project-path.js';
7
+ import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../../utils/project-path.js';
8
8
  import { uploadAvatar } from '../../utils/avatar-upload.js';
9
9
  import { agentmdGet, agentmdPut, updateAgentMdFrontmatterName } from '../../aun/aid/agentmd.js';
10
10
  import { isValidAid } from '../../aun/aid/validation.js';
@@ -367,7 +367,7 @@ export async function execAgentAction(action, args, peerId, eventBus) {
367
367
  if (!isValidAid(owner))
368
368
  return { error: `无效 owner AID: ${owner}`, code: 'INVALID_ARGS' };
369
369
  if (!a.project || typeof a.project !== 'string') {
370
- return { error: 'project 缺失且无法兜底(需 defaults.projects.rootPath/defaultPath)', code: 'INVALID_ARGS' };
370
+ return { error: 'project 缺失(调用方应先解析默认项目路径)', code: 'INVALID_ARGS' };
371
371
  }
372
372
  const model = typeof a.model === 'string' && a.model.trim() ? a.model.trim() : undefined;
373
373
  const rawEffort = typeof a.effort === 'string' && a.effort.trim() ? a.effort.trim() : undefined;
@@ -401,9 +401,10 @@ export async function execAgentAction(action, args, peerId, eventBus) {
401
401
  if (action === 'enable' || action === 'disable') {
402
402
  if (!a.aid)
403
403
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
404
- const res = action === 'enable' ? await agentEnable(a.aid) : await agentDisable(a.aid);
404
+ const options = { force: a.force === true };
405
+ const res = action === 'enable' ? await agentEnable(a.aid, options) : await agentDisable(a.aid, options);
405
406
  if (!('ok' in res) || res.ok !== true)
406
- return { error: res.error, code: classifyError(res.error) };
407
+ return { error: res.error, code: res.code || classifyError(res.error) };
407
408
  eventBus?.publish({
408
409
  type: action === 'enable' ? 'agent:enabled' : 'agent:disabled',
409
410
  aid: res.aid,
@@ -415,9 +416,9 @@ export async function execAgentAction(action, args, peerId, eventBus) {
415
416
  if (action === 'reload') {
416
417
  if (!a.aid)
417
418
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
418
- const res = await agentReload(a.aid);
419
+ const res = await agentReload(a.aid, { force: a.force === true });
419
420
  if (!('ok' in res) || res.ok !== true)
420
- return { error: res.error, code: classifyError(res.error) };
421
+ return { error: res.error, code: res.code || classifyError(res.error) };
421
422
  eventBus?.publish({ type: 'agent:reloaded', aid: a.aid, timestamp: Date.now() });
422
423
  return { data: { aid: a.aid, reloaded: true } };
423
424
  }
@@ -564,14 +565,16 @@ export async function execAgentUpdate(args) {
564
565
  }
565
566
  return { data: { ...data, saved: true, ...(hasConfigPatch ? { requiresReload: true } : {}) } };
566
567
  }
567
- /** project 兜底:显式值 > rootPath 合成 > defaultPath > undefined */
568
+ /**
569
+ * project 解析与 ec agent create 保持一致:
570
+ * 显式值 > 默认项目根目录 + AID 前缀。
571
+ * 默认项目根目录由 defaults.projects.rootPath/defaultPath 或运行时根目录推导。
572
+ */
568
573
  export function resolveProjectPath(explicit, aid, defaults) {
569
574
  if (explicit && explicit.trim())
570
575
  return explicit;
571
- const root = defaults?.projects?.rootPath;
572
- if (root)
573
- return deriveAgentProjectPath(root, aid);
574
- return defaults?.projects?.defaultPath;
576
+ const root = agentProjectRootFromDefaults(defaults, resolveRoot());
577
+ return deriveAgentProjectPath(root, aid);
575
578
  }
576
579
  /** name=agent 的 menu.query:查单个 agent 详情,附构建进度(D3)。 */
577
580
  export async function execAgentQuery(args) {
@@ -873,20 +873,36 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
873
873
  });
874
874
  const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
875
875
  if (verb === 'query') {
876
+ if (cmdBase === '/pwd')
877
+ return intent('project.current', 'relation');
878
+ if (cmdBase === '/session' || cmdBase === '/s')
879
+ return intent('session.current', 'relation');
880
+ if (cmdBase === '/topic')
881
+ return intent('session.topic.current', 'relation');
882
+ if (cmdBase === '/baseagent')
883
+ return intent('agent.baseagent.current', fromControlChannel ? 'agent' : 'relation');
876
884
  if (cmdBase === '/file')
877
885
  return intent('file.fetch', 'filesystem', { filePath: args?.path });
878
886
  if (cmdBase === '/model')
879
887
  return intent('model.current', modelScope);
880
888
  if (cmdBase === '/effort')
881
- return intent('model.current', modelScope);
889
+ return intent('model.effort.current', modelScope);
882
890
  if (cmdBase === '/chatmode')
883
891
  return intent('chatmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
884
892
  if (cmdBase === '/mentionmode')
885
893
  return intent('mentionmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
894
+ if (cmdBase === '/perm')
895
+ return intent('permission.current', args?.scope === 'agent' ? 'agent' : 'relation');
896
+ if (cmdBase === '/activity')
897
+ return intent('activity.current', args?.scope === 'agent' ? 'agent' : 'relation');
898
+ if (cmdBase === '/observable')
899
+ return intent('observable.current', 'agent');
900
+ if (cmdBase === '/capability')
901
+ return intent('capability.read', 'agent');
886
902
  if (cmdBase === '/gateway')
887
903
  return intent('gateway.read', 'process');
888
904
  if (cmdBase === '/config')
889
- return intent('config.read', 'process');
905
+ return intent('config.read', args?.scope === 'agent' ? 'agent' : 'process');
890
906
  if (cmdBase === '/system')
891
907
  return intent('system.status', 'process');
892
908
  if (cmdBase === '/agent') {
@@ -895,6 +911,32 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
895
911
  if (cmdBase === '/trigger')
896
912
  return intent('trigger.list', 'relation');
897
913
  }
914
+ if (verb === 'options') {
915
+ if (cmdBase === '/agent')
916
+ return intent(fromControlChannel ? 'agent.list' : 'agent.show', fromControlChannel ? 'control' : 'agent');
917
+ if (cmdBase === '/capability')
918
+ return intent('capability.read', 'agent');
919
+ if (cmdBase === '/trigger')
920
+ return intent('trigger.list', 'relation');
921
+ if (cmdBase === '/topic')
922
+ return intent('session.topic.list', 'relation');
923
+ if (cmdBase === '/session' || cmdBase === '/s' || cmdBase === '/del')
924
+ return intent('session.list', 'relation');
925
+ if (cmdBase === '/baseagent')
926
+ return intent('agent.baseagent.list', 'agent');
927
+ if (cmdBase === '/model')
928
+ return intent('model.list', modelScope);
929
+ if (cmdBase === '/effort')
930
+ return intent('model.effort.current', modelScope);
931
+ if (cmdBase === '/chatmode')
932
+ return intent('chatmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
933
+ if (cmdBase === '/mentionmode')
934
+ return intent('mentionmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
935
+ if (cmdBase === '/activity')
936
+ return intent('activity.current', args?.scope === 'agent' ? 'agent' : 'relation');
937
+ if (cmdBase === '/perm')
938
+ return intent('permission.current', args?.scope === 'agent' ? 'agent' : 'relation');
939
+ }
898
940
  if (verb === 'update') {
899
941
  if (cmdBase === '/model')
900
942
  return intent('model.use', modelScope, { model: value });
@@ -1010,6 +1052,9 @@ async function authorizeMenuIntent(params) {
1010
1052
  peerKey: subject.peerKey,
1011
1053
  });
1012
1054
  }
1055
+ else if (intent.scope === 'agent' && subject.selfAid && intent.args.self === undefined) {
1056
+ intent.args = { ...intent.args, self: subject.selfAid };
1057
+ }
1013
1058
  const source = params.source ?? 'menu';
1014
1059
  intent.source = source;
1015
1060
  const decision = await authorizeOperation({ source, intent, subject });
@@ -1428,13 +1473,25 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1428
1473
  throw { code: result.code, message: result.error, data: result.data };
1429
1474
  return result.data;
1430
1475
  }
1476
+ const optionsIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1477
+ const optionsAuthDenied = await authorizeMenuIntent.call(this, {
1478
+ intent: buildMenuIntent('options', cmdBase0, args, undefined, undefined, fromControlChannel),
1479
+ identity: optionsIdentity,
1480
+ subject,
1481
+ session,
1482
+ explicitChatType,
1483
+ channel,
1484
+ channelId,
1485
+ userId,
1486
+ fromControlChannel,
1487
+ source,
1488
+ });
1489
+ if (optionsAuthDenied)
1490
+ throw { code: optionsAuthDenied.code, message: optionsAuthDenied.error, data: optionsAuthDenied.data };
1431
1491
  // ── /agent list(只读) ──
1432
- // 控制 channel:验 evolcore.owners,返回全量;agent channel:放行但仅返回自身单条。
1492
+ // 控制 channel:返回全量;agent channel:仅返回自身单条。
1433
1493
  if (cmd === '/agent') {
1434
1494
  if (fromControlChannel) {
1435
- if (!subject.isDaemonOwner) {
1436
- throw { code: 'FORBIDDEN', message: '操作需要 owner 权限' };
1437
- }
1438
1495
  const res = await execAgentOptions(args);
1439
1496
  if ('error' in res)
1440
1497
  throw { code: res.code, message: res.error };
@@ -1465,24 +1522,9 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1465
1522
  }
1466
1523
  // ── 关系级 /trigger list(每个 trigger 一个 MenuItem) ──
1467
1524
  if (cmd === '/trigger') {
1468
- const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1469
- const authDenied = await authorizeMenuIntent.call(this, {
1470
- intent: buildMenuIntent('query', cmd, args, undefined, undefined, fromControlChannel),
1471
- identity,
1472
- subject,
1473
- session,
1474
- explicitChatType,
1475
- channel,
1476
- channelId,
1477
- userId,
1478
- fromControlChannel,
1479
- source,
1480
- });
1481
- if (authDenied)
1482
- throw { code: authDenied.code, message: authDenied.error, data: authDenied.data };
1483
1525
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1484
1526
  const scope = args?.options === 'all' ? 'all' : 'enabled';
1485
- const role = identity.role;
1527
+ const role = optionsIdentity.role;
1486
1528
  const isAdmin = isManagementRole(role);
1487
1529
  if (!triggerScheduler)
1488
1530
  return [];
@@ -1501,10 +1543,6 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1501
1543
  });
1502
1544
  }
1503
1545
  if (cmd === '/topic') {
1504
- const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1505
- if (!this.canReadTopics(identity.role)) {
1506
- throw { code: 'FORBIDDEN', message: '无权限查看话题' };
1507
- }
1508
1546
  if (args?.mode === 'fork-turns') {
1509
1547
  const source = await resolveTopicForkSource(this.sessionManager, channel, channelId, args?.sourceThreadId);
1510
1548
  if (!source)
@@ -1577,14 +1615,13 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1577
1615
  return available.map((name) => ({ value: name, label: name, selected: name === currentAgent }));
1578
1616
  }
1579
1617
  if (cmd === '/model') {
1580
- const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1581
1618
  const target = resolveMenuModelTarget.call(this, {
1582
1619
  args,
1583
1620
  session,
1584
1621
  channel,
1585
1622
  channelId,
1586
1623
  userId,
1587
- role: identity.role,
1624
+ role: optionsIdentity.role,
1588
1625
  explicitChatType,
1589
1626
  fromControlChannel,
1590
1627
  field: 'model',
@@ -1593,31 +1630,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1593
1630
  throw { code: target.code, message: target.error };
1594
1631
  const agent = this.getAgent(channel, target.baseagent);
1595
1632
  if (hasModelSwitcher(agent) && agent.listModels) {
1596
- const role = identity.role;
1597
- const authIntent = {
1598
- operation: 'model.list',
1599
- scope: target.scope,
1600
- source: 'menu',
1601
- args: {},
1602
- };
1603
- const subject = buildMenuAuthSubject(this, {
1604
- identity,
1605
- session,
1606
- explicitChatType,
1607
- channel,
1608
- channelId,
1609
- userId,
1610
- fromControlChannel,
1611
- subject: authSubject,
1612
- });
1613
- authIntent.args = target.scope === 'relation'
1614
- ? buildRelationIntentArgs({ args, selfAid: target.sel.self, peerKey: target.sel.peerKey })
1615
- : { ...(args ?? {}), self: target.sel.self };
1616
- const decision = await authorizeOperation({ source: 'menu', intent: authIntent, subject, audit: false });
1617
- if (!decision.allow)
1618
- throw { code: decision.code, message: decision.reason };
1619
1633
  const rawModels = await agent.listModels() ?? [];
1620
- const models = filterModelsForRole(role, target.baseagent, rawModels, agent.resolveModelId?.bind(agent));
1634
+ const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, agent.resolveModelId?.bind(agent));
1621
1635
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1622
1636
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1623
1637
  if (models.length > 0)
@@ -1813,12 +1827,9 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1813
1827
  if (authDenied)
1814
1828
  return authDenied;
1815
1829
  // ── /agent 查询(只读) ──
1816
- // 控制 channel:验 owners,按 args.aid 查任意 agent;agent channel:强制查自身。
1830
+ // 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
1817
1831
  if (cmdBase === '/agent') {
1818
1832
  if (fromControlChannel) {
1819
- if (!subject.isDaemonOwner) {
1820
- return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1821
- }
1822
1833
  return await execAgentQuery(args);
1823
1834
  }
1824
1835
  const selfAid = this.getOwningAgent?.(channel)?.aid;
@@ -1827,29 +1838,17 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1827
1838
  return await execAgentQuery({ ...(args ?? {}), aid: selfAid });
1828
1839
  }
1829
1840
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
1830
- // 进程级:闸门已要求 fromControlChannel;此处再验 owners 非空。
1831
1841
  if (cmdBase === '/gateway') {
1832
- if (!subject.isDaemonOwner) {
1833
- return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1834
- }
1835
1842
  return gatewayList();
1836
1843
  }
1837
1844
  // ── /config 查询(只读,查询各层配置) ──
1838
1845
  if (cmdBase === '/config') {
1839
1846
  const scope = args?.scope || 'process';
1840
1847
  if (scope === 'process') {
1841
- // 进程级配置需要 owner 权限
1842
- if (!fromControlChannel || !subject.isDaemonOwner) {
1843
- return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1844
- }
1845
1848
  const cfg = loadDaemonConfig();
1846
1849
  return { data: { scope: 'process', config: cfg } };
1847
1850
  }
1848
1851
  if (scope === 'defaults') {
1849
- // 全局默认配置需要 owner 权限
1850
- if (!fromControlChannel || !subject.isDaemonOwner) {
1851
- return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1852
- }
1853
1852
  const { read, ConfigTarget } = await import('../../config/config-manager.js');
1854
1853
  const cfg = read(ConfigTarget.Defaults, undefined, { cache: true });
1855
1854
  return { data: { scope: 'defaults', config: cfg } };
@@ -1938,9 +1937,6 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1938
1937
  return { data };
1939
1938
  }
1940
1939
  if (cmdBase === '/topic') {
1941
- if (!this.canReadTopics(identity.role)) {
1942
- return { error: '无权限查看话题', code: 'FORBIDDEN' };
1943
- }
1944
1940
  const target = (args?.target ?? '').toString().trim();
1945
1941
  if (!target)
1946
1942
  return { error: '缺少 args.target', code: 'MISSING_VALUE' };
@@ -2110,8 +2106,6 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2110
2106
  };
2111
2107
  }
2112
2108
  if (cmdBase === '/observable') {
2113
- if (identity.role !== 'owner')
2114
- return { error: '观察者模式仅限 owner 查看', code: 'NO_PERMISSION' };
2115
2109
  if (!evolagent)
2116
2110
  return { error: '找不到通道所属 agent', code: 'MISSING_AID' };
2117
2111
  const observable = evolagent?.getObservable() ?? false;
@@ -2173,9 +2167,6 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2173
2167
  };
2174
2168
  }
2175
2169
  if (cmdBase === '/system') {
2176
- if (!subject.isDaemonOwner) {
2177
- return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
2178
- }
2179
2170
  const data = {
2180
2171
  aid: loadDaemonConfig().aid ?? null,
2181
2172
  pid: process.pid,
@@ -1641,6 +1641,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1641
1641
  if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
1642
1642
  return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
1643
1643
  }
1644
+ const targetAid = aidArg ?? selfAid;
1645
+ if (!targetAid) {
1646
+ return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
1647
+ }
1644
1648
  const reloadScope = 'agent';
1645
1649
  const authDenied = await authorizeIntent({
1646
1650
  intent: {
@@ -1664,10 +1668,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1664
1668
  if (!isDaemonOwner && !isAdmin) {
1665
1669
  return { kind: 'command.error', text: '❌ 无权限:/reload 仅限 daemon owner 或 agent owner/admin 使用' };
1666
1670
  }
1667
- const targetAid = aidArg ?? selfAid;
1668
- if (!targetAid) {
1669
- return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
1670
- }
1671
1671
  // 繁忙检查(同 menu /agent reload)
1672
1672
  const busyInfo = getAgentBusyInfo(this, targetAid);
1673
1673
  if (busyInfo && busyInfo.count > 0) {