evolcore 0.0.23 → 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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  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 +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
@@ -48,6 +48,8 @@ export function getManagementCommandPermissions(role) {
48
48
  'config.unset': { allow: true, scopes: ['agent', 'relation'] },
49
49
  'role.policy.write': { allow: true, dangerous: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
50
50
  'ec.aid.show': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
51
+ 'ec.aid.lookup': { allow: true, scopes: ['agent'] },
52
+ 'ec.aid.agentmd.get': { allow: true, scopes: ['agent'] },
51
53
  'ec.aid.agentmd.put': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
52
54
  'ec.fs.mkdir': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
53
55
  'ec.fs.upload': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
@@ -33,10 +33,11 @@ export function buildAuthSubject(input) {
33
33
  && fullAccessAuthorization.processRole === 'fullaccess-run'
34
34
  && fullAccessAuthorization.dataScope === 'daemon';
35
35
  const isDaemonService = input.trustedProcessRole === 'daemon-service';
36
- const isDaemonOwner = !isDaemonService && !isFullAccessRun && isProcessOwner({
36
+ const isTrustedDaemonOwner = input.trustedProcessRole === 'daemon-owner';
37
+ const isDaemonOwner = !isDaemonService && !isFullAccessRun && (isTrustedDaemonOwner || isProcessOwner({
37
38
  actor: roleDetail.actor,
38
39
  processOwners,
39
- });
40
+ }));
40
41
  const suppliedRole = input.identity?.role && input.identity.role !== 'none' ? input.identity.role : undefined;
41
42
  const role = suppliedRole ?? roleDetail.effectiveRole ?? 'none';
42
43
  const processRole = isFullAccessRun
@@ -10,6 +10,8 @@ export function auditCommandAuthorization(event) {
10
10
  (event.decision === 'allow' && event.dangerous) ||
11
11
  (event.source === 'agent-tool' && event.operation.startsWith('config.')) ||
12
12
  event.operation === 'codex.approval' ||
13
+ event.operation === 'permission.approval' ||
14
+ event.operation === 'permission.runtime' ||
13
15
  event.operation.startsWith('fullaccess.') ||
14
16
  event.operation.startsWith('role.') ||
15
17
  event.operation === 'cli.exec.raw';
@@ -198,33 +200,78 @@ export function auditCodexApprovalDecision(input) {
198
200
  * infrastructure denials without parsing localized log text.
199
201
  */
200
202
  export function auditPermissionDecision(input) {
201
- auditCommandAuthorization({
202
- ts: Date.now(),
203
- callId: input.requestId,
204
- correlationId: input.requestId,
205
- requestId: input.requestId,
203
+ const requestId = input.requestId ?? `perm-audit-${crypto.randomUUID()}`;
204
+ auditPermissionLifecycle({
205
+ phase: 'final',
206
+ requestId,
206
207
  sessionId: input.sessionId,
207
208
  agentAid: input.agentAid,
208
209
  agentName: input.agentName,
209
210
  permissionMode: input.permissionMode,
210
211
  toolName: input.toolName,
212
+ decision: 'deny',
213
+ decisionSource: input.decisionSource,
214
+ policyCode: input.policyCode,
215
+ reason: input.reason,
216
+ role: input.role,
217
+ taskId: input.taskId,
218
+ summary: input.summary,
219
+ operation: 'permission.runtime',
220
+ });
221
+ }
222
+ /**
223
+ * Emit a canonical request/response/final lifecycle for runtime approvals.
224
+ * All three phases share one eventKey and complete correlation fields so
225
+ * daily analysis can group them without parsing runner-specific log text.
226
+ */
227
+ export function auditPermissionLifecycle(input) {
228
+ const sessionId = input.sessionId ?? 'unknown';
229
+ auditCommandAuthorization({
230
+ ts: Date.now(),
231
+ eventKey: buildToolLifecycleEventKey({
232
+ sessionId,
233
+ callId: input.callId,
234
+ correlationId: input.correlationId,
235
+ requestId: input.requestId,
236
+ }),
237
+ eventPhase: input.phase,
238
+ callId: input.callId ?? input.requestId,
239
+ correlationId: input.correlationId ?? input.callId ?? input.requestId,
240
+ requestId: input.requestId,
241
+ sessionId,
242
+ agentAid: input.agentAid ?? 'unknown',
243
+ agentName: input.agentName,
244
+ permissionMode: input.permissionMode ?? 'unknown',
245
+ toolName: input.toolName,
246
+ approvalMethod: input.approvalMethod,
211
247
  policyCode: input.policyCode,
212
248
  decisionSource: input.decisionSource,
213
- errorCode: input.decisionSource === 'approval'
214
- ? 'USER_DENIED'
215
- : input.decisionSource === 'policy'
216
- ? 'POLICY_DENIED'
217
- : infrastructureErrorCode(input.policyCode),
249
+ errorCode: input.decision === 'deny'
250
+ ? input.decisionSource === 'approval'
251
+ ? 'USER_DENIED'
252
+ : input.decisionSource === 'policy'
253
+ ? 'POLICY_DENIED'
254
+ : infrastructureErrorCode(input.policyCode)
255
+ : undefined,
218
256
  source: 'agent-tool',
219
- operation: 'permission.runtime',
257
+ operation: input.operation ?? 'permission.approval',
220
258
  scope: 'filesystem',
221
259
  dangerous: false,
222
260
  role: input.role ?? 'unknown',
223
261
  taskId: input.taskId,
224
- argsSummary: input.summary ? { summary: input.summary } : undefined,
225
- decision: 'deny',
262
+ argsSummary: input.summary || input.response
263
+ ? {
264
+ ...(input.summary ? { summary: input.summary } : {}),
265
+ ...(input.response ? { response: input.response } : {}),
266
+ }
267
+ : undefined,
268
+ decision: input.decision,
226
269
  executed: false,
227
- executionState: 'blocked',
270
+ executionState: input.phase === 'request'
271
+ ? 'requested'
272
+ : input.phase === 'response'
273
+ ? 'responded'
274
+ : input.decision === 'deny' ? 'blocked' : 'authorized',
228
275
  reason: input.reason,
229
276
  });
230
277
  }
@@ -270,7 +317,7 @@ function infrastructureErrorCode(policyCode) {
270
317
  }
271
318
  function buildAuditRecord(event) {
272
319
  const executed = event.executed ?? false;
273
- const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : 'command');
320
+ const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : event.decision === 'pending' ? 'approval' : 'command');
274
321
  const errorCode = event.errorCode ?? (event.decision === 'deny'
275
322
  ? decisionSource === 'approval'
276
323
  ? 'USER_DENIED'
@@ -281,9 +328,11 @@ function buildAuditRecord(event) {
281
328
  const executionState = event.executionState
282
329
  ?? (event.decision === 'deny'
283
330
  ? 'blocked'
284
- : executed
285
- ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
286
- : 'authorized');
331
+ : event.decision === 'pending'
332
+ ? 'requested'
333
+ : executed
334
+ ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
335
+ : 'authorized');
287
336
  const lifecycleId = event.callId ?? event.correlationId ?? event.requestId;
288
337
  return {
289
338
  ts: event.ts,
@@ -386,7 +435,11 @@ function logAuditEvent(record) {
386
435
  : routineEcwebRoleRead
387
436
  ? 'debug'
388
437
  : 'info';
389
- const marker = record.decision === 'deny' ? 'DENY' : record.dangerous ? 'DANGEROUS_ALLOW' : 'ALLOW';
438
+ const marker = record.decision === 'deny'
439
+ ? 'DENY'
440
+ : record.decision === 'pending'
441
+ ? 'PENDING'
442
+ : record.dangerous ? 'DANGEROUS_ALLOW' : 'ALLOW';
390
443
  const message = [
391
444
  `[CommandAudit:${marker}]`,
392
445
  `operation=${record.operation}`,
@@ -34,6 +34,9 @@ const DAEMON_SERVICE_OPERATIONS = new Set([
34
34
  'trigger.cancel',
35
35
  'trigger.delete',
36
36
  'trigger.run',
37
+ 'agent.reload',
38
+ 'system.restart',
39
+ 'system.upgrade',
37
40
  ]);
38
41
  export function authorizeCommand(ctx) {
39
42
  return authorizeCommandInternal(ctx);
@@ -121,6 +124,47 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
121
124
  return denyDecision(ctx, 'NOT_ALLOWED', `Operation ${operation} is not available from source ${ctx.source}`, operation, intent.scope, opMeta.dangerous);
122
125
  }
123
126
  if (ctx.processRole === 'daemon-service') {
127
+ // ECWeb uses the authenticated daemon service for its read-only menu
128
+ // bootstrap (model/effort, agent metadata, trigger views, etc.). These
129
+ // queries are safe to authorize by catalog category; mutating operations
130
+ // remain restricted to the explicit daemon-service allowlist below.
131
+ const ECWEB_SERVICE_READ_OPERATIONS = new Set([
132
+ 'system.status', 'model.current', 'model.list', 'baseagent.models.list',
133
+ 'model.effort.current', 'agent.baseagent.current', 'agent.baseagent.list',
134
+ 'agent.show',
135
+ 'agent.list',
136
+ 'capability.read',
137
+ 'gateway.read', 'stats.summary', 'stats.session', 'stats.context',
138
+ 'role.policy.read', 'role.relation.read', 'config.read',
139
+ 'model.info', 'model.check',
140
+ 'trigger.list', 'trigger.show', 'trigger.history', 'trigger.eventCatalog',
141
+ ]);
142
+ if (ECWEB_SERVICE_READ_OPERATIONS.has(operation)) {
143
+ return {
144
+ allow: true,
145
+ operation,
146
+ scope: intent.scope,
147
+ role: 'daemon-service',
148
+ dangerous: opMeta.dangerous,
149
+ matchedRule: 'daemon-service-read',
150
+ };
151
+ }
152
+ // The in-process service may perform system lifecycle actions only when
153
+ // the request came through the authenticated ECWeb management surface.
154
+ // A raw daemon-service subject on the control source is not a human owner.
155
+ if ((operation === 'system.restart' || operation === 'system.upgrade') && ctx.source === 'control') {
156
+ return denyDecision(ctx, 'NOT_ALLOWED', `Daemon service is not authorized for ${operation}`, operation, intent.scope, opMeta.dangerous, 'daemon-service');
157
+ }
158
+ if (ctx.source !== 'control' && (operation === 'observable.current' || operation === 'observable.update')) {
159
+ return {
160
+ allow: true,
161
+ operation,
162
+ scope: intent.scope,
163
+ role: 'daemon-service',
164
+ dangerous: opMeta.dangerous,
165
+ matchedRule: 'ecweb-daemon-service-agent-management',
166
+ };
167
+ }
124
168
  if (!DAEMON_SERVICE_OPERATIONS.has(operation)) {
125
169
  return denyDecision(ctx, 'NOT_ALLOWED', `Daemon service is not authorized for ${operation}`, operation, intent.scope, opMeta.dangerous, 'daemon-service');
126
170
  }
@@ -189,6 +233,20 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
189
233
  if (roleDef.allowAccess === false) {
190
234
  return denyDecision(ctx, 'ROLE_ACCESS_DENIED', `Role ${role} is not allowed to access commands`, operation, intent.scope, opMeta.dangerous);
191
235
  }
236
+ // AID lookup and public agent.md reads are available to every accessible
237
+ // role and may target any AID that passed managed-command validation. The
238
+ // operation source is restricted to agent-tool by the catalog.
239
+ if ((operation === 'ec.aid.lookup' || operation === 'ec.aid.agentmd.get')
240
+ && (intent.scope === 'relation' || intent.scope === 'agent')) {
241
+ return {
242
+ allow: true,
243
+ operation,
244
+ scope: intent.scope,
245
+ role,
246
+ dangerous: opMeta.dangerous,
247
+ matchedRule: 'public-aid-read',
248
+ };
249
+ }
192
250
  let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
193
251
  // Agent self-service contact add deliberately reuses the same low-trust
194
252
  // relation boundary as ec.msg.send. Older per-Agent role files may still
@@ -317,7 +375,9 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
317
375
  return false;
318
376
  }
319
377
  function hasDaemonOwnerRole(ctx) {
320
- return ctx.isDaemonOwner === true || ctx.processRole === 'daemon-owner';
378
+ return ctx.isDaemonOwner === true
379
+ || ctx.processRole === 'daemon-owner'
380
+ || ctx.processRole === 'daemon-service';
321
381
  }
322
382
  function daemonOwnerDenialReasonCode(ctx) {
323
383
  if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload')
@@ -340,8 +400,8 @@ function isCurrentAgentTarget(ctx) {
340
400
  return !!ctx.selfAid && !!targetAid && targetAid === ctx.selfAid;
341
401
  }
342
402
  function daemonOwnerRequiredMessage(operation) {
343
- return `${operation} is a daemon-level operation and requires DaemonOwner. `
344
- + 'Agent owner/admin and permissionMode=bypass do not grant this permission.';
403
+ return `无权限:${operation} 是 daemon 级操作,需要 DaemonOwner。`
404
+ + 'Agent owner/admin 和 permissionMode=bypass 均不能授予此权限。';
345
405
  }
346
406
  function getRuleRank(rule, permission, operation, category, isDangerous) {
347
407
  const denyOffset = permission.allow ? 0 : 1;
@@ -116,6 +116,14 @@ const QUEUE_OPERATIONS = [['read', 'Read the current Agent message queue'], ['cl
116
116
  */
117
117
  const OPERATIONS = [
118
118
  // ── Model Operations ──
119
+ {
120
+ id: 'baseagent.models.list',
121
+ category: 'read',
122
+ dangerous: false,
123
+ defaultScopes: ['process'],
124
+ description: '查询主机默认 Baseagent 模型目录',
125
+ sources: ['menu', 'ecweb', 'control'],
126
+ },
119
127
  {
120
128
  id: 'model.list',
121
129
  category: 'read',
@@ -917,7 +925,7 @@ const OPERATIONS = [
917
925
  category: 'read',
918
926
  dangerous: false,
919
927
  defaultScopes: ['relation', 'agent'],
920
- description: 'Look up the current Agent public AID record',
928
+ description: 'Look up a public AID record',
921
929
  sources: ['agent-tool'],
922
930
  },
923
931
  {
@@ -925,7 +933,7 @@ const OPERATIONS = [
925
933
  category: 'read',
926
934
  dangerous: false,
927
935
  defaultScopes: ['relation', 'agent'],
928
- description: 'Read and verify the current Agent public agent.md',
936
+ description: 'Read and verify a public Agent agent.md',
929
937
  sources: ['agent-tool'],
930
938
  },
931
939
  {
@@ -2,8 +2,9 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { resolvePaths } from '../../paths.js';
5
- export const REMOTE_ACCESS_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
6
- export const REMOTE_ACCESS_PAIRING_TTL_MS = 5 * 60 * 1000;
5
+ export const REMOTE_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
6
+ // 与 owner 绑定二维码(BindService)的默认有效期保持一致:10 分钟。
7
+ export const REMOTE_ACCESS_PAIRING_TTL_MS = 10 * 60 * 1000;
7
8
  export function remoteAccessTokenStorePath() {
8
9
  return path.join(resolvePaths().instanceDir, 'control-access-tokens.json');
9
10
  }
@@ -20,16 +20,16 @@ export class AgentLoader {
20
20
  * Iterate over all EvolAgents in the registry × all registered plugins.
21
21
  * Each successful (agent, plugin) pair yields one runner instance.
22
22
  */
23
- createAll(registry, callbacks, creationErrors) {
23
+ async createAll(registry, callbacks, creationErrors) {
24
24
  const instances = [];
25
25
  const allAgents = registry.runnableAgents();
26
26
  for (const agent of allAgents) {
27
- instances.push(...this.createForAgent(agent, callbacks, creationErrors));
27
+ instances.push(...await this.createForAgent(agent, callbacks, creationErrors));
28
28
  }
29
29
  return instances;
30
30
  }
31
31
  /** Create runners for one EvolAgent, used by runtime hot-load. */
32
- createForAgent(agent, callbacks, creationErrors) {
32
+ async createForAgent(agent, callbacks, creationErrors) {
33
33
  const instances = [];
34
34
  for (const [pluginName, plugin] of this.plugins) {
35
35
  if (!plugin.isEnabled(agent)) {
@@ -37,7 +37,7 @@ export class AgentLoader {
37
37
  continue;
38
38
  }
39
39
  try {
40
- const instance = plugin.createAgent(agent, callbacks);
40
+ const instance = await plugin.createAgent(agent, callbacks);
41
41
  if (!instance) {
42
42
  logger.debug(`Plugin '${pluginName}' returned null for agent '${agent.name}', skipping`);
43
43
  continue;
@@ -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 } };