evolcore 0.0.25 → 0.0.27

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 (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { getBaseagentSnapshot, loadedBaseagentIds } from '../../agents/baseagent-availability.js';
1
2
  import { normalizePeer, writeScope } from '../model/config-scope.js';
2
3
  import { formatPeerKey } from '../relation/peer-identity.js';
3
4
  import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
@@ -13,7 +14,7 @@ import crypto from 'crypto';
13
14
  import { CronExpressionParser } from 'cron-parser';
14
15
  import { parseDuration } from '../../trigger/parser.js';
15
16
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
16
- import { commandExists, resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
17
+ import { resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
17
18
  import { cachedCliVersion } from '../../utils/cross-platform.js';
18
19
  import { scanInstances } from '../../utils/instance-registry.js';
19
20
  import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
@@ -29,6 +30,7 @@ import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../au
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';
33
+ import { isUnsupportedBaseagentConfigField } from '../../config/config-field-policy.js';
32
34
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
33
35
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
34
36
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
@@ -40,13 +42,16 @@ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
40
42
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
41
43
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
42
44
  import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
43
- import { validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
45
+ import { remoteAccessService, validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
44
46
  import { listDiagnosticLogOptions, listRawConfigResources, queryDiagnosticLogs, readRawConfigResource, } from './daemon-diagnostics.js';
45
47
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
46
48
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
47
49
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
48
50
  import { getMenuCatalog, execMenuValueBatch, resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from './menu-catalog.js';
49
51
  import { probeSessionActivity } from './slash-gate.js';
52
+ import { dataRootFailure, dataRootResolver } from './data-root-resolver.js';
53
+ import { agentStorageMenuService, isAgentStorageMenuRequest } from './agent-storage-menu.js';
54
+ import { isArchivedTopicLable, normalizeTopicLable, resolveTopicArchiveFilter, } from '../session/session-mapper.js';
50
55
  /**
51
56
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
52
57
  * 失败返回 null(命令不存在或执行失败)。
@@ -180,6 +185,8 @@ function getRenameName(args) {
180
185
  }
181
186
  function buildSessionPayload(session, name) {
182
187
  const payload = { id: session.id, name };
188
+ if (session.lable)
189
+ payload.lable = session.lable;
183
190
  if (session.agentSessionId)
184
191
  payload.agentSessionId = session.agentSessionId;
185
192
  return payload;
@@ -413,7 +420,10 @@ function hostCatalogArgs(args = {}) {
413
420
  }
414
421
  if (args.refresh !== undefined && typeof args.refresh !== 'boolean')
415
422
  invalid('refresh must be a boolean');
416
- return { baseagent: menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba') ?? 'claude', refresh: args.refresh === true };
423
+ const baseagent = menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba');
424
+ if (!baseagent)
425
+ return invalid('baseagent is required (claude / codex / ecagent / gemini)');
426
+ return { baseagent, refresh: args.refresh === true };
417
427
  }
418
428
  const LEGACY_SESSION_CONFIG_ARG_KEYS = [
419
429
  'session',
@@ -851,7 +861,7 @@ export function isProcessLevelOwner(peerId, owners) {
851
861
  // ── 控制面双轨鉴权(见 docs/.../2026-06-10-control-channel-auth-design.md)──
852
862
  // 白名单而非黑名单:默认安全,新增进程级 action 需显式加入。
853
863
  /** /agent 的进程级 action:仅控制 channel 可执行。 */
854
- export const PROCESS_LEVEL_AGENT_ACTIONS = new Set(['create', 'delete', 'enable', 'disable']);
864
+ export const PROCESS_LEVEL_AGENT_ACTIONS = new Set(['create', 'initialize', 'delete', 'enable', 'disable']);
855
865
  /** /agent 的「本 agent 自管理」action:agent channel 仅 owner/admin 可对自身 aid 执行;update 另行收紧为 owner。 */
856
866
  export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
857
867
  /** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
@@ -898,6 +908,10 @@ function gateControlScope(opts) {
898
908
  return null;
899
909
  }
900
910
  function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel = false) {
911
+ if (cmdBase === '/agent-data-root' || cmdBase === '/relation-data-root')
912
+ return null;
913
+ if (cmdBase === '/file' && args?.rootId !== undefined)
914
+ return null;
901
915
  const payload = { ...(args ?? {}), ...(value !== undefined ? { value } : {}) };
902
916
  const intent = (operation, scope = 'relation', extra) => ({
903
917
  operation,
@@ -909,6 +923,9 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
909
923
  if (verb === 'query') {
910
924
  if (cmdBase === '/baseagent-models')
911
925
  return intent('baseagent.models.list', 'process');
926
+ if (cmdBase === '/baseagents') {
927
+ return intent(fromControlChannel ? 'system.status' : 'agent.baseagent.list', fromControlChannel ? 'process' : 'agent');
928
+ }
912
929
  if (cmdBase === '/pwd')
913
930
  return intent('project.current', 'relation');
914
931
  if (cmdBase === '/session' || cmdBase === '/s')
@@ -1015,12 +1032,19 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1015
1032
  return intent('system.upgrade', 'process', { action });
1016
1033
  if (action === 'check')
1017
1034
  return intent('system.status', 'process', { action });
1035
+ if (action === 'paircode')
1036
+ return intent('system.paircode', 'process', { action });
1018
1037
  }
1019
1038
  if (cmdBase === '/agent') {
1039
+ if (action === 'update' && args?.patch && Object.hasOwn(args.patch, 'avatar')) {
1040
+ return intent('ec.aid.avatar', 'agent', { action });
1041
+ }
1020
1042
  if (action === 'reload')
1021
1043
  return intent('agent.reload', fromControlChannel ? 'control' : 'agent', { action });
1022
1044
  if (action === 'create')
1023
1045
  return intent('agent.create', 'control', { action });
1046
+ if (action === 'initialize')
1047
+ return intent('agent.initialize', 'control', { action });
1024
1048
  if (action === 'delete')
1025
1049
  return intent('agent.delete', 'control', { action });
1026
1050
  if (action === 'enable')
@@ -1061,6 +1085,7 @@ function buildMenuAuthSubject(owner, params) {
1061
1085
  if (params.subject) {
1062
1086
  return {
1063
1087
  ...params.subject,
1088
+ fromControlChannel: params.fromControlChannel || params.subject.fromControlChannel,
1064
1089
  ...(params.requestId && !params.subject.requestId ? { requestId: params.requestId } : {}),
1065
1090
  ...(params.session?.id && !params.subject.sessionId ? { sessionId: params.session.id } : {}),
1066
1091
  };
@@ -1509,9 +1534,27 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1509
1534
  });
1510
1535
  return items;
1511
1536
  }
1537
+ /** All Menu reads and writes must resolve the same transport-scoped topic. */
1538
+ async function loadMenuTopicSession(sessionManager, channel, channelId, threadId) {
1539
+ let session;
1540
+ try {
1541
+ if (typeof sessionManager.getThreadSession !== 'function')
1542
+ throw new Error('topic lookup unavailable');
1543
+ session = await sessionManager.getThreadSession(channel, channelId, threadId);
1544
+ }
1545
+ catch (error) {
1546
+ logger.debug(`[MenuHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
1547
+ throw { code: 'TOPIC_SESSION_LOOKUP_FAILED', message: '无法读取话题会话,拒绝回退到主会话' };
1548
+ }
1549
+ if (!session)
1550
+ throw { code: 'TOPIC_SESSION_NOT_FOUND', message: '话题会话不存在,拒绝回退到主会话' };
1551
+ return session;
1552
+ }
1512
1553
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
1513
- export async function getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
1514
- const session = await this.sessionManager.getActiveSession(channel, channelId);
1554
+ export async function getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext) {
1555
+ const session = transportReplyContext?.threadId
1556
+ ? await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId)
1557
+ : await this.sessionManager.getActiveSession(channel, channelId);
1515
1558
  const subject = buildMenuAuthSubject(this, {
1516
1559
  identity: overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session)),
1517
1560
  session,
@@ -1615,19 +1658,23 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1615
1658
  : await execAgentOptions(args);
1616
1659
  if ('error' in res)
1617
1660
  throw { code: res.code, message: res.error };
1618
- return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
1661
+ return res.data.agents.map(ag => ({
1662
+ value: ag.aid, label: ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1663
+ }));
1619
1664
  }
1620
1665
  // agent channel:直接读取自身,避免先枚举全部 Agent 再过滤。
1621
1666
  const selfAid = this.getOwningAgent?.(channel)?.aid;
1622
1667
  if (!selfAid)
1623
1668
  throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1624
1669
  const res = this.agentApplicationService
1625
- ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService)
1626
- : await execAgentQuery({ aid: selfAid });
1670
+ ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, subject)
1671
+ : await execAgentQuery({ aid: selfAid }, undefined, subject);
1627
1672
  if ('error' in res)
1628
1673
  throw { code: res.code, message: res.error };
1629
1674
  const ag = res.data;
1630
- return [{ value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status }];
1675
+ return [{
1676
+ value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1677
+ }];
1631
1678
  }
1632
1679
  if (cmd === '/capability') {
1633
1680
  const type = args?.type;
@@ -1683,12 +1730,33 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1683
1730
  preview: turn.userContent,
1684
1731
  turn: turn.index,
1685
1732
  assistantMessageId: turn.assistantUuid,
1733
+ assistantMessageIds: turn.assistantUuids,
1686
1734
  userMessageId: turn.userUuid,
1735
+ userTextHash: turn.userTextHash,
1736
+ assistantTextHashes: turn.assistantTextHashes,
1687
1737
  }));
1688
1738
  }
1689
- const sessions = await this.sessionManager.listSessions(channel, channelId);
1739
+ const hasLable = args && (Object.hasOwn(args, 'lable') || Object.hasOwn(args, 'label'));
1740
+ let lable;
1741
+ if (hasLable) {
1742
+ try {
1743
+ lable = normalizeTopicLable(Object.hasOwn(args, 'lable') ? args.lable : args.label);
1744
+ }
1745
+ catch (error) {
1746
+ throw { code: 'INVALID_ARGS', message: error.message };
1747
+ }
1748
+ }
1749
+ let archiveFilter;
1750
+ try {
1751
+ const archived = args?.archived === undefined && isArchivedTopicLable(lable) ? true : args?.archived;
1752
+ archiveFilter = resolveTopicArchiveFilter(archived);
1753
+ }
1754
+ catch (error) {
1755
+ throw { code: 'INVALID_ARGS', message: error.message };
1756
+ }
1757
+ const sessions = await this.sessionManager.listSessions(channel, channelId, { archiveFilter });
1690
1758
  return sessions
1691
- .filter((s) => !!s.threadId)
1759
+ .filter((s) => !!s.threadId && (!hasLable || s.lable === lable))
1692
1760
  .map((s) => this.buildTopicMenuItem(s));
1693
1761
  }
1694
1762
  if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
@@ -1733,7 +1801,12 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1733
1801
  const available = requestedAgent?.name
1734
1802
  ? (this.getAvailableBaseagentsForOwner?.(requestedAgent.name) ?? [])
1735
1803
  : this.getAvailableBaseagents(channel);
1736
- return available.map((name) => ({ value: name, label: name, selected: name === currentAgent }));
1804
+ const inventory = await getBaseagentSnapshot({
1805
+ config: { active_baseagent: currentAgent }, loaded: available, probe: false,
1806
+ });
1807
+ return inventory.baseagents
1808
+ .filter(item => item.loaded === true)
1809
+ .map(item => ({ value: item.id, label: item.id, selected: item.id === currentAgent }));
1737
1810
  }
1738
1811
  if (cmd === '/model') {
1739
1812
  const target = resolveMenuModelTarget.call(this, {
@@ -1767,7 +1840,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1767
1840
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1768
1841
  const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1769
1842
  if (models.length > 0)
1770
- return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '') + (catalog.freshness === 'stale' ? ' [stale]' : ''), selected: modelMatches(agent, m, currentModel) }));
1843
+ return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : ''), selected: modelMatches(agent, m, currentModel) }));
1771
1844
  }
1772
1845
  return null;
1773
1846
  }
@@ -1892,7 +1965,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1892
1965
  // 所有方法返回 { data } 或 { error, code? }。code 是结构化错误码(NO_ACTIVE_SESSION 等),
1893
1966
  // 客户端可据此决定降级策略。message-bridge 把 code 透传到 menu.response。
1894
1967
  /** menu.query — 查询当前值。 */
1895
- export async function execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu') {
1968
+ export async function execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu', transportReplyContext) {
1896
1969
  const cmdBase = cmd.trim().split(' ')[0];
1897
1970
  if (!cmdBase)
1898
1971
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -1903,7 +1976,17 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1903
1976
  const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
1904
1977
  if (gated)
1905
1978
  return gated;
1906
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
1979
+ const loadedContext = await this.loadMenuContext(channel, channelId);
1980
+ const evolagent = loadedContext.evolagent;
1981
+ let session = loadedContext.session;
1982
+ if (transportReplyContext?.threadId) {
1983
+ try {
1984
+ session = await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId);
1985
+ }
1986
+ catch (error) {
1987
+ return menuResultFailure(error);
1988
+ }
1989
+ }
1907
1990
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1908
1991
  const subject = buildMenuAuthSubject(this, {
1909
1992
  identity,
@@ -1915,6 +1998,15 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1915
1998
  fromControlChannel,
1916
1999
  subject: authSubject,
1917
2000
  });
2001
+ const storageRequest = { type: 'menu.query', cmd, args };
2002
+ if (isAgentStorageMenuRequest(storageRequest)) {
2003
+ try {
2004
+ return { data: await agentStorageMenuService.execute(storageRequest, subject, this.agentStorageClientResolver) };
2005
+ }
2006
+ catch (error) {
2007
+ return dataRootFailure(error);
2008
+ }
2009
+ }
1918
2010
  if (isRequesterRoleQuery) {
1919
2011
  if (!subject.allowAccess) {
1920
2012
  return { error: `Role ${subject.role} is not allowed to access this agent`, code: 'ROLE_ACCESS_DENIED' };
@@ -1983,6 +2075,28 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1983
2075
  });
1984
2076
  if (authDenied)
1985
2077
  return authDenied;
2078
+ if (cmdBase === '/agent-data-root') {
2079
+ if (args && Object.keys(args).length > 0) {
2080
+ return { error: 'agent-data-root does not accept args', code: 'INVALID_ARGUMENT' };
2081
+ }
2082
+ try {
2083
+ return { data: dataRootResolver.issueAgentRoot(subject) };
2084
+ }
2085
+ catch (error) {
2086
+ return dataRootFailure(error);
2087
+ }
2088
+ }
2089
+ if (cmdBase === '/relation-data-root') {
2090
+ if (args && Object.keys(args).some(key => key !== 'peerAid')) {
2091
+ return { error: 'relation-data-root only accepts args.peerAid', code: 'INVALID_ARGUMENT' };
2092
+ }
2093
+ try {
2094
+ return { data: dataRootResolver.issueRelationRoot(subject, args?.peerAid) };
2095
+ }
2096
+ catch (error) {
2097
+ return dataRootFailure(error);
2098
+ }
2099
+ }
1986
2100
  if (cmdBase === '/baseagent-models') {
1987
2101
  try {
1988
2102
  const query = hostCatalogArgs(args);
@@ -2001,8 +2115,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2001
2115
  if (fromControlChannel) {
2002
2116
  if (args?.aid) {
2003
2117
  return this.agentApplicationService
2004
- ? await execAgentQuery(args, this.agentApplicationService)
2005
- : await execAgentQuery(args);
2118
+ ? await execAgentQuery(args, this.agentApplicationService, subject)
2119
+ : await execAgentQuery(args, undefined, subject);
2006
2120
  }
2007
2121
  return this.agentApplicationService
2008
2122
  ? await execAgentOptions(args, this.agentApplicationService)
@@ -2012,8 +2126,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2012
2126
  if (!selfAid)
2013
2127
  return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2014
2128
  return this.agentApplicationService
2015
- ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService)
2016
- : await execAgentQuery({ ...(args ?? {}), aid: selfAid });
2129
+ ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, subject)
2130
+ : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, subject);
2017
2131
  }
2018
2132
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
2019
2133
  if (cmdBase === '/gateway') {
@@ -2093,6 +2207,9 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2093
2207
  if (!session) {
2094
2208
  return { data: { status: 'no-session' } };
2095
2209
  }
2210
+ if (args?.view === 'subtasks') {
2211
+ return { data: { scope: 'retained-history', tasks: this.sessionManager.getRunnerTaskHistory(session.id) } };
2212
+ }
2096
2213
  const sessionKey = this.getQueueKey(session, channel, channelId);
2097
2214
  const sessionAgent = this.getAgent(channel, session.baseagent);
2098
2215
  const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
@@ -2115,6 +2232,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2115
2232
  status: isProcessing ? 'processing' : 'idle',
2116
2233
  createdAt: session.createdAt,
2117
2234
  updatedAt: session.updatedAt,
2235
+ subtasks: this.sessionManager.getRunnerTaskHistory(session.id).length,
2236
+ subtasksScope: 'retained-history',
2118
2237
  };
2119
2238
  if (processingDuration !== undefined)
2120
2239
  data.processingDuration = processingDuration;
@@ -2130,6 +2249,21 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2130
2249
  data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
2131
2250
  return { data };
2132
2251
  }
2252
+ if (cmdBase === '/baseagents') {
2253
+ if (args && Object.keys(args).length > 0) {
2254
+ return { error: 'baseagents does not accept query arguments', code: 'INVALID_ARGUMENT' };
2255
+ }
2256
+ if (fromControlChannel) {
2257
+ return { data: await getBaseagentSnapshot({ loaded: loadedBaseagentIds(this.agentMap.keys()) }) };
2258
+ }
2259
+ if (!evolagent)
2260
+ return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2261
+ return { data: await getBaseagentSnapshot({
2262
+ config: evolagent.config ?? {},
2263
+ loaded: this.getAvailableBaseagents(channel),
2264
+ probe: false,
2265
+ }) };
2266
+ }
2133
2267
  if (cmdBase === '/topic') {
2134
2268
  const target = (args?.target ?? '').toString().trim();
2135
2269
  if (!target)
@@ -2137,6 +2271,9 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2137
2271
  const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
2138
2272
  if (!topic)
2139
2273
  return { error: '话题不存在', code: 'NOT_FOUND' };
2274
+ if (args?.view === 'subtasks') {
2275
+ return { data: { scope: 'retained-history', tasks: this.sessionManager.getRunnerTaskHistory(topic.id) } };
2276
+ }
2140
2277
  const sessionKey = this.getQueueKey(topic, channel, channelId);
2141
2278
  const sessionAgent = this.getAgent(channel, topic.baseagent);
2142
2279
  const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
@@ -2155,10 +2292,13 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2155
2292
  const data = {
2156
2293
  threadId: topic.threadId,
2157
2294
  name: topic.name || null,
2295
+ lable: topic.lable || null,
2158
2296
  agentSessionId: topic.agentSessionId || null,
2159
2297
  status: isProcessing ? 'processing' : 'idle',
2160
2298
  createdAt: topic.createdAt,
2161
2299
  updatedAt: topic.updatedAt,
2300
+ subtasks: this.sessionManager.getRunnerTaskHistory(topic.id).length,
2301
+ subtasksScope: 'retained-history',
2162
2302
  };
2163
2303
  if (processingDuration !== undefined)
2164
2304
  data.processingDuration = processingDuration;
@@ -2372,6 +2512,24 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2372
2512
  }
2373
2513
  if (cmdBase === '/system') {
2374
2514
  const view = args?.view ?? 'status';
2515
+ if (view === 'version') {
2516
+ const data = {};
2517
+ try {
2518
+ const pkgPath = path.join(getPackageRoot(), 'package.json');
2519
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
2520
+ if (pkg?.version)
2521
+ data.version = pkg.version;
2522
+ }
2523
+ catch { }
2524
+ try {
2525
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
2526
+ const fp2 = JSON.parse(fs.readFileSync(fp, 'utf-8'));
2527
+ if (fp2?.version)
2528
+ data.fastaunVersion = fp2.version;
2529
+ }
2530
+ catch { }
2531
+ return { data };
2532
+ }
2375
2533
  if (view === 'monitor') {
2376
2534
  if (typeof this.daemonMonitorProvider !== 'function') {
2377
2535
  return { error: 'Daemon monitor snapshot is unavailable', code: 'DEPENDENCY_UNAVAILABLE' };
@@ -2416,24 +2574,41 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2416
2574
  if (ecwebPkg?.version)
2417
2575
  data.ecwebVersion = ecwebPkg.version;
2418
2576
  // 收集主机级可用的所有 baseagent 类型(检测已安装的 CLI + 版本)
2419
- const baseagents = [];
2420
- for (const cmd of ['claude', 'gemini', 'codex']) {
2421
- if (commandExists(cmd)) {
2422
- baseagents.push({ name: cmd, version: getBaseagentVersion(cmd) });
2423
- }
2424
- }
2425
- data.baseagents = baseagents;
2577
+ const inventory = await getBaseagentSnapshot({
2578
+ loaded: loadedBaseagentIds(this.agentMap.keys()),
2579
+ });
2580
+ data.baseagents = inventory.baseagents.map(item => ({
2581
+ ...item, name: item.id,
2582
+ version: item.version ?? (item.detected && item.id !== 'ecagent' ? getBaseagentVersion(item.id) : null),
2583
+ }));
2426
2584
  return { data };
2427
2585
  }
2428
2586
  // ── name=file:文件元信息(§5.1) ──
2429
2587
  // operation 权限已在入口统一判定;项目外文件仍仅 owner 可取。
2430
2588
  if (cmdBase === '/file') {
2431
- const role = identity.role;
2432
- const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
2433
- const resolved = resolveMenuFilePath(args?.path, projectPath, role, 'file');
2434
- if ('error' in resolved)
2435
- return resolved;
2436
- const { realPath, stat } = resolved;
2589
+ let realPath;
2590
+ let stat;
2591
+ let responsePath = (args?.path ?? '').toString();
2592
+ let dataRoot;
2593
+ if (args?.rootId !== undefined) {
2594
+ try {
2595
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, args?.path, 'file');
2596
+ ({ realPath, stat } = resolved);
2597
+ responsePath = resolved.relativePath;
2598
+ dataRoot = resolved;
2599
+ }
2600
+ catch (error) {
2601
+ return dataRootFailure(error);
2602
+ }
2603
+ }
2604
+ else {
2605
+ const role = identity.role;
2606
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
2607
+ const resolved = resolveMenuFilePath(args?.path, projectPath, role, 'file');
2608
+ if ('error' in resolved)
2609
+ return resolved;
2610
+ ({ realPath, stat } = resolved);
2611
+ }
2437
2612
  // sha256 仅对 ≤ 2 MB 文件计算,超过返回 null,客户端降级到 size+mtime(§4、§7 决策 2)
2438
2613
  let sha256 = null;
2439
2614
  if (stat.size <= FILE_HASH_MAX_SIZE) {
@@ -2446,7 +2621,16 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2446
2621
  }
2447
2622
  return {
2448
2623
  data: {
2449
- path: (args?.path ?? '').toString(),
2624
+ path: responsePath,
2625
+ ...(dataRoot ? {
2626
+ root: {
2627
+ scope: dataRoot.scope,
2628
+ targetAid: dataRoot.targetAid,
2629
+ ...(dataRoot.peerAid ? { peerAid: dataRoot.peerAid } : {}),
2630
+ ...(dataRoot.peerKey ? { peerKey: dataRoot.peerKey } : {}),
2631
+ expiresAt: dataRoot.expiresAt,
2632
+ },
2633
+ } : {}),
2450
2634
  sha256,
2451
2635
  size: stat.size,
2452
2636
  mtime: stat.mtimeMs,
@@ -2475,17 +2659,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2475
2659
  const loadedContext = await this.loadMenuContext(channel, channelId);
2476
2660
  let session = loadedContext.session;
2477
2661
  const evolagent = loadedContext.evolagent;
2478
- if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
2662
+ if (transportReplyContext?.threadId) {
2479
2663
  try {
2480
- const topicSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId);
2481
- if (!topicSession) {
2482
- return { error: '话题会话不存在,拒绝回退到主会话', code: 'TOPIC_SESSION_NOT_FOUND' };
2483
- }
2484
- session = topicSession;
2664
+ session = await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId);
2485
2665
  }
2486
2666
  catch (error) {
2487
- logger.debug('[MenuHandler] topic session lookup failed for menu update: thread=' + transportReplyContext.threadId + ': ' + (error instanceof Error ? error.message : String(error)));
2488
- return { error: '无法读取话题会话,拒绝回退到主会话', code: 'TOPIC_SESSION_LOOKUP_FAILED' };
2667
+ return menuResultFailure(error);
2489
2668
  }
2490
2669
  }
2491
2670
  if (cmdBase === '/baseagent-models')
@@ -2764,7 +2943,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2764
2943
  if ('error' in target)
2765
2944
  return target;
2766
2945
  const agent = this.getAgent(channel, target.baseagent);
2767
- let targetModel = arg;
2946
+ const requestedModel = arg;
2768
2947
  if (hasModelSwitcher(agent)) {
2769
2948
  const catalog = await getCatalog(target.sel.self, target.baseagent);
2770
2949
  assertCatalogSelectable(catalog);
@@ -2772,20 +2951,22 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2772
2951
  const decision = validateModelSelectionForRole({
2773
2952
  role: identity.role,
2774
2953
  baseagent: target.baseagent,
2775
- requestedModel: arg,
2954
+ requestedModel,
2776
2955
  models,
2777
2956
  selfAid: target.sel.self,
2778
2957
  resolveModelId: model => resolveCatalogModel(catalog, model),
2779
2958
  });
2780
2959
  if (!decision.ok)
2781
2960
  return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
2782
- targetModel = decision.model || arg;
2783
- if (models.length && !models.includes(targetModel)) {
2961
+ // Validate the resolved ID, but persist/echo the selected value so aliases
2962
+ // such as `sonnet` keep following the catalog instead of pinning a version.
2963
+ const resolvedModel = decision.model || requestedModel;
2964
+ if (models.length && !models.includes(resolvedModel)) {
2784
2965
  return { error: `无效模型: ${arg}`, code: 'INVALID_VALUE' };
2785
2966
  }
2786
2967
  }
2787
2968
  try {
2788
- writeMenuModel(target, targetModel);
2969
+ writeMenuModel(target, requestedModel);
2789
2970
  }
2790
2971
  catch (e) {
2791
2972
  return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
@@ -2795,12 +2976,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2795
2976
  sessionId: session?.id,
2796
2977
  agentName: evolagent?.name,
2797
2978
  baseagent: target.baseagent,
2798
- model: targetModel,
2979
+ model: requestedModel,
2799
2980
  timestamp: Date.now(),
2800
2981
  });
2801
2982
  return {
2802
2983
  data: {
2803
- model: targetModel,
2984
+ model: requestedModel,
2804
2985
  baseagent: target.baseagent,
2805
2986
  scope: target.scope,
2806
2987
  field: target.fieldPath,
@@ -3024,6 +3205,15 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3024
3205
  fromControlChannel,
3025
3206
  subject: authSubject,
3026
3207
  });
3208
+ const storageRequest = { type: 'menu.action', cmd, action, args };
3209
+ if (isAgentStorageMenuRequest(storageRequest)) {
3210
+ try {
3211
+ return { data: await agentStorageMenuService.execute(storageRequest, subject, this.agentStorageClientResolver) };
3212
+ }
3213
+ catch (error) {
3214
+ return dataRootFailure(error);
3215
+ }
3216
+ }
3027
3217
  if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
3028
3218
  return {
3029
3219
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
@@ -3103,7 +3293,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3103
3293
  }
3104
3294
  const isMenuAdmin = isManagementRole(authIdentity.role);
3105
3295
  // ── /aid avatar ──
3106
- // The AID namespace is the only avatar mutation surface.
3296
+ // Canonical avatar action; legacy agent/update patch.avatar reuses the same uploader.
3107
3297
  if (cmdBase === '/aid') {
3108
3298
  if (action !== 'avatar')
3109
3299
  return { error: `不支持 aid action: ${action}`, code: 'NOT_SUPPORTED' };
@@ -3245,7 +3435,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3245
3435
  }
3246
3436
  }
3247
3437
  }
3248
- return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService);
3438
+ return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, subject);
3249
3439
  }
3250
3440
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
3251
3441
  if (cmdBase === '/trigger') {
@@ -3290,6 +3480,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3290
3480
  targetThreadId: args.targetThreadId,
3291
3481
  agentId: args.agentId,
3292
3482
  model: args.model,
3483
+ executionTimeoutMs: args.executionTimeoutMs,
3293
3484
  effort: args.effort,
3294
3485
  permissionMode: args.permissionMode,
3295
3486
  triggerThread,
@@ -3427,6 +3618,16 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3427
3618
  return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
3428
3619
  }
3429
3620
  if (cmdBase === '/topic') {
3621
+ const hasLable = args && (Object.hasOwn(args, 'lable') || Object.hasOwn(args, 'label'));
3622
+ let lable;
3623
+ if (hasLable && (action === 'fork' || action === 'label')) {
3624
+ try {
3625
+ lable = normalizeTopicLable(Object.hasOwn(args, 'lable') ? args.lable : args.label);
3626
+ }
3627
+ catch (error) {
3628
+ return { error: error.message, code: 'INVALID_ARGS' };
3629
+ }
3630
+ }
3430
3631
  if (action === 'fork') {
3431
3632
  if (!this.canReadTopics(identity.role))
3432
3633
  return { error: '无权限创建话题分支', code: 'FORBIDDEN' };
@@ -3503,21 +3704,24 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3503
3704
  const executeFork = async (current, currentAgent) => {
3504
3705
  const messages = await currentAgent.getSessionMessages(current.agentSessionId, current.projectPath);
3505
3706
  const turns = buildSessionTurnList(messages);
3506
- const forkTurn = turns.find((turn) => turn.assistantUuid === sourceAssistantMessageId);
3707
+ // fork-turns resolves bubble aliases to this canonical, complete-turn
3708
+ // anchor. Never fall back to a runner iteration or a guessed index.
3709
+ const forkTurn = turns.find(turn => turn.assistantUuid === sourceAssistantMessageId);
3507
3710
  if (!forkTurn)
3508
3711
  return { error: '分叉点已失效,请刷新历史后重试', code: 'NOT_FOUND' };
3509
- const forkedAgentSessionId = await currentAgent.forkSessionAt(current.agentSessionId, current.projectPath, sourceAssistantMessageId, name || undefined);
3712
+ const forkedAgentSessionId = await currentAgent.forkSessionAt(current.agentSessionId, current.projectPath, forkTurn.assistantUuid, name || undefined, { sessionId: current.id, permissionMode: subject.permissionMode, role: subject.role });
3510
3713
  const topic = await this.sessionManager.createForkedThreadSession(current, forkedAgentSessionId, targetThreadId, {
3511
3714
  name: name || undefined,
3715
+ ...(hasLable ? { lable: lable ?? null } : {}),
3512
3716
  creatorPeerId: userId,
3513
- sourceAssistantMessageId,
3717
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3514
3718
  sourceTurn: forkTurn.index,
3515
3719
  requestId,
3516
3720
  });
3517
3721
  await currentAgent.updateSessionMetadata?.(forkedAgentSessionId, {
3518
3722
  evolcoreSessionId: topic.id,
3519
3723
  sourceSessionId: current.id,
3520
- sourceAssistantMessageId,
3724
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3521
3725
  sourceTurn: forkTurn.index,
3522
3726
  }).catch((error) => {
3523
3727
  logger.debug(`[MenuHandler] Topic fork metadata sync failed: ${error}`);
@@ -3528,7 +3732,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3528
3732
  sourceSessionId: current.id,
3529
3733
  name: topic.name,
3530
3734
  threadId: topic.threadId,
3531
- sourceAssistantMessageId,
3735
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3532
3736
  sourceTurn: forkTurn.index,
3533
3737
  });
3534
3738
  return {
@@ -3540,7 +3744,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3540
3744
  threadId: topic.threadId,
3541
3745
  sourceSessionId: current.id,
3542
3746
  sourceThreadId: current.threadId || null,
3543
- sourceAssistantMessageId,
3747
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3544
3748
  sourceTurn: forkTurn.index,
3545
3749
  },
3546
3750
  },
@@ -3629,7 +3833,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3629
3833
  topicForkTargetsInFlight.delete(targetKey);
3630
3834
  }
3631
3835
  }
3632
- if (action !== 'delete' && action !== 'rename') {
3836
+ if (action !== 'delete' && action !== 'rename' && action !== 'label') {
3633
3837
  return { error: `不支持的 topic action: ${action}`, code: 'NOT_SUPPORTED' };
3634
3838
  }
3635
3839
  const target = (args?.target ?? '').toString().trim();
@@ -3638,6 +3842,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3638
3842
  const renameName = action === 'rename' ? getRenameName(args) : '';
3639
3843
  if (action === 'rename' && !renameName)
3640
3844
  return { error: '缺少 args.name', code: 'MISSING_VALUE' };
3845
+ if (action === 'label' && !hasLable)
3846
+ return { error: '缺少 args.lable', code: 'MISSING_VALUE' };
3641
3847
  const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
3642
3848
  if (!topic)
3643
3849
  return { error: '话题不存在', code: 'NOT_FOUND' };
@@ -3647,7 +3853,18 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3647
3853
  ? 'private'
3648
3854
  : this.resolveMenuChatType(channel, channelId, explicitChatType);
3649
3855
  if (!this.canDeleteTopic(identity.role, chatType, topic, userId)) {
3650
- return { error: action === 'rename' ? '无权限重命名话题' : '无权限删除话题', code: 'FORBIDDEN' };
3856
+ return { error: action === 'label' ? '无权限修改话题分类' : action === 'rename' ? '无权限重命名话题' : '无权限删除话题', code: 'FORBIDDEN' };
3857
+ }
3858
+ if (action === 'label') {
3859
+ const success = await this.sessionManager.setTopicLable(topic.id, lable ?? null);
3860
+ if (!success)
3861
+ return { error: '话题不存在', code: 'NOT_FOUND' };
3862
+ return {
3863
+ data: {
3864
+ action: 'label', success: true,
3865
+ topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: topic.threadId, lable: lable ?? null },
3866
+ },
3867
+ };
3651
3868
  }
3652
3869
  if (action === 'rename') {
3653
3870
  const newName = renameName;
@@ -3885,6 +4102,12 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3885
4102
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
3886
4103
  return { data: { action: 'restart', success: true } };
3887
4104
  }
4105
+ if (action === 'paircode') {
4106
+ // Keep Menu pairing on the same daemon-owned store as /pair, ECWeb, and
4107
+ // the local IPC endpoint. Each successful redemption consumes the code.
4108
+ const pair = remoteAccessService.getPairingCode(subject.actorId, Date.now(), source === 'ecweb' ? 'ecweb-local' : 'menu');
4109
+ return { data: { code: pair.code, expiresAt: pair.expiresAt } };
4110
+ }
3888
4111
  if (action === 'check') {
3889
4112
  const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3890
4113
  // delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
@@ -4016,6 +4239,15 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4016
4239
  },
4017
4240
  });
4018
4241
  if (!decision.allow) {
4242
+ // Keep unsupported runner fields in the aggregate so the CLI can
4243
+ // return a per-field error while still returning supported values.
4244
+ if (isBatchGet
4245
+ && decision.code === 'UNSUPPORTED_FIELD'
4246
+ && parsed.resolvedConfigCommand?.kind === 'field'
4247
+ && parsed.resolvedConfigCommand.subcommand === 'get'
4248
+ && isUnsupportedBaseagentConfigField(parsed.resolvedConfigCommand.field)) {
4249
+ continue;
4250
+ }
4019
4251
  return { error: decision.reason, code: decision.code };
4020
4252
  }
4021
4253
  authorizedCommands.push({ parsed, decision });
@@ -4047,26 +4279,78 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4047
4279
  // fetch 文件作为独立 result.file 消息异步发回;把请求 id 作为 correlationId 透传,
4048
4280
  // 客户端用它把异步到达的文件消息对回这次 fetch 点击。
4049
4281
  if (cmdBase === '/file') {
4050
- const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4051
4282
  if (action === 'list') {
4052
4283
  const dirArg = (args?.path ?? '.').toString().trim() || '.';
4053
- const resolved = resolveMenuFilePath(dirArg, projectPath, identity.role, 'directory');
4054
- if ('error' in resolved)
4055
- return resolved;
4056
4284
  const offset = parseFileListOffset(args?.offset);
4057
4285
  const limit = parseFileListLimit(args?.limit);
4058
4286
  const includeHidden = args?.includeHidden === true;
4059
- const listed = listDirectory(resolved.realPath, { offset, limit, includeHidden, projectPath: resolved.projectPath, role: identity.role });
4287
+ let listed;
4288
+ let responsePath = dirArg;
4289
+ let dataRoot;
4290
+ if (args?.rootId !== undefined) {
4291
+ try {
4292
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, dirArg, 'directory');
4293
+ listed = listDirectory(resolved.realPath, {
4294
+ offset,
4295
+ limit,
4296
+ includeHidden,
4297
+ projectPath: resolved.rootPath,
4298
+ role: undefined,
4299
+ });
4300
+ responsePath = resolved.relativePath;
4301
+ dataRoot = resolved;
4302
+ }
4303
+ catch (error) {
4304
+ return dataRootFailure(error);
4305
+ }
4306
+ }
4307
+ else {
4308
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4309
+ const resolved = resolveMenuFilePath(dirArg, projectPath, identity.role, 'directory');
4310
+ if ('error' in resolved)
4311
+ return resolved;
4312
+ listed = listDirectory(resolved.realPath, { offset, limit, includeHidden, projectPath: resolved.projectPath, role: identity.role });
4313
+ }
4060
4314
  if ('error' in listed)
4061
4315
  return listed;
4062
- return { data: { path: dirArg, ...listed.data } };
4316
+ return {
4317
+ data: {
4318
+ path: responsePath,
4319
+ ...(dataRoot ? {
4320
+ root: {
4321
+ scope: dataRoot.scope,
4322
+ targetAid: dataRoot.targetAid,
4323
+ ...(dataRoot.peerAid ? { peerAid: dataRoot.peerAid } : {}),
4324
+ ...(dataRoot.peerKey ? { peerKey: dataRoot.peerKey } : {}),
4325
+ expiresAt: dataRoot.expiresAt,
4326
+ },
4327
+ } : {}),
4328
+ ...listed.data,
4329
+ },
4330
+ };
4063
4331
  }
4064
4332
  if (action !== 'fetch')
4065
4333
  return { error: `不支持的 file action: ${action}`, code: 'NOT_SUPPORTED' };
4066
- const resolved = resolveMenuFilePath(args?.path, projectPath, identity.role, 'file');
4067
- if ('error' in resolved)
4068
- return resolved;
4069
- const { realPath, stat } = resolved;
4334
+ let realPath;
4335
+ let stat;
4336
+ let dataRoot;
4337
+ if (args?.rootId !== undefined) {
4338
+ try {
4339
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, args?.path, 'file');
4340
+ ({ realPath, stat } = resolved);
4341
+ dataRoot = resolved;
4342
+ }
4343
+ catch (error) {
4344
+ return dataRootFailure(error);
4345
+ }
4346
+ }
4347
+ else {
4348
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4349
+ const resolved = resolveMenuFilePath(args?.path, projectPath, identity.role, 'file');
4350
+ if ('error' in resolved)
4351
+ return resolved;
4352
+ ({ realPath, stat } = resolved);
4353
+ }
4070
4354
  if (stat.size > FILE_FETCH_MAX_SIZE) {
4071
4355
  return { error: `文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 ${FILE_FETCH_MAX_SIZE / 1024 / 1024} MB)`, code: 'FILE_TOO_LARGE' };
4072
4356
  }
@@ -4087,15 +4371,21 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4087
4371
  }
4088
4372
  : sessionReplyContext;
4089
4373
  const sendFile = async () => {
4090
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
4374
+ const receipt = await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
4375
+ if (receipt && typeof receipt === 'object' && receipt.status === 'failed') {
4376
+ throw Object.assign(new Error(receipt.error || '文件发送失败'), {
4377
+ code: receipt.code ?? 'EXEC_FAILED',
4378
+ outboxId: receipt.outboxId,
4379
+ });
4380
+ }
4091
4381
  };
4092
4382
  if (deferSideEffect) {
4093
4383
  deferSideEffect(sendFile);
4094
- return { data: { accepted: true } };
4384
+ return { data: { accepted: true, ...(dataRoot ? { scope: dataRoot.scope, expiresAt: dataRoot.expiresAt } : {}) } };
4095
4385
  }
4096
4386
  try {
4097
4387
  await sendFile();
4098
- return { data: { accepted: true } };
4388
+ return { data: { accepted: true, ...(dataRoot ? { scope: dataRoot.scope, expiresAt: dataRoot.expiresAt } : {}) } };
4099
4389
  }
4100
4390
  catch (e) {
4101
4391
  return { error: `文件发送失败: ${e?.message ?? e}`, code: 'EXEC_FAILED' };
@@ -4152,8 +4442,8 @@ function menuCommandToken(cmd) {
4152
4442
  }
4153
4443
  function isProcessLevelMenu(name, cmd) {
4154
4444
  const cmdToken = menuCommandToken(cmd);
4155
- return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models'
4156
- || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models';
4445
+ return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models' || name === 'baseagents'
4446
+ || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models' || cmdToken === '/baseagents';
4157
4447
  }
4158
4448
  function isProcessContextMenu(payload) {
4159
4449
  const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
@@ -4173,8 +4463,15 @@ async function execMenuForSystemControl(payload, context) {
4173
4463
  const isFileFetch = (payload?.type === 'menu.query'
4174
4464
  || (payload?.type === 'menu.action' && payload?.action === 'fetch'))
4175
4465
  && (name === 'file' || compatCmd === '/file');
4176
- if (isFileFetch) {
4177
- return ecwebErr(id, name, 'NOT_SUPPORTED', 'Host/control 不支持 file.fetch,请通过目标 Agent AID 请求');
4466
+ const isDataRootQuery = payload?.type === 'menu.query'
4467
+ && (name === 'agent-data-root'
4468
+ || name === 'relation-data-root'
4469
+ || compatCmd === '/agent-data-root'
4470
+ || compatCmd === '/relation-data-root');
4471
+ const isRootedFile = (name === 'file' || compatCmd === '/file')
4472
+ && typeof payload?.args?.rootId === 'string';
4473
+ if (isFileFetch || isDataRootQuery || isRootedFile || isAgentStorageMenuRequest(payload ?? {})) {
4474
+ return ecwebErr(id, name, 'NOT_SUPPORTED', 'Host/control 不支持数据根文件访问,请通过目标 Agent AID 请求');
4178
4475
  }
4179
4476
  if (name === 'cli' || compatCmd === '/cli') {
4180
4477
  const message = context.source === 'ecweb'
@@ -4216,11 +4513,18 @@ async function execMenuForSystemControl(payload, context) {
4216
4513
  conversationId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
4217
4514
  processOwners: context.owners,
4218
4515
  fromControlChannel: true,
4516
+ ecwebManagement: context.source === 'ecweb',
4219
4517
  canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
4220
4518
  trustedProcessRole: context.source === 'ecweb' ? 'daemon-owner' : undefined,
4221
4519
  });
4222
4520
  const trustedIdentity = trustedSubject.identity;
4223
4521
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
4522
+ if (payload?.type === 'menu.action'
4523
+ && menuCommandToken(cmd) === '/system'
4524
+ && payload.action === 'paircode'
4525
+ && (context.authMethod !== 'daemon-owner' || (context.source === 'control' && !context.encrypted))) {
4526
+ return ecwebErr(id, name, 'NOT_ALLOWED', 'Pairing code issuance requires encrypted DaemonOwner authentication');
4527
+ }
4224
4528
  try {
4225
4529
  if (menuCommandToken(cmd) === '/baseagent-models') {
4226
4530
  if (payload.type !== 'menu.query')
@@ -4239,6 +4543,8 @@ async function execMenuForSystemControl(payload, context) {
4239
4543
  channelScope: 'control',
4240
4544
  channelId: targetChannel,
4241
4545
  request: payload.args.context,
4546
+ paircodeAllowed: context.authMethod === 'daemon-owner'
4547
+ && (context.source !== 'control' || context.encrypted),
4242
4548
  });
4243
4549
  return menuSuccess({ id, ...(name ? { name } : {}) }, data);
4244
4550
  }
@@ -4343,15 +4649,6 @@ export async function execMenuForEcweb(payload, trusted) {
4343
4649
  const validationError = validateMenuRequest(payload ?? {});
4344
4650
  if (validationError)
4345
4651
  return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
4346
- const isProcessLevel = isProcessLevelMenu(name, payload?.cmd) || isProcessContextMenu(payload);
4347
- const owners = loadDaemonConfig().owners ?? [];
4348
- if (isProcessLevel && owners.length === 0) {
4349
- return menuFailure({ id, ...(name ? { name } : {}) }, {
4350
- code: 'NOT_ALLOWED',
4351
- message: formatAuthorizationDenial('Configure daemon.json.owners before using process-level operations.', 'DAEMON_OWNER_REQUIRED'),
4352
- data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4353
- });
4354
- }
4355
4652
  if (!trusted || (!trusted.localDirect && trusted.authenticated !== true)) {
4356
4653
  return menuFailure({ id, ...(name ? { name } : {}) }, {
4357
4654
  code: 'PERMISSION_DENIED',
@@ -4359,29 +4656,22 @@ export async function execMenuForEcweb(payload, trusted) {
4359
4656
  data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
4360
4657
  });
4361
4658
  }
4362
- // A verified ECWeb connection has daemon-owner management authority. If the
4363
- // bearer token has no explicit owner provenance (the normal pairing path),
4364
- // use the first currently configured owner as the canonical management
4365
- // principal. This keeps audit/Trigger approval attribution deterministic
4366
- // without requiring owner-bound pairing metadata.
4659
+ // ECWeb authentication grants daemon-owner authority independently of the
4660
+ // configured owners. Prefer an owner for audit/Trigger attribution when
4661
+ // available; otherwise keep local and remote management actors distinct.
4662
+ const owners = loadDaemonConfig().owners ?? [];
4367
4663
  const userId = (trusted.actorAid && owners.includes(trusted.actorAid))
4368
4664
  ? trusted.actorAid
4369
- : owners[0] ?? (trusted.localDirect ? 'local-direct' : undefined);
4370
- if (!userId) {
4371
- return menuFailure({ id, ...(name ? { name } : {}) }, {
4372
- code: 'PERMISSION_DENIED',
4373
- message: 'ECWeb operation requires configured daemon owners',
4374
- data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
4375
- });
4376
- }
4665
+ : owners[0] ?? (trusted.localDirect ? 'local-direct' : 'ecweb-remote');
4377
4666
  return execMenuForSystemControl.call(this, payload, {
4378
4667
  source: 'ecweb',
4379
4668
  actorAid: userId,
4669
+ authMethod: trusted.localDirect ? 'daemon-owner' : 'remote-access-token',
4670
+ encrypted: trusted.localDirect,
4380
4671
  localDirect: trusted.localDirect,
4381
4672
  // The ECWeb server/IPC control-token gate authenticates this management
4382
4673
  // request. All authenticated ECWeb connections are equally eligible to
4383
- // approve persistent fullaccess; actorAid is used only when it identifies
4384
- // a current configured owner.
4674
+ // approve persistent fullaccess without owner-bound token metadata.
4385
4675
  canApprovePersistentFullAccess: true,
4386
4676
  owners,
4387
4677
  });
@@ -4415,6 +4705,8 @@ export async function execMenuForControl(payload, peerId, auth = {}) {
4415
4705
  return execMenuForSystemControl.call(this, payload, {
4416
4706
  source: 'control',
4417
4707
  actorAid: managementActor,
4708
+ authMethod: authorization.allowed ? authorization.authMethod : 'daemon-owner',
4709
+ encrypted: auth.encrypted === true,
4418
4710
  localDirect: false,
4419
4711
  canApprovePersistentFullAccess: false,
4420
4712
  owners,