evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -6,6 +6,7 @@ import { hasModelSwitcher } from '../../agents/runner-types.js';
6
6
  import { getCodexEfforts } from '../../agents/codex-runner.js';
7
7
  import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
9
+ import { inspectDataMigrationRequirement } from '../data-migration.js';
9
10
  import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
10
11
  import path from 'path';
11
12
  import fs from 'fs';
@@ -16,7 +17,7 @@ import { CronExpressionParser } from 'cron-parser';
16
17
  import { parseDuration } from '../../trigger/parser.js';
17
18
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
18
19
  import { commandExists, spawnDetachedNode } from '../../utils/cross-platform.js';
19
- import { loadDefaults, loadDaemonConfig } from '../../config-store.js';
20
+ import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
20
21
  import { WEB_PACKAGE_NAME } from '../../product.js';
21
22
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
22
23
  import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './agent-control.js';
@@ -25,7 +26,7 @@ import { displaySessionTitle } from '../session/session-title.js';
25
26
  import { buildSessionTurnList } from '../session/session-turns.js';
26
27
  import { isCapabilityType, listCapabilityOptions, queryCapabilityTypes, resolveCapabilityContext, updateCapabilityPolicy, } from '../capability/capability-manager.js';
27
28
  import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArgv, withDefaultRelationContext } from './cli-intent-parser.js';
28
- import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
29
+ import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../auth/authorization-audit.js';
29
30
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
30
31
  import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
31
32
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
@@ -43,6 +44,7 @@ import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, role
43
44
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
44
45
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
45
46
  import { getMenuCatalog, execMenuValueBatch, resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from './menu-catalog.js';
47
+ import { probeSessionActivity } from './slash-gate.js';
46
48
  /**
47
49
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
48
50
  * 失败返回 null(命令不存在或执行失败)。
@@ -455,6 +457,24 @@ function menuConfigSource(fieldPath, sel) {
455
457
  catch { }
456
458
  return null;
457
459
  }
460
+ /** Read the explicit value stored in the scope being queried, without resolving inheritance. */
461
+ function readMenuScopeOverride(scope, fieldPath, sel) {
462
+ const target = scope === 'agent' ? ConfigTarget.Agent : ConfigTarget.Relation;
463
+ try {
464
+ let current = cfgRead(target, sel, { cache: false });
465
+ for (const part of fieldPath.split('.')) {
466
+ if (!current || typeof current !== 'object'
467
+ || !Object.prototype.hasOwnProperty.call(current, part)) {
468
+ return null;
469
+ }
470
+ current = current[part];
471
+ }
472
+ return current === undefined ? null : current;
473
+ }
474
+ catch {
475
+ return null;
476
+ }
477
+ }
458
478
  function menuModelConfigFieldPath(target) {
459
479
  return target.fieldPath;
460
480
  }
@@ -1131,6 +1151,8 @@ function buildRoleMenuContext(owner, channel, subject) {
1131
1151
  actorRoleSource: subject.roleSource,
1132
1152
  actorAllowAccess: subject.allowAccess,
1133
1153
  actorAid: subject.actorId,
1154
+ isDaemonOwner: subject.isDaemonOwner,
1155
+ fromControlChannel: subject.fromControlChannel,
1134
1156
  chatType: subject.chatType,
1135
1157
  aunAvailable,
1136
1158
  ...(contactAdapter?.send ? {
@@ -1549,7 +1571,9 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1549
1571
  // 控制 channel:返回全量;agent channel:仅返回自身单条。
1550
1572
  if (cmd === '/agent') {
1551
1573
  if (fromControlChannel) {
1552
- const res = await execAgentOptions(args);
1574
+ const res = this.agentApplicationService
1575
+ ? await execAgentOptions(args, this.agentApplicationService)
1576
+ : await execAgentOptions(args);
1553
1577
  if ('error' in res)
1554
1578
  throw { code: res.code, message: res.error };
1555
1579
  return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
@@ -1558,7 +1582,9 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1558
1582
  const selfAid = this.getOwningAgent?.(channel)?.aid;
1559
1583
  if (!selfAid)
1560
1584
  throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1561
- const res = await execAgentQuery({ aid: selfAid });
1585
+ const res = this.agentApplicationService
1586
+ ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService)
1587
+ : await execAgentQuery({ aid: selfAid });
1562
1588
  if ('error' in res)
1563
1589
  throw { code: res.code, message: res.error };
1564
1590
  const ag = res.data;
@@ -1902,12 +1928,21 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1902
1928
  // 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
1903
1929
  if (cmdBase === '/agent') {
1904
1930
  if (fromControlChannel) {
1905
- return args?.aid ? await execAgentQuery(args) : await execAgentOptions(args);
1931
+ if (args?.aid) {
1932
+ return this.agentApplicationService
1933
+ ? await execAgentQuery(args, this.agentApplicationService)
1934
+ : await execAgentQuery(args);
1935
+ }
1936
+ return this.agentApplicationService
1937
+ ? await execAgentOptions(args, this.agentApplicationService)
1938
+ : await execAgentOptions(args);
1906
1939
  }
1907
1940
  const selfAid = this.getOwningAgent?.(channel)?.aid;
1908
1941
  if (!selfAid)
1909
1942
  return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
1910
- return await execAgentQuery({ ...(args ?? {}), aid: selfAid });
1943
+ return this.agentApplicationService
1944
+ ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService)
1945
+ : await execAgentQuery({ ...(args ?? {}), aid: selfAid });
1911
1946
  }
1912
1947
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
1913
1948
  if (cmdBase === '/gateway') {
@@ -2058,12 +2093,15 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2058
2093
  : null;
2059
2094
  const targetAgent = requestedAgent ?? evolagent;
2060
2095
  const value = targetAgent?.baseagent ?? targetAgent?.config?.active_baseagent ?? null;
2096
+ const override = targetAgent?.aid
2097
+ ? readMenuScopeOverride('agent', 'active_baseagent', { self: targetAgent.aid })
2098
+ : null;
2061
2099
  const source = value == null
2062
2100
  ? null
2063
2101
  : targetAgent?.aid
2064
2102
  ? menuConfigSource('active_baseagent', { self: targetAgent.aid }) ?? 'agent'
2065
2103
  : 'agent';
2066
- return { data: { baseagent: value, scope: 'agent', source } };
2104
+ return { data: { baseagent: value, scope: 'agent', override, source } };
2067
2105
  }
2068
2106
  if (cmdBase === '/model') {
2069
2107
  const target = resolveMenuModelTarget.call(this, {
@@ -2087,6 +2125,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2087
2125
  baseagent: target.baseagent,
2088
2126
  source: current.source,
2089
2127
  scope: target.scope,
2128
+ override: readMenuScopeOverride(target.scope, target.fieldPath, target.sel),
2090
2129
  field: target.fieldPath,
2091
2130
  self: target.sel.self,
2092
2131
  ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
@@ -2115,6 +2154,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2115
2154
  baseagent: target.baseagent,
2116
2155
  source: current.source,
2117
2156
  scope: target.scope,
2157
+ override: readMenuScopeOverride(target.scope, target.fieldPath, target.sel),
2118
2158
  field: target.fieldPath,
2119
2159
  self: target.sel.self,
2120
2160
  ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
@@ -2140,6 +2180,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2140
2180
  mode: current.value,
2141
2181
  source: current.source,
2142
2182
  scope: target.scope,
2183
+ override: readMenuScopeOverride(target.scope, target.fieldPath, target.sel),
2143
2184
  field: target.fieldPath,
2144
2185
  self: target.sel.self,
2145
2186
  ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
@@ -2170,6 +2211,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2170
2211
  mode: current.value,
2171
2212
  source: current.source,
2172
2213
  scope: target.scope,
2214
+ override: readMenuScopeOverride(target.scope, target.fieldPath, target.sel),
2173
2215
  field: target.fieldPath,
2174
2216
  self: target.sel.self,
2175
2217
  ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
@@ -2180,10 +2222,13 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2180
2222
  if (!evolagent)
2181
2223
  return { error: '找不到通道所属 agent', code: 'MISSING_AID' };
2182
2224
  const observable = evolagent?.getObservable() ?? false;
2225
+ const override = evolagent?.aid
2226
+ ? readMenuScopeOverride('agent', 'observable', { self: evolagent.aid })
2227
+ : null;
2183
2228
  const source = evolagent?.aid
2184
2229
  ? menuConfigSource('observable', { self: evolagent.aid }) ?? 'builtin'
2185
2230
  : 'builtin';
2186
- return { data: { observable, source } };
2231
+ return { data: { observable, scope: 'agent', override, source } };
2187
2232
  }
2188
2233
  if (cmdBase === '/perm') {
2189
2234
  const target = resolveMenuPermissionTarget.call(this, {
@@ -2231,6 +2276,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2231
2276
  mode: current.value,
2232
2277
  source: current.source,
2233
2278
  scope: target.scope,
2279
+ override: readMenuScopeOverride(target.scope, target.fieldPath, target.sel),
2234
2280
  field: target.fieldPath,
2235
2281
  self: target.sel.self,
2236
2282
  ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
@@ -2420,11 +2466,13 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2420
2466
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
2421
2467
  if (!triggerScheduler)
2422
2468
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
2423
- const updated = await this.updateTriggerFromPatch(triggerScheduler, patch.nameOrId, patch, channel, channelId, userId ?? '', isAdmin);
2469
+ const updated = await this.updateTriggerFromPatch(triggerScheduler, patch.nameOrId, patch, channel, channelId, userId ?? '', isAdmin, undefined, undefined, subject.isDaemonOwner, subject.canApprovePersistentFullAccess);
2424
2470
  if (!updated.ok) {
2425
2471
  return {
2426
2472
  error: updated.error,
2427
- code: updated.code ?? (/不存在|无权限/.test(updated.error) ? 'NOT_FOUND' : 'INVALID_ARGS'),
2473
+ code: updated.code ?? (/不存在/.test(updated.error) ? 'NOT_FOUND'
2474
+ : /无权限|只有 daemon-owner|不能批准/.test(updated.error) ? 'FORBIDDEN'
2475
+ : 'INVALID_ARGS'),
2428
2476
  ...(updated.currentRevision ? { data: { currentRevision: updated.currentRevision } } : {}),
2429
2477
  };
2430
2478
  }
@@ -2474,7 +2522,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2474
2522
  return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
2475
2523
  }
2476
2524
  const previousBaseagent = evolagent.baseagent;
2477
- evolagent.setActiveBaseagent(arg);
2525
+ try {
2526
+ evolagent.setActiveBaseagent(arg);
2527
+ }
2528
+ catch (error) {
2529
+ return { error: error instanceof Error ? error.message : String(error), code: 'INVALID_VALUE' };
2530
+ }
2478
2531
  this.eventBus.publish({
2479
2532
  type: 'agent:baseagent-changed',
2480
2533
  aid: evolagent.aid,
@@ -2890,23 +2943,31 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2890
2943
  if (!a.aid)
2891
2944
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
2892
2945
  const hooks = globalThis.__evolcore_reloadHooks;
2893
- if (!hooks)
2946
+ if (!this.agentApplicationService && !hooks)
2894
2947
  return { error: 'Reload hooks 未初始化', code: 'INTERNAL' };
2895
2948
  try {
2896
2949
  if (action === 'stop') {
2897
- if (!this.agentRegistry?.stopAgent)
2898
- return { error: 'stopAgent 不可用', code: 'INTERNAL' };
2899
- await this.agentRegistry.stopAgent(a.aid, hooks);
2950
+ const result = this.agentApplicationService
2951
+ ? await this.agentApplicationService.stop(a.aid)
2952
+ : this.agentRegistry?.stopAgent
2953
+ ? await this.agentRegistry.stopAgent(a.aid, hooks).then(() => ({ ok: true, aid: a.aid }))
2954
+ : { ok: false, error: 'stopAgent 不可用', code: 'INTERNAL' };
2955
+ if (!result.ok)
2956
+ return { error: result.error, code: result.code || 'INTERNAL' };
2900
2957
  this.eventBus.publish({ type: 'agent:stopped', aid: a.aid, timestamp: Date.now() });
2901
2958
  // 中断该 agent 正在执行的大模型调用
2902
- const handle = this.agentRegistry.get(a.aid);
2959
+ const handle = this.agentRegistry?.get(a.aid);
2903
2960
  if (handle)
2904
2961
  this.messageQueue.interruptByAgent(handle.name);
2905
2962
  }
2906
2963
  else {
2907
- if (!this.agentRegistry?.startAgent)
2908
- return { error: 'startAgent 不可用', code: 'INTERNAL' };
2909
- await this.agentRegistry.startAgent(a.aid, hooks);
2964
+ const result = this.agentApplicationService
2965
+ ? await this.agentApplicationService.start(a.aid)
2966
+ : this.agentRegistry?.startAgent
2967
+ ? await this.agentRegistry.startAgent(a.aid, hooks).then(() => ({ ok: true, aid: a.aid }))
2968
+ : { ok: false, error: 'startAgent 不可用', code: 'INTERNAL' };
2969
+ if (!result.ok)
2970
+ return { error: result.error, code: result.code || 'INTERNAL' };
2910
2971
  this.eventBus.publish({ type: 'agent:started', aid: a.aid, timestamp: Date.now() });
2911
2972
  }
2912
2973
  return { data: { aid: a.aid, action } };
@@ -2929,7 +2990,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2929
2990
  }
2930
2991
  }
2931
2992
  }
2932
- return await execAgentAction(action, a, userId ?? '', this.eventBus);
2993
+ return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService);
2933
2994
  }
2934
2995
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
2935
2996
  if (cmdBase === '/trigger') {
@@ -2979,9 +3040,15 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2979
3040
  triggerThread,
2980
3041
  baseagent: args.baseagent,
2981
3042
  };
2982
- const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined, this.resolveMenuChatType(channel, channelId, explicitChatType), undefined, isAdmin);
2983
- if (!r.ok)
2984
- return { error: r.error, code: /已存在|exists|重复/.test(r.error) ? 'CONFLICT' : 'INVALID_ARGS' };
3043
+ const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined, this.resolveMenuChatType(channel, channelId, explicitChatType), undefined, isAdmin, subject.isDaemonOwner, subject.canApprovePersistentFullAccess);
3044
+ if (!r.ok) {
3045
+ const code = /已存在|exists|重复/.test(r.error)
3046
+ ? 'CONFLICT'
3047
+ : /不能批准|只有 daemon-owner|无权限/.test(r.error)
3048
+ ? 'FORBIDDEN'
3049
+ : 'INVALID_ARGS';
3050
+ return { error: r.error, code };
3051
+ }
2985
3052
  return { data: { id: r.trigger.id, name: r.trigger.name, nextFireAt: r.trigger.nextFireAt } };
2986
3053
  }
2987
3054
  if (action === 'cancel') {
@@ -3006,7 +3073,35 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3006
3073
  const trigger = this.findTriggerDefinition(triggerScheduler, nameOrId, userId ?? '', channel, isAdmin);
3007
3074
  if (!trigger)
3008
3075
  return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
3076
+ if (action === 'enable' && trigger.execution.permissionMode === 'fullaccess') {
3077
+ if (!isFullAccessEnabled())
3078
+ return { error: 'fullaccess 功能未启用', code: 'NOT_SUPPORTED' };
3079
+ if (!subject.isDaemonOwner)
3080
+ return { error: '只有 daemon-owner 可以启用 fullaccess Trigger', code: 'FORBIDDEN' };
3081
+ if (subject.canApprovePersistentFullAccess === false) {
3082
+ return { error: '当前认证通道不能批准持久 fullaccess Trigger', code: 'FORBIDDEN' };
3083
+ }
3084
+ const authorizedBy = subject.principalId ?? userId;
3085
+ if (!authorizedBy)
3086
+ return { error: '无法确认 fullaccess Trigger 的批准者', code: 'FORBIDDEN' };
3087
+ // Enabling is itself a persistent fullaccess configuration action. Keep
3088
+ // the canonical owner provenance on definitions created before this
3089
+ // field existed, or after an owner explicitly re-enables the Trigger.
3090
+ triggerScheduler.update(trigger.id, { ...trigger, authorizedBy });
3091
+ }
3009
3092
  const updated = triggerScheduler.setEnabled(trigger.id, action === 'enable');
3093
+ if (action === 'enable' && updated.execution.permissionMode === 'fullaccess') {
3094
+ auditFullAccessEvent({
3095
+ event: 'fullaccess.trigger.configured',
3096
+ source: 'trigger',
3097
+ actorId: userId,
3098
+ processRole: subject.isDaemonOwner ? 'daemon-owner' : 'none',
3099
+ agentAid: updated.agentAid,
3100
+ triggerId: updated.id,
3101
+ authorizedBy: updated.authorizedBy,
3102
+ reason: 'fullaccess Trigger enabled',
3103
+ });
3104
+ }
3010
3105
  return { data: { id: updated.id, enabled: updated.enabled } };
3011
3106
  }
3012
3107
  if (action === 'delete') {
@@ -3021,6 +3116,18 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3021
3116
  if (trigger.enabled)
3022
3117
  return { error: '请先禁用触发器再删除', code: 'INVALID_STATE' };
3023
3118
  const deleted = triggerScheduler.delete(trigger.id);
3119
+ if (deleted.execution.permissionMode === 'fullaccess') {
3120
+ auditFullAccessEvent({
3121
+ event: 'fullaccess.trigger.deescalated',
3122
+ source: 'trigger',
3123
+ actorId: userId,
3124
+ processRole: subject.isDaemonOwner ? 'daemon-owner' : 'none',
3125
+ agentAid: deleted.agentAid,
3126
+ triggerId: deleted.id,
3127
+ authorizedBy: deleted.authorizedBy,
3128
+ reason: 'fullaccess Trigger deleted',
3129
+ });
3130
+ }
3024
3131
  return { data: { id: deleted.id, deleted: true } };
3025
3132
  }
3026
3133
  if (action === 'run' || action === 'test') {
@@ -3124,72 +3231,146 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3124
3231
  return { error: `当前 Agent (${agent.name}) 不支持按轮次分叉`, code: 'NOT_SUPPORTED' };
3125
3232
  }
3126
3233
  const sourceKey = this.getQueueKey(source, channel, channelId);
3127
- const isBusy = !!source.processingState
3234
+ const sourceIsBusy = (current, currentAgent) => !!current.processingState
3235
+ || !!current.metadata?.turnState?.active
3128
3236
  || this.messageQueue.isProcessing?.(sourceKey)
3129
- || this.messageQueue.getQueueLength?.(sourceKey) > 0
3130
- || agent.hasActiveStream(sourceKey);
3131
- if (isBusy)
3237
+ || (this.messageQueue.getQueueLength?.(sourceKey) ?? 0) > 0
3238
+ || currentAgent.hasActiveStream(sourceKey)
3239
+ || (this.processor?.isPauseRequested?.(sourceKey) ?? false);
3240
+ if (!source.threadId && sourceIsBusy(source, agent)) {
3132
3241
  return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
3242
+ }
3133
3243
  const targetKey = JSON.stringify([source.channelType || source.channel, source.selfAID || '', channelId, targetThreadId]);
3134
3244
  if (topicForkTargetsInFlight.has(targetKey))
3135
3245
  return { error: '目标话题正在创建', code: 'CONFLICT' };
3136
3246
  topicForkTargetsInFlight.add(targetKey);
3137
- const releaseLock = this.messageQueue.acquireLock?.(sourceKey) ?? (() => { });
3138
3247
  try {
3139
- if (this.messageQueue.isProcessing?.(sourceKey) || agent.hasActiveStream(sourceKey)) {
3140
- return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
3141
- }
3142
- const messages = await agent.getSessionMessages(source.agentSessionId, source.projectPath);
3143
- const turns = buildSessionTurnList(messages);
3144
- const forkTurn = turns.find((turn) => turn.assistantUuid === sourceAssistantMessageId);
3145
- if (!forkTurn)
3146
- return { error: '分叉点已失效,请刷新历史后重试', code: 'NOT_FOUND' };
3147
- const forkedAgentSessionId = await agent.forkSessionAt(source.agentSessionId, source.projectPath, sourceAssistantMessageId, name || undefined);
3148
- const topic = await this.sessionManager.createForkedThreadSession(source, forkedAgentSessionId, targetThreadId, {
3149
- name: name || undefined,
3150
- creatorPeerId: userId,
3151
- sourceAssistantMessageId,
3152
- sourceTurn: forkTurn.index,
3153
- requestId,
3154
- });
3155
- await agent.updateSessionMetadata?.(forkedAgentSessionId, {
3156
- evolcoreSessionId: topic.id,
3157
- sourceSessionId: source.id,
3158
- sourceAssistantMessageId,
3159
- sourceTurn: forkTurn.index,
3160
- }).catch((error) => {
3161
- logger.debug(`[MenuHandler] Topic fork metadata sync failed: ${error}`);
3162
- });
3163
- this.eventBus.publish({
3164
- type: 'session:forked',
3165
- sessionId: topic.id,
3166
- sourceSessionId: source.id,
3167
- name: topic.name,
3168
- threadId: topic.threadId,
3169
- sourceAssistantMessageId,
3170
- sourceTurn: forkTurn.index,
3171
- });
3172
- return {
3173
- data: {
3174
- action: 'fork',
3175
- success: true,
3176
- topic: {
3177
- ...buildSessionPayload(topic, topic.name || ''),
3178
- threadId: topic.threadId,
3179
- sourceSessionId: source.id,
3180
- sourceThreadId: source.threadId || null,
3181
- sourceAssistantMessageId,
3182
- sourceTurn: forkTurn.index,
3248
+ const executeFork = async (current, currentAgent) => {
3249
+ const messages = await currentAgent.getSessionMessages(current.agentSessionId, current.projectPath);
3250
+ const turns = buildSessionTurnList(messages);
3251
+ const forkTurn = turns.find((turn) => turn.assistantUuid === sourceAssistantMessageId);
3252
+ if (!forkTurn)
3253
+ return { error: '分叉点已失效,请刷新历史后重试', code: 'NOT_FOUND' };
3254
+ const forkedAgentSessionId = await currentAgent.forkSessionAt(current.agentSessionId, current.projectPath, sourceAssistantMessageId, name || undefined);
3255
+ const topic = await this.sessionManager.createForkedThreadSession(current, forkedAgentSessionId, targetThreadId, {
3256
+ name: name || undefined,
3257
+ creatorPeerId: userId,
3258
+ sourceAssistantMessageId,
3259
+ sourceTurn: forkTurn.index,
3260
+ requestId,
3261
+ });
3262
+ await currentAgent.updateSessionMetadata?.(forkedAgentSessionId, {
3263
+ evolcoreSessionId: topic.id,
3264
+ sourceSessionId: current.id,
3265
+ sourceAssistantMessageId,
3266
+ sourceTurn: forkTurn.index,
3267
+ }).catch((error) => {
3268
+ logger.debug(`[MenuHandler] Topic fork metadata sync failed: ${error}`);
3269
+ });
3270
+ this.eventBus.publish({
3271
+ type: 'session:forked',
3272
+ sessionId: topic.id,
3273
+ sourceSessionId: current.id,
3274
+ name: topic.name,
3275
+ threadId: topic.threadId,
3276
+ sourceAssistantMessageId,
3277
+ sourceTurn: forkTurn.index,
3278
+ });
3279
+ return {
3280
+ data: {
3281
+ action: 'fork',
3282
+ success: true,
3283
+ topic: {
3284
+ ...buildSessionPayload(topic, topic.name || ''),
3285
+ threadId: topic.threadId,
3286
+ sourceSessionId: current.id,
3287
+ sourceThreadId: current.threadId || null,
3288
+ sourceAssistantMessageId,
3289
+ sourceTurn: forkTurn.index,
3290
+ },
3183
3291
  },
3184
- },
3292
+ };
3185
3293
  };
3294
+ const runForkUnderBarrier = async () => {
3295
+ if (source.threadId) {
3296
+ if (typeof this.messageQueue.withSessionBarrier !== 'function') {
3297
+ return { error: '当前运行环境不支持安全话题分叉', code: 'NOT_SUPPORTED' };
3298
+ }
3299
+ return this.messageQueue.withSessionBarrier(sourceKey, async (snapshot) => {
3300
+ const latest = await this.sessionManager.getSessionById?.(source.id);
3301
+ const current = latest;
3302
+ if (!current) {
3303
+ return { error: '来源会话不存在或归属已变化', code: 'NOT_FOUND' };
3304
+ }
3305
+ if (current.id !== source.id || (current.threadId || '') !== (source.threadId || '')) {
3306
+ return { error: '来源会话不存在或归属已变化', code: 'NOT_FOUND' };
3307
+ }
3308
+ if (!current.agentSessionId) {
3309
+ return { error: '来源会话暂无对话历史', code: 'INVALID_STATE' };
3310
+ }
3311
+ const currentAgent = this.getAgent(channel, current.baseagent);
3312
+ if (!currentAgent.capabilities?.forkAtTurn || !currentAgent.getSessionMessages || !currentAgent.forkSessionAt) {
3313
+ return { error: `当前 Agent (${currentAgent.name}) 不支持按轮次分叉`, code: 'NOT_SUPPORTED' };
3314
+ }
3315
+ const probes = {
3316
+ paused: () => this.processor?.isPauseRequested?.(current.id) ?? false,
3317
+ activeStream: () => currentAgent.hasActiveStream(current.id),
3318
+ };
3319
+ if (this.permissionGateway) {
3320
+ probes.pendingPermission = () => this.permissionGateway.getPendingRequests(current.id).length > 0;
3321
+ }
3322
+ if (this.interactionRouter) {
3323
+ probes.pendingInteraction = () => this.interactionRouter.getPending(current.id).length > 0;
3324
+ }
3325
+ const selfAid = current.selfAID || this.getOwningAgent?.(channel)?.aid;
3326
+ if (this.handoffRuntime) {
3327
+ probes.activeHandoff = () => {
3328
+ if (!selfAid)
3329
+ throw new Error('handoff activity owner unavailable');
3330
+ return this.handoffRuntime.hasActiveSessionWork(selfAid, current.id);
3331
+ };
3332
+ probes.openHandoffTarget = () => {
3333
+ if (!selfAid)
3334
+ throw new Error('handoff activity owner unavailable');
3335
+ return this.handoffRuntime.hasOpenTarget(selfAid, current.id);
3336
+ };
3337
+ }
3338
+ const activity = probeSessionActivity({
3339
+ prequeue: snapshot.prequeue,
3340
+ active: snapshot.active,
3341
+ queued: snapshot.queued,
3342
+ dispatching: snapshot.dispatching,
3343
+ processing: !!current.processingState,
3344
+ activeTurn: !!current.metadata?.turnState?.active,
3345
+ }, probes);
3346
+ if (activity === 'busy') {
3347
+ return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
3348
+ }
3349
+ if (activity === 'unknown') {
3350
+ return { error: '无法可靠确认来源会话为空闲,请稍后重试', code: 'INVALID_STATE' };
3351
+ }
3352
+ return executeFork(current, currentAgent);
3353
+ });
3354
+ }
3355
+ // Main-session sources keep compatibility with older embeddings.
3356
+ const release = this.messageQueue.acquireLock?.(sourceKey) ?? (() => { });
3357
+ try {
3358
+ if (sourceIsBusy(source, agent)) {
3359
+ return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
3360
+ }
3361
+ return await executeFork(source, agent);
3362
+ }
3363
+ finally {
3364
+ release();
3365
+ }
3366
+ };
3367
+ return await runForkUnderBarrier();
3186
3368
  }
3187
3369
  catch (error) {
3188
3370
  logger.error('[MenuHandler] Topic history fork failed:', error);
3189
3371
  return { error: error instanceof Error ? error.message : '话题分叉失败', code: 'EXEC_FAILED' };
3190
3372
  }
3191
3373
  finally {
3192
- releaseLock();
3193
3374
  topicForkTargetsInFlight.delete(targetKey);
3194
3375
  }
3195
3376
  }
@@ -3383,6 +3564,14 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3383
3564
  : 'AUN_OUTBOUND_ROUTE_REQUIRED',
3384
3565
  };
3385
3566
  }
3567
+ const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
3568
+ if (migrationRequirement.required) {
3569
+ return {
3570
+ error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3571
+ code: 'DATA_MIGRATION_REQUIRED',
3572
+ data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
3573
+ };
3574
+ }
3386
3575
  if (!suppressRealRestart) {
3387
3576
  const restartInfo = {
3388
3577
  channel,
@@ -3694,6 +3883,7 @@ async function execMenuForSystemControl(payload, context) {
3694
3883
  conversationId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
3695
3884
  processOwners: context.owners,
3696
3885
  fromControlChannel: true,
3886
+ canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
3697
3887
  });
3698
3888
  const trustedIdentity = trustedSubject.identity;
3699
3889
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
@@ -3820,20 +4010,37 @@ export async function execMenuForEcweb(payload, trusted) {
3820
4010
  data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
3821
4011
  });
3822
4012
  }
3823
- if (!trusted || (!trusted.localDirect && !trusted.actorAid)) {
4013
+ if (!trusted || (!trusted.localDirect && trusted.authenticated !== true)) {
4014
+ return menuFailure({ id, ...(name ? { name } : {}) }, {
4015
+ code: 'PERMISSION_DENIED',
4016
+ message: 'ECWeb operation requires an authenticated ECWeb connection',
4017
+ data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
4018
+ });
4019
+ }
4020
+ // A verified ECWeb connection has daemon-owner management authority. If the
4021
+ // bearer token has no explicit owner provenance (the normal pairing path),
4022
+ // use the first currently configured owner as the canonical management
4023
+ // principal. This keeps audit/Trigger approval attribution deterministic
4024
+ // without requiring owner-bound pairing metadata.
4025
+ const userId = (trusted.actorAid && owners.includes(trusted.actorAid))
4026
+ ? trusted.actorAid
4027
+ : owners[0] ?? (trusted.localDirect ? 'local-direct' : undefined);
4028
+ if (!userId) {
3824
4029
  return menuFailure({ id, ...(name ? { name } : {}) }, {
3825
4030
  code: 'PERMISSION_DENIED',
3826
- message: 'ECWeb operation requires a trusted local connection or authenticated actor',
4031
+ message: 'ECWeb operation requires configured daemon owners',
3827
4032
  data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
3828
4033
  });
3829
4034
  }
3830
- // A verified local ECWeb connection acts through the configured human
3831
- // DaemonOwner identity; it is not an anonymous daemon service principal.
3832
- const userId = trusted.actorAid ?? (trusted.localDirect ? (owners[0] || 'local-direct') : undefined);
3833
4035
  return execMenuForSystemControl.call(this, payload, {
3834
4036
  source: 'ecweb',
3835
- actorAid: userId || 'local-direct',
4037
+ actorAid: userId,
3836
4038
  localDirect: trusted.localDirect,
4039
+ // The ECWeb server/IPC control-token gate authenticates this management
4040
+ // request. All authenticated ECWeb connections are equally eligible to
4041
+ // approve persistent fullaccess; actorAid is used only when it identifies
4042
+ // a current configured owner.
4043
+ canApprovePersistentFullAccess: true,
3837
4044
  owners,
3838
4045
  });
3839
4046
  }
@@ -3861,6 +4068,7 @@ export async function execMenuForControl(payload, peerId) {
3861
4068
  source: 'control',
3862
4069
  actorAid: peerId,
3863
4070
  localDirect: false,
4071
+ canApprovePersistentFullAccess: false,
3864
4072
  owners,
3865
4073
  });
3866
4074
  }
@@ -272,7 +272,7 @@ const STABLE_CODES = new Set([
272
272
  'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
273
273
  'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
274
274
  'INVALID_CONTEXT', 'SCHEMA_VERSION_UNSUPPORTED', 'DEPENDENCY_UNAVAILABLE',
275
- 'RATE_LIMITED', 'CATALOG_STALE',
275
+ 'RATE_LIMITED', 'CATALOG_STALE', 'DATA_MIGRATION_REQUIRED',
276
276
  ]);
277
277
  export function normalizeMenuError(error) {
278
278
  const source = error && typeof error === 'object' ? error : {};