evolcore 0.0.22 → 0.0.24

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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -18,8 +18,16 @@ export function bootstrapInitialMessageOperationPrefix(aid) {
18
18
  export function bootstrapInitialRetryOperationId(aid, at = Date.now()) {
19
19
  return `${bootstrapInitialMessageOperationPrefix(aid)}:${at}`;
20
20
  }
21
- export function postBootstrapWelcomeOperationId(aid) {
22
- return `bootstrap-complete:v1:${aid}`;
21
+ export function postBootstrapWelcomeOperationId(aid, owner) {
22
+ const normalizedAid = aid.replace(/^@/, '');
23
+ return owner ? `bootstrap-complete:v2:${normalizedAid}:${owner}` : `bootstrap-complete:v1:${normalizedAid}`;
24
+ }
25
+ export function isPostBootstrapWelcomeOperationId(operationId, aid) {
26
+ if (!operationId)
27
+ return false;
28
+ const normalizedAid = aid.replace(/^@/, '');
29
+ return operationId === postBootstrapWelcomeOperationId(normalizedAid)
30
+ || operationId.startsWith(`bootstrap-complete:v2:${normalizedAid}:`);
23
31
  }
24
32
  export function renderPostBootstrapWelcome(aid, owner, ownerName) {
25
33
  const mdPath = agentMdPath(aid);
@@ -36,9 +44,13 @@ export function renderPostBootstrapWelcome(aid, owner, ownerName) {
36
44
  });
37
45
  }
38
46
  export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, delivery = { chatType: 'private' }) {
39
- const operationId = postBootstrapWelcomeOperationId(aid);
47
+ const operationId = postBootstrapWelcomeOperationId(aid, owner);
40
48
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
41
- if (hasMessageLogOperation(chatDir, operationId))
49
+ const legacyOperationId = postBootstrapWelcomeOperationId(aid);
50
+ if (hasMessageLogOperation(chatDir, operationId) || hasMessageLogOperation(chatDir, legacyOperationId))
51
+ return;
52
+ const legacy = outbox.findByDedupeKey(aid, legacyOperationId);
53
+ if (legacy?.channelId === owner)
42
54
  return;
43
55
  const existing = outbox.findByDedupeKey(aid, operationId);
44
56
  if (existing
@@ -68,21 +80,30 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, deliver
68
80
  });
69
81
  }
70
82
  export function hasPendingPostBootstrapWelcomeOutbox(aid) {
71
- const entry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
72
- return !!entry && outbox.isDeliveryForChannel(entry.delivery, entry.channelId);
83
+ return findPostBootstrapWelcomeOutbox(aid)
84
+ .some(entry => outbox.isDeliveryForChannel(entry.delivery, entry.channelId));
85
+ }
86
+ /** Keep pending welcomes from the single-owner v1 format retryable after upgrades. */
87
+ export function findPostBootstrapWelcomeOutbox(aid, opts = {}) {
88
+ const normalizedAid = aid.replace(/^@/, '');
89
+ const entries = outbox.findByDedupePrefix(normalizedAid, `bootstrap-complete:v2:${normalizedAid}:`, opts);
90
+ const legacy = outbox.findByDedupeKey(normalizedAid, postBootstrapWelcomeOperationId(normalizedAid), opts);
91
+ return [...entries, ...(legacy ? [legacy] : [])];
92
+ }
93
+ export function pendingPostBootstrapWelcomeOwners(aid) {
94
+ return findPostBootstrapWelcomeOutbox(aid)
95
+ .filter(entry => outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
96
+ .map(entry => entry.channelId)
97
+ .filter((owner, index, all) => all.indexOf(owner) === index);
73
98
  }
74
99
  /** Bind the durable completion welcome to the fresh post-bootstrap session. */
75
- export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
76
- const entry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
77
- if (!entry || !outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
100
+ export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId, owner) {
101
+ const normalizedAid = aid.replace(/^@/, '');
102
+ const entries = findPostBootstrapWelcomeOutbox(normalizedAid)
103
+ .filter(entry => (!owner || entry.channelId === owner) && outbox.isDeliveryForChannel(entry.delivery, entry.channelId));
104
+ // A session is scoped to one peer: never silently pick an arbitrary Owner.
105
+ if (!entries.length || (!owner && new Set(entries.map(entry => entry.channelId)).size !== 1))
78
106
  return false;
79
- if (entry.context?.sessionId === sessionId)
80
- return true;
81
- return outbox.replaceIfRouteMatches(aid, entry, {
82
- ...entry,
83
- context: {
84
- ...(entry.context ?? {}),
85
- sessionId,
86
- },
87
- }) === 'replaced';
107
+ return entries.every(entry => entry.context?.sessionId === sessionId
108
+ || outbox.replaceIfRouteMatches(normalizedAid, entry, { ...entry, context: { ...(entry.context ?? {}), sessionId } }) === 'replaced');
88
109
  }
@@ -52,32 +52,38 @@ export async function completeBootstrapWithWelcome(service, aid, welcome, sessio
52
52
  reloaded: false,
53
53
  };
54
54
  }
55
- try {
56
- mainSession = await sessions.ensurePostBootstrapMainSession();
57
- }
58
- catch (e) {
59
- return {
60
- ok: false,
61
- code: 'MAIN_SESSION_CREATE_FAILED',
62
- error: e instanceof Error ? e.message : `Failed to create the post-bootstrap main session for ${aid}`,
63
- reloaded: false,
64
- };
65
- }
66
- if (!mainSession.sessionId) {
67
- return {
68
- ok: false,
69
- code: 'MAIN_SESSION_CREATE_FAILED',
70
- error: `Failed to create the post-bootstrap main session for ${aid}`,
71
- reloaded: false,
72
- };
73
- }
74
- if (welcomePending && welcome && !await welcome.bindPostBootstrapWelcomeToSession(mainSession.sessionId)) {
75
- return {
76
- ok: false,
77
- code: 'WELCOME_SESSION_BIND_FAILED',
78
- error: `Failed to bind the post-bootstrap welcome to session ${mainSession.sessionId}`,
79
- reloaded: false,
80
- };
55
+ const pendingOwners = welcome?.pendingPostBootstrapWelcomeOwners?.();
56
+ const sessionOwners = pendingOwners?.length ? pendingOwners : [undefined];
57
+ for (const owner of sessionOwners) {
58
+ let ownerSession;
59
+ try {
60
+ ownerSession = await sessions.ensurePostBootstrapMainSession(owner);
61
+ }
62
+ catch (e) {
63
+ return {
64
+ ok: false,
65
+ code: 'MAIN_SESSION_CREATE_FAILED',
66
+ error: e instanceof Error ? e.message : `Failed to create the post-bootstrap main session for ${aid}`,
67
+ reloaded: false,
68
+ };
69
+ }
70
+ if (!ownerSession.sessionId) {
71
+ return {
72
+ ok: false,
73
+ code: 'MAIN_SESSION_CREATE_FAILED',
74
+ error: `Failed to create the post-bootstrap main session for ${aid}`,
75
+ reloaded: false,
76
+ };
77
+ }
78
+ if (welcomePending && welcome && !await welcome.bindPostBootstrapWelcomeToSession(ownerSession.sessionId, owner)) {
79
+ return {
80
+ ok: false,
81
+ code: 'WELCOME_SESSION_BIND_FAILED',
82
+ error: `Failed to bind the post-bootstrap welcome to session ${ownerSession.sessionId}`,
83
+ reloaded: false,
84
+ };
85
+ }
86
+ mainSession ??= ownerSession;
81
87
  }
82
88
  }
83
89
  const result = service.completeBootstrap(aid);
@@ -13,10 +13,11 @@ function defaultTypeConfig() {
13
13
  return { mode: 'inherit', overrides: {} };
14
14
  }
15
15
  function resolveBaseagent(input) {
16
- return input.baseagent
16
+ const value = input.baseagent
17
17
  || input.sessionBaseagent
18
- || input.config?.active_baseagent
19
- || 'claude';
18
+ || input.config?.active_baseagent;
19
+ const normalized = typeof value === 'string' ? value.trim() : '';
20
+ return normalized || undefined;
20
21
  }
21
22
  export function resolveCapabilityContext(input) {
22
23
  const aid = input.aid || input.config?.aid;
@@ -25,9 +26,12 @@ export function resolveCapabilityContext(input) {
25
26
  const projectPath = input.config?.projects?.defaultPath || input.projectPath || input.sessionProjectPath || null;
26
27
  if (!projectPath)
27
28
  return { error: '无法解析当前 agent projectPath', code: 'NO_PROJECT' };
29
+ const baseagent = resolveBaseagent(input);
30
+ if (!baseagent)
31
+ return { error: '无法解析当前 agent 的 baseagent', code: 'BASEAGENT_REQUIRED' };
28
32
  return {
29
33
  aid,
30
- baseagent: resolveBaseagent(input),
34
+ baseagent,
31
35
  projectPath,
32
36
  };
33
37
  }
@@ -1,6 +1,6 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
- import { execCodexCliSync } from '../../../utils/codex-cli.js';
3
+ import { execCodexCliSync } from '../../../agents/baseagent.js';
4
4
  import { discoverSkillDirectories } from '../skill-discovery.js';
5
5
  function discoverSkills(ctx) {
6
6
  const roots = [
@@ -1,4 +1,5 @@
1
- import { agentCreateNonInteractive, agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, agentReload, } from '../../cli/agent.js';
1
+ import { agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, agentReload, } from '../../cli/agent.js';
2
+ import { agentCreateNonInteractive } from '../../cli/agent-create.js';
2
3
  import { logger } from '../../utils/logger.js';
3
4
  import { resolvePaths, resolveRoot } from '../../paths.js';
4
5
  import path from 'path';
@@ -297,6 +298,7 @@ async function runCreateInBackground(opts, w, service) {
297
298
  const createOptions = {
298
299
  aid: opts.aid, name: opts.name, baseagent: opts.baseagent,
299
300
  project: opts.project, owner: opts.owner,
301
+ description: opts.description,
300
302
  onPhase: (phase, state, detail) => {
301
303
  if (state === 'begin') {
302
304
  curPhase = phase;
@@ -393,6 +395,9 @@ export async function execAgentAction(action, args, peerId, eventBus, service) {
393
395
  const model = typeof a.model === 'string' && a.model.trim() ? a.model.trim() : undefined;
394
396
  const rawEffort = typeof a.effort === 'string' && a.effort.trim() ? a.effort.trim() : undefined;
395
397
  const effort = rawEffort && rawEffort !== 'auto' ? rawEffort : undefined;
398
+ const description = typeof a.description === 'string' && a.description.trim()
399
+ ? a.description.trim()
400
+ : undefined;
396
401
  // D3: accepted=true 必须意味着后续 query 能查到该 AID 的创建状态。
397
402
  let progressWriter;
398
403
  try {
@@ -405,7 +410,7 @@ export async function execAgentAction(action, args, peerId, eventBus, service) {
405
410
  void runCreateInBackground({
406
411
  aid: a.aid, name: a.name, baseagent: a.baseagent,
407
412
  project: a.project, owner,
408
- model, effort, chatmode: a.chatmode,
413
+ model, effort, description, chatmode: a.chatmode,
409
414
  eventBus,
410
415
  }, progressWriter, service).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
411
416
  return { data: { accepted: true, aid: a.aid } };
@@ -22,7 +22,7 @@ import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
22
22
  import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../auth/authorization-audit.js';
23
23
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
24
24
  import { hasTrustedFullAccessDelegation, trustedExecutionIdentityInput } from '../auth/agent-delegation.js';
25
- import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
25
+ import { executeManagedAidOperation, ManagedAidOperationError, managedAidShowViewForRole, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
26
26
  import { executeManagedGroupOperation, isManagedGroupRelationRead, ManagedGroupOperationError, resolveManagedGroupOperation, executeManagedMsgOperation, ManagedMsgOperationError, resolveManagedMsgOperation, executeManagedFsOperation, ManagedFsOperationError, resolveManagedFsOperation, } from '../../aun/msg/managed-operation.js';
27
27
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
28
28
  import { isHClassPath, isLClassPath, isSameOrDescendant } from '../permission/protected-paths.js';
@@ -108,13 +108,19 @@ export class CommandHandler {
108
108
  interactionRouter;
109
109
  statsCollector;
110
110
  agentMap;
111
+ legacySingleRunnerMode;
111
112
  primaryRunnerKey;
112
113
  agentRegistry;
113
114
  agentApplicationService;
114
115
  triggerSchedulerResolver;
115
116
  triggerSchedulerFallback;
116
117
  triggerSchedulerFallbackAid;
118
+ /** Runtime-only owner for the daemon trigger scheduler. It is intentionally
119
+ * kept outside EvolAgentRegistry because the daemon channel is not an Agent. */
120
+ daemonTriggerOwnerAid;
121
+ daemonTriggerChannel = 'daemon';
117
122
  daemonStatusProvider;
123
+ daemonMonitorProvider;
118
124
  agentDelegationRegistry;
119
125
  handoffRuntime;
120
126
  contactRequestSubmitter;
@@ -126,23 +132,42 @@ export class CommandHandler {
126
132
  * different backend; that would corrupt session metadata.
127
133
  */
128
134
  getAgent(channel, baseagent) {
129
- if (channel && baseagent) {
135
+ const requestedBaseagent = typeof baseagent === 'string' ? baseagent.trim() : '';
136
+ // Support callers that intentionally provide a single-runner map keyed by
137
+ // the backend name. This exact lookup does not select another agent.
138
+ if (requestedBaseagent && this.agentMap.has(requestedBaseagent)) {
139
+ return this.agentMap.get(requestedBaseagent);
140
+ }
141
+ // A caller that passed one runner explicitly has already selected the
142
+ // backend; this compatibility path does not exist for multi-agent maps.
143
+ if (this.legacySingleRunnerMode && this.agentMap.size === 1
144
+ && this.agentMap.values().next().value?.name === requestedBaseagent) {
145
+ return this.agentMap.values().next().value;
146
+ }
147
+ if (channel && requestedBaseagent) {
130
148
  const owner = this.agentRegistry?.resolveByChannel(channel);
131
149
  const evolName = owner?.name || '<unknown>';
132
- const key = `${evolName}::${baseagent}`;
150
+ const key = `${evolName}::${requestedBaseagent}`;
133
151
  if (this.agentMap.has(key))
134
152
  return this.agentMap.get(key);
135
153
  if (owner) {
136
- throw new BaseagentRunnerUnavailableError(evolName, baseagent, this.getAvailableBaseagentsForOwner(evolName));
154
+ throw new BaseagentRunnerUnavailableError(evolName, requestedBaseagent, this.getAvailableBaseagentsForOwner(evolName));
137
155
  }
138
- const globalRunner = [...this.agentMap.entries()]
139
- .find(([runnerKey]) => runnerKey === baseagent || runnerKey.endsWith(`::${baseagent}`));
140
- if (globalRunner)
141
- return globalRunner[1];
142
- }
143
- if (this.agentMap.has(this.primaryRunnerKey))
144
- return this.agentMap.get(this.primaryRunnerKey);
145
- return this.agentMap.values().next().value;
156
+ const matches = [...this.agentMap.entries()]
157
+ .filter(([runnerKey]) => runnerKey.endsWith(`::${requestedBaseagent}`));
158
+ if (matches.length === 1)
159
+ return matches[0][1];
160
+ logger.error(`[RunnerLookup] unavailable channel=${channel ?? '<none>'} baseagent=${requestedBaseagent} owner=${evolName} keys=${[...this.agentMap.keys()].join(',')}`);
161
+ throw new BaseagentRunnerUnavailableError('<unknown>', requestedBaseagent, []);
162
+ }
163
+ if (requestedBaseagent) {
164
+ const matches = [...this.agentMap.entries()]
165
+ .filter(([runnerKey]) => runnerKey.endsWith(`::${requestedBaseagent}`));
166
+ if (matches.length === 1)
167
+ return matches[0][1];
168
+ }
169
+ logger.error(`[RunnerLookup] unavailable channel=${channel ?? '<none>'} baseagent=${requestedBaseagent || '<missing>'} keys=${[...this.agentMap.keys()].join(',')}`);
170
+ throw new BaseagentRunnerUnavailableError('<unknown>', requestedBaseagent || '<missing>', []);
146
171
  }
147
172
  getAvailableBaseagentsForOwner(evolName) {
148
173
  const prefix = `${evolName}::`;
@@ -200,11 +225,13 @@ export class CommandHandler {
200
225
  this.eventBus = eventBus;
201
226
  if (agentRunnerOrMap instanceof Map) {
202
227
  this.agentMap = agentRunnerOrMap;
228
+ this.legacySingleRunnerMode = false;
203
229
  this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
204
230
  }
205
231
  else {
206
232
  // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
207
233
  this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
234
+ this.legacySingleRunnerMode = true;
208
235
  this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
209
236
  }
210
237
  }
@@ -219,6 +246,32 @@ export class CommandHandler {
219
246
  setTriggerSchedulerResolver(resolver) {
220
247
  this.triggerSchedulerResolver = resolver;
221
248
  }
249
+ /** Bind the virtual daemon channel to its trigger scheduler owner AID. */
250
+ setDaemonTriggerOwner(aid, channel = 'daemon') {
251
+ const normalizedAid = String(aid || '').trim();
252
+ if (!normalizedAid)
253
+ throw new Error('daemon trigger owner AID is required');
254
+ this.daemonTriggerOwnerAid = normalizedAid;
255
+ this.daemonTriggerChannel = String(channel || 'daemon').trim() || 'daemon';
256
+ }
257
+ /** Used by control-menu routing; daemon owner is not in the Agent registry. */
258
+ getDaemonTriggerOwnerAid(channel = 'daemon') {
259
+ if (channel !== this.daemonTriggerChannel)
260
+ return undefined;
261
+ if (this.daemonTriggerOwnerAid)
262
+ return this.daemonTriggerOwnerAid;
263
+ try {
264
+ const controlAid = String(loadDaemonConfig().aid || '__daemon__').trim() || '__daemon__';
265
+ const existing = this.agentRegistry?.get?.(controlAid);
266
+ // Match index.ts: only a runnable (pre-start `stopped`) Agent causes
267
+ // the daemon scheduler owner to receive the collision suffix.
268
+ const conflicts = !!existing && (existing.status === undefined || existing.status === 'stopped');
269
+ return conflicts ? `${controlAid}#daemon` : controlAid;
270
+ }
271
+ catch {
272
+ return undefined;
273
+ }
274
+ }
222
275
  /** Compatibility entrypoint for older tests/callers that inject a single scheduler. */
223
276
  setTriggerScheduler(scheduler, manager) {
224
277
  this.triggerSchedulerFallback = scheduler;
@@ -233,6 +286,9 @@ export class CommandHandler {
233
286
  setDaemonStatusProvider(provider) {
234
287
  this.daemonStatusProvider = provider;
235
288
  }
289
+ setDaemonMonitorProvider(provider) {
290
+ this.daemonMonitorProvider = provider;
291
+ }
236
292
  /** 返回管理当前通道的 EvolAgent,无则返回 null */
237
293
  getOwningAgent(channel) {
238
294
  if (!this.agentRegistry)
@@ -240,7 +296,9 @@ export class CommandHandler {
240
296
  return this.agentRegistry.resolveByChannel(channel);
241
297
  }
242
298
  getTriggerSchedulerForChannel(channel) {
243
- const agentAid = this.getOwningAgent(channel)?.aid ?? tryParseChannelKey(channel)?.selfAID;
299
+ const agentAid = this.getOwningAgent(channel)?.aid
300
+ ?? this.getDaemonTriggerOwnerAid(channel)
301
+ ?? tryParseChannelKey(channel)?.selfAID;
244
302
  return agentAid ? this.triggerSchedulerResolver?.(agentAid) : this.triggerSchedulerFallback;
245
303
  }
246
304
  getTriggerSchedulerForAgent(agentAid) {
@@ -388,13 +446,13 @@ export class CommandHandler {
388
446
  return fallback;
389
447
  }
390
448
  }
391
- validateTriggerPermissionModeForRole(mode, isAdmin, role, selfAid, isDaemonOwner = false) {
449
+ validateTriggerPermissionModeForRole(mode, isAdmin, role, selfAid, isDaemonOwner = false, canApprovePersistentFullAccess = isDaemonOwner) {
392
450
  if (!mode)
393
451
  return undefined;
394
452
  if (mode === 'fullaccess') {
395
453
  if (!isFullAccessEnabled())
396
454
  return 'fullaccess 功能未启用';
397
- return isDaemonOwner ? undefined : '无权限:只有 daemon-owner 可以配置 fullaccess Trigger';
455
+ return canApprovePersistentFullAccess ? undefined : '无权限:只有 daemon-owner 可以配置 fullaccess Trigger';
398
456
  }
399
457
  if (!role) {
400
458
  if (mode === 'bypass' && !isAdmin)
@@ -450,7 +508,9 @@ export class CommandHandler {
450
508
  return modelDecision.message || '当前角色不允许使用该 Trigger 模型';
451
509
  }
452
510
  if (definition.execution.effort) {
453
- const base = executionBaseagent || 'claude';
511
+ if (!executionBaseagent)
512
+ return '无法确定 Trigger 会话的 baseagent';
513
+ const base = executionBaseagent;
454
514
  const effortDecision = validateRuntimeStringFieldOverride({
455
515
  selfAid: definition.agentAid,
456
516
  role,
@@ -539,7 +599,7 @@ export class CommandHandler {
539
599
  } : undefined;
540
600
  const permissionMode = this.triggerPermissionFromParsed(parsed);
541
601
  const triggerRole = this.triggerRoleContext(schedulerAid, channel, peerId, chatType);
542
- const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid, isDaemonOwner);
602
+ const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid, isDaemonOwner, canApprovePersistentFullAccess);
543
603
  if (permissionError)
544
604
  return { error: permissionError };
545
605
  if (permissionMode === 'fullaccess' && !canApprovePersistentFullAccess) {
@@ -568,7 +628,7 @@ export class CommandHandler {
568
628
  };
569
629
  }
570
630
  }
571
- const authorizedBy = permissionMode === 'fullaccess' && isDaemonOwner
631
+ const authorizedBy = permissionMode === 'fullaccess' && canApprovePersistentFullAccess
572
632
  ? this.canonicalDaemonOwnerId(schedulerAid, channel, peerId, chatType)
573
633
  : undefined;
574
634
  const definition = {
@@ -964,8 +1024,8 @@ export class CommandHandler {
964
1024
  return await menuExecMenuQuery.call(this, cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel, overrideIdentity, authSubject, source);
965
1025
  }
966
1026
  /** menu.update — 写入新值。 */
967
- async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
968
- return await menuExecMenuUpdate.call(this, cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel, args, authSubject, source);
1027
+ async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu', transportReplyContext) {
1028
+ return await menuExecMenuUpdate.call(this, cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel, args, authSubject, source, transportReplyContext);
969
1029
  }
970
1030
  /** menu.action — 触发动词。 */
971
1031
  async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext, deferSideEffect) {
@@ -975,9 +1035,9 @@ export class CommandHandler {
975
1035
  async execMenuForEcweb(payload, auth) {
976
1036
  return await menuExecMenuForEcweb.call(this, payload, auth);
977
1037
  }
978
- /** 控制 AID channel 专用入口:peerId 须 ∈ evolcore.owners,全量权限。 */
979
- async execMenuForControl(payload, peerId) {
980
- return await menuExecMenuForControl.call(this, payload, peerId);
1038
+ /** Control AID Menu entry: daemon-owner or validated remote-access token. */
1039
+ async execMenuForControl(payload, peerId, auth) {
1040
+ return await menuExecMenuForControl.call(this, payload, peerId, auth);
981
1041
  }
982
1042
  /**
983
1043
  * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
@@ -1067,7 +1127,7 @@ export class CommandHandler {
1067
1127
  /** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
1068
1128
  async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
1069
1129
  try {
1070
- const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId, undefined, undefined, undefined, undefined, undefined, opts.overrideIdentity, opts.authSubject);
1130
+ const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId, opts.threadId, opts.chatType, undefined, undefined, undefined, opts.overrideIdentity, opts.authSubject, opts.replyContext);
1071
1131
  if (result == null) {
1072
1132
  // null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
1073
1133
  return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
@@ -1344,7 +1404,7 @@ export class CommandHandler {
1344
1404
  /trigger list [--all] — 查看所有触发器
1345
1405
  /trigger create <参数> — 创建触发器(set 为兼容别名)
1346
1406
  /trigger update <名称|ID> <参数> — 修改触发器
1347
- 常用参数:--exec script|trigger-session|target-session(create 必填)、--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1407
+ 常用参数:--delay/--at/--cron/--every、--prompt、--description、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)、--feedback、--target-channel/--target-channel-id、--trigger-thread、--baseagent、--on-error、--retry-attempts/--retry-backoff、--script-runtime/--script-args/--script-timeout
1348
1408
  /trigger enable <名称|ID> — 启用触发器
1349
1409
  /trigger disable <名称|ID> — 暂停触发器
1350
1410
  /trigger show <名称|ID> — 查看触发器详情
@@ -1396,7 +1456,7 @@ export class CommandHandler {
1396
1456
  return { ok: false, error: err?.message || String(err) };
1397
1457
  }
1398
1458
  if (updated.execution.permissionMode === 'fullaccess') {
1399
- if (!isDaemonOwner) {
1459
+ if (!isDaemonOwner && !canApprovePersistentFullAccess) {
1400
1460
  return { ok: false, code: 'FORBIDDEN', error: '无权限:只有 daemon-owner 可以配置 fullaccess Trigger' };
1401
1461
  }
1402
1462
  if (!canApprovePersistentFullAccess) {
@@ -1419,7 +1479,7 @@ export class CommandHandler {
1419
1479
  return { ok: false, error: '无权限:只有 owner/admin 可以修改 script trigger' };
1420
1480
  }
1421
1481
  const triggerRole = this.triggerRoleContext(definition.agentAid, definition.origin?.channelKey || channel, definition.origin?.peerId || peerId);
1422
- const permissionError = this.validateTriggerPermissionModeForRole(updated.execution.permissionMode, isAdmin, triggerRole.role, definition.agentAid, isDaemonOwner);
1482
+ const permissionError = this.validateTriggerPermissionModeForRole(updated.execution.permissionMode, isAdmin, triggerRole.role, definition.agentAid, isDaemonOwner, canApprovePersistentFullAccess);
1423
1483
  if (permissionError) {
1424
1484
  return {
1425
1485
  ok: false,
@@ -1886,7 +1946,10 @@ export class CommandHandler {
1886
1946
  if (sessionSelfAid && sessionSelfAid !== grantSelfAid) {
1887
1947
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation self agent does not match the active session' };
1888
1948
  }
1889
- if (parsed.command.aid !== grantSelfAid && !hasTrustedFullAccessDelegation(delegation.grant)) {
1949
+ const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put';
1950
+ if (requiresSelfAid
1951
+ && parsed.command.aid !== grantSelfAid
1952
+ && !hasTrustedFullAccessDelegation(delegation.grant)) {
1890
1953
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent AID can be targeted' };
1891
1954
  }
1892
1955
  let conversationId;
@@ -1926,7 +1989,12 @@ export class CommandHandler {
1926
1989
  if (!decision.allow)
1927
1990
  return { ok: false, code: decision.code, error: decision.reason };
1928
1991
  try {
1929
- return { ok: true, result: await executeManagedAidOperation(parsed.command) };
1992
+ return {
1993
+ ok: true,
1994
+ result: await executeManagedAidOperation(parsed.command, {
1995
+ showView: managedAidShowViewForRole(subject.role),
1996
+ }),
1997
+ };
1930
1998
  }
1931
1999
  catch (error) {
1932
2000
  const message = error instanceof Error ? error.message : String(error);
@@ -2152,7 +2220,14 @@ export class CommandHandler {
2152
2220
  if (!hasTrustedFullAccessDelegation(grant) && (grant.channelType !== 'aun' || grant.chatType !== 'private')) {
2153
2221
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'Managed msg reads require the current AUN private relation' };
2154
2222
  }
2155
- if (!hasTrustedFullAccessDelegation(grant) && parsed.command.target !== conversationId) {
2223
+ // A management role may read history for any peer, one peer per query.
2224
+ // `issuedRole` is resolved daemon-side from the persisted relation
2225
+ // config, so it cannot be spoofed by the calling task. Narrower roles
2226
+ // stay pinned here, and authorizeOperation below remains the authority
2227
+ // for every role (member carries currentRelationOnly; visitor is denied).
2228
+ const mayReadCrossPeer = hasTrustedFullAccessDelegation(grant)
2229
+ || (parsed.command.kind === 'history' && isAdminRole(grant.issuedRole));
2230
+ if (!mayReadCrossPeer && parsed.command.target !== conversationId) {
2156
2231
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current AUN private relation can be targeted' };
2157
2232
  }
2158
2233
  }
@@ -2422,6 +2497,29 @@ export class CommandHandler {
2422
2497
  });
2423
2498
  if (!ctlDecision.allow)
2424
2499
  return { ok: false, code: ctlDecision.code, error: ctlDecision.reason };
2500
+ if (cmd.startsWith('/status ') && cmd !== '/status --format json') {
2501
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'status 仅支持可选参数 --format json' };
2502
+ }
2503
+ if (cmd === '/status --format json') {
2504
+ const context = {
2505
+ selfAid: grantSelfAid,
2506
+ sessionId,
2507
+ taskId: grant.taskId,
2508
+ ...(grant.messageId ? { messageId: grant.messageId } : {}),
2509
+ baseagent: session.baseagent,
2510
+ channel: grant.channelType,
2511
+ channelKey: grant.channel,
2512
+ channelId: conversationId,
2513
+ chatType: grant.chatType,
2514
+ peerKey: grant.peerKey,
2515
+ peerId: conversationId,
2516
+ peerRole: ctlSubject.role,
2517
+ permissionMode: grant.permissionMode ?? ctlSubject.permissionMode,
2518
+ projectPath: path.resolve(session.projectPath),
2519
+ ...(grant.sessionRuntimeDir ? { tmpDir: grant.sessionRuntimeDir } : {}),
2520
+ };
2521
+ return { ok: true, result: JSON.stringify(context, null, 2), data: context };
2522
+ }
2425
2523
  if (cmd.startsWith('/model info ')) {
2426
2524
  const modelId = cmd.slice('/model info '.length).trim();
2427
2525
  if (!modelId || /\s/.test(modelId))