evolcore 0.0.20 → 0.0.21

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