evolcore 0.0.1 → 0.0.3

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 (379) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +659 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +874 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1446 -0
  161. package/dist/core/message/message-utils.js +76 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. package/dist/index.js.map +0 -1
@@ -0,0 +1,1017 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
3
+ import { renderActionAsText } from '../interaction-router.js';
4
+ import { buildEnvelope, sendInteractionPayload } from '../message/message-utils.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ import { summarizeToolInput } from '../../utils/tool-summary.js';
7
+ import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
8
+ import { recordCausationSpan } from '../causation/audit.js';
9
+ import { checkDangerousCommand } from './tool-policy.js';
10
+ export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
11
+ const dangerCheck = checkDangerousCommand(toolName, input);
12
+ if (!dangerCheck.isDangerous) {
13
+ return { matched: false };
14
+ }
15
+ if (mode === 'bypass' && dangerCheck.approvalScope === 'request') {
16
+ return { matched: true, decision: 'allow' };
17
+ }
18
+ if (!gateway || !sendPrompt) {
19
+ logger.warn(`[PermissionGateway] Dangerous operation denied because approval is unavailable: session=${sessionId} tool=${toolName}`);
20
+ return { matched: true, decision: 'deny' };
21
+ }
22
+ const decision = await gateway.requestPermission(sessionId, dangerCheck.cacheKey, input, sendPrompt, context, `⚠️ ${dangerCheck.command}`, dangerCheck.reason, grantScope, dangerCheck.kind === 'git-destructive' ? 'agent_manager' : undefined);
23
+ return { matched: true, decision };
24
+ }
25
+ const SESSION_GRANT_TTL_MS = 30 * 60 * 1000;
26
+ const LOCAL_APPROVAL_TTL_MS = 20 * 60 * 1000;
27
+ const MANAGEMENT_APPROVAL_REVALIDATION_INTERVAL_MS = 5_000;
28
+ const APPROVAL_DETAIL_LIMIT = 800;
29
+ function stablePermissionInput(value) {
30
+ if (value === undefined)
31
+ return 'undefined';
32
+ if (typeof value === 'bigint')
33
+ return `${value.toString()}n`;
34
+ if (value === null || typeof value !== 'object')
35
+ return JSON.stringify(value) ?? String(value);
36
+ if (Array.isArray(value))
37
+ return `[${value.map(stablePermissionInput).join(',')}]`;
38
+ const record = value;
39
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${stablePermissionInput(record[key])}`).join(',')}}`;
40
+ }
41
+ function permissionInputFingerprint(input) {
42
+ return createHash('sha256').update(stablePermissionInput(input)).digest('hex');
43
+ }
44
+ function truncateApprovalDetail(value) {
45
+ const trimmed = value.trim();
46
+ return trimmed.length > APPROVAL_DETAIL_LIMIT
47
+ ? `${trimmed.slice(0, APPROVAL_DETAIL_LIMIT - 3)}...`
48
+ : trimmed;
49
+ }
50
+ function structuredApprovalAuditSummary(input) {
51
+ const fields = [...new Set(Object.keys(input)
52
+ .map(key => key.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 48))
53
+ .filter(Boolean))]
54
+ .sort()
55
+ .slice(0, 16);
56
+ return fields.length > 0 ? `input_fields=${fields.join(',')}` : 'input_withheld';
57
+ }
58
+ function requiredApproverRole(policy, routeKind) {
59
+ if (policy === 'requester')
60
+ return 'requester';
61
+ if (policy === 'agent_owner' || routeKind === 'handoff')
62
+ return 'owner';
63
+ return 'manager';
64
+ }
65
+ function formatApprovalTarget(toolName, input) {
66
+ if (toolName === 'Bash' || toolName.startsWith('dangerous:Bash:')) {
67
+ const command = typeof input.command === 'string' ? input.command : '';
68
+ if (command)
69
+ return truncateApprovalDetail(command);
70
+ }
71
+ const summary = summarizeToolInput(toolName, input);
72
+ if (summary)
73
+ return truncateApprovalDetail(summary);
74
+ return truncateApprovalDetail(stablePermissionInput(input));
75
+ }
76
+ function escapeApprovalMarkdown(value) {
77
+ return value
78
+ .replace(/\\/g, '\\\\')
79
+ .replace(/([`*_{}[\]()])/g, '\\$1')
80
+ .replace(/^(\s*)(#{1,6}|>|[-+])(\s)/gm, '$1\\$2$3')
81
+ .replace(/^(\s*\d+)\.(\s)/gm, '$1\\.$2');
82
+ }
83
+ function approvalInlineCode(value) {
84
+ const text = value.replace(/\s+/g, ' ').trim();
85
+ const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map(run => run.length));
86
+ const fence = '`'.repeat(longestRun + 1);
87
+ return `${fence}${text}${fence}`;
88
+ }
89
+ function approvalCodeBlock(value) {
90
+ const longestRun = Math.max(0, ...(value.match(/`+/g) ?? []).map(run => run.length));
91
+ const fence = '`'.repeat(Math.max(3, longestRun + 1));
92
+ return `${fence}text\n${value}\n${fence}`;
93
+ }
94
+ export function planApprovalRoute(challenge, context) {
95
+ if (!challenge.grantable) {
96
+ return { kind: 'unavailable', reason: 'challenge_not_grantable' };
97
+ }
98
+ const actor = context?.actor;
99
+ if (actor && context?.userId && actor.operatorId !== context.userId) {
100
+ return { kind: 'unavailable', reason: 'actor_operator_mismatch' };
101
+ }
102
+ if (actor && context?.adapter && actor.operatorChannelKey !== context.adapter.channelKey) {
103
+ return { kind: 'unavailable', reason: 'actor_channel_mismatch' };
104
+ }
105
+ const requesterId = actor?.operatorId || context?.userId || '';
106
+ if (!requesterId)
107
+ return { kind: 'unavailable', reason: 'no_requester_approver' };
108
+ const requesterChannelKey = actor?.operatorChannelKey || context?.adapter?.channelKey;
109
+ if (challenge.approverPolicy === 'requester') {
110
+ return {
111
+ kind: 'local',
112
+ approverOperatorId: requesterId,
113
+ approverChannelKey: requesterChannelKey,
114
+ approverPrincipalId: hasTrustedPrincipal(actor) ? actor.principalId : undefined,
115
+ };
116
+ }
117
+ const routing = context?.approvalRouting;
118
+ let candidateSnapshot;
119
+ if (routing?.resolveApproverCandidates) {
120
+ try {
121
+ candidateSnapshot = routing.resolveApproverCandidates();
122
+ }
123
+ catch {
124
+ return { kind: 'unavailable', reason: 'approver_candidates_unavailable' };
125
+ }
126
+ }
127
+ const owners = Array.from(new Set((candidateSnapshot?.owners ?? routing?.owners ?? []).filter(Boolean)));
128
+ const admins = Array.from(new Set((candidateSnapshot?.admins ?? routing?.admins ?? []).filter(Boolean)));
129
+ const eligibleLocalPrincipals = challenge.approverPolicy === 'agent_manager'
130
+ ? new Set([...owners, ...admins])
131
+ : new Set(owners);
132
+ const supportsAuthenticatedApproval = context?.adapter?.capabilities.authenticatedApproval === true;
133
+ const actorMatchesContext = !!actor
134
+ && actor.operatorId === requesterId
135
+ && (!context?.userId || actor.operatorId === context.userId)
136
+ && actor.operatorChannelKey === requesterChannelKey
137
+ && context?.adapter?.channelKey === actor.operatorChannelKey;
138
+ if (!routing?.forceHandoff
139
+ && context?.chatType !== 'group'
140
+ && supportsAuthenticatedApproval
141
+ && actorMatchesContext
142
+ && hasTrustedPrincipal(actor)
143
+ && eligibleLocalPrincipals.has(actor.principalId)) {
144
+ return {
145
+ kind: 'local',
146
+ approverOperatorId: actor.operatorId,
147
+ approverChannelKey: actor.operatorChannelKey,
148
+ approverPrincipalId: actor.principalId,
149
+ };
150
+ }
151
+ if (owners.length === 0) {
152
+ return { kind: 'unavailable', reason: 'no_agent_owner_configured' };
153
+ }
154
+ // Both management policies hand off to the first configured AUN owner.
155
+ // Admin handoff approval and implicit approver selection are intentionally
156
+ // out of scope; a future caller may explicitly select from the owner list.
157
+ const approverId = owners[0];
158
+ if (!approverId) {
159
+ return { kind: 'unavailable', reason: 'no_aun_owner_available' };
160
+ }
161
+ if (!routing?.ownerAdapter) {
162
+ return { kind: 'unavailable', reason: 'owner_approval_channel_unavailable' };
163
+ }
164
+ return {
165
+ kind: 'handoff',
166
+ approverOperatorId: approverId,
167
+ approverChannelKey: routing.ownerAdapter.channelKey,
168
+ approverPrincipalId: approverId,
169
+ channel: 'aun',
170
+ adapter: routing.ownerAdapter,
171
+ };
172
+ }
173
+ export class PermissionGateway {
174
+ pending = new Map();
175
+ crossSessionPending = new Map();
176
+ temporaryGrants = new Map();
177
+ managementRevalidationTimer;
178
+ eventBus;
179
+ setEventBus(eventBus) {
180
+ this.eventBus = eventBus;
181
+ }
182
+ hasManagementPending() {
183
+ return [...this.pending.values(), ...this.crossSessionPending.values()]
184
+ .some(pending => pending.requiredApproverRole !== 'requester');
185
+ }
186
+ syncManagementRevalidationTimer() {
187
+ if (!this.hasManagementPending()) {
188
+ if (this.managementRevalidationTimer)
189
+ clearTimeout(this.managementRevalidationTimer);
190
+ this.managementRevalidationTimer = undefined;
191
+ return;
192
+ }
193
+ if (this.managementRevalidationTimer)
194
+ return;
195
+ this.managementRevalidationTimer = setTimeout(() => {
196
+ this.managementRevalidationTimer = undefined;
197
+ this.revalidatePendingApprovals(undefined, 'periodic');
198
+ this.syncManagementRevalidationTimer();
199
+ }, MANAGEMENT_APPROVAL_REVALIDATION_INTERVAL_MS);
200
+ this.managementRevalidationTimer.unref?.();
201
+ }
202
+ clearPendingResources(pending) {
203
+ if (pending.timeout)
204
+ clearTimeout(pending.timeout);
205
+ if (pending.abortSignal && pending.abortListener) {
206
+ pending.abortSignal.removeEventListener('abort', pending.abortListener);
207
+ }
208
+ }
209
+ bindAbortSignal(pending, signal, onAbort) {
210
+ if (!signal)
211
+ return false;
212
+ pending.abortSignal = signal;
213
+ pending.abortListener = onAbort;
214
+ if (signal.aborted) {
215
+ onAbort();
216
+ return true;
217
+ }
218
+ signal.addEventListener('abort', onAbort, { once: true });
219
+ return false;
220
+ }
221
+ /** Clear all temporary grants when the permission context changes. */
222
+ clearAlwaysAllow() {
223
+ this.temporaryGrants.clear();
224
+ }
225
+ temporaryGrantKey(sessionId, toolName, inputFingerprint, grantScope) {
226
+ return `${sessionId}\u0000${grantScope}\u0000${toolName}\u0000${inputFingerprint}`;
227
+ }
228
+ pruneTemporaryGrants(now) {
229
+ for (const [key, grant] of this.temporaryGrants.entries()) {
230
+ if (grant.expiresAt <= now)
231
+ this.temporaryGrants.delete(key);
232
+ }
233
+ }
234
+ hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
235
+ return !!this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
236
+ }
237
+ getTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
238
+ const now = Date.now();
239
+ this.pruneTemporaryGrants(now);
240
+ const key = this.temporaryGrantKey(sessionId, toolName, permissionInputFingerprint(toolInput), grantScope);
241
+ return this.temporaryGrants.get(key);
242
+ }
243
+ addTemporaryGrant(pending, causation) {
244
+ const now = Date.now();
245
+ this.pruneTemporaryGrants(now);
246
+ const expiresAt = now + SESSION_GRANT_TTL_MS;
247
+ const key = this.temporaryGrantKey(pending.sessionId, pending.toolName, pending.inputFingerprint, pending.grantScope);
248
+ this.temporaryGrants.set(key, { expiresAt, causation: normalizeCausation(causation) });
249
+ return expiresAt;
250
+ }
251
+ authorizePendingResponse(pending, operatorId, channelKey) {
252
+ if (!operatorId || operatorId !== pending.approverOperatorId) {
253
+ return { status: 'identity_mismatch' };
254
+ }
255
+ if (pending.approverChannelKey && channelKey !== pending.approverChannelKey) {
256
+ return { status: 'identity_mismatch' };
257
+ }
258
+ if (pending.requiredApproverRole === 'requester') {
259
+ return pending.approverPolicy === 'requester'
260
+ ? { status: 'authorized' }
261
+ : { status: 'revoked', reason: 'approver_role_requirement_invalid' };
262
+ }
263
+ if (pending.approverPolicy === 'requester') {
264
+ return { status: 'revoked', reason: 'approver_role_requirement_invalid' };
265
+ }
266
+ if (!pending.approverChannelKey || !pending.approverPrincipalId) {
267
+ return { status: 'revoked', reason: 'approver_identity_incomplete' };
268
+ }
269
+ if (!pending.reauthorizeApproval) {
270
+ return { status: 'revoked', reason: 'approver_reauthorization_unavailable' };
271
+ }
272
+ const request = {
273
+ approverPolicy: pending.approverPolicy,
274
+ operatorId,
275
+ operatorChannelKey: channelKey,
276
+ expectedPrincipalId: pending.approverPrincipalId,
277
+ selfAid: pending.selfAid,
278
+ originPrincipalId: pending.originPrincipalId,
279
+ originAuthRevision: pending.originAuthRevision,
280
+ };
281
+ let result;
282
+ try {
283
+ result = pending.reauthorizeApproval(request);
284
+ }
285
+ catch (error) {
286
+ logger.warn(`[PermissionGateway] Approval reauthorization failed: operator=${operatorId} policy=${pending.approverPolicy} error=${error instanceof Error ? error.message : String(error)}`);
287
+ return { status: 'revoked', reason: 'approver_reauthorization_failed' };
288
+ }
289
+ if (!result.ok)
290
+ return { status: 'revoked', reason: result.reason };
291
+ const actor = result.actor;
292
+ if (actor.operatorId !== operatorId || actor.operatorChannelKey !== channelKey) {
293
+ return { status: 'revoked', reason: 'approver_identity_changed' };
294
+ }
295
+ if (!hasTrustedPrincipal(actor)) {
296
+ const reason = actor.principalStatus === 'unmapped'
297
+ ? 'principal_mapping_removed'
298
+ : actor.principalStatus === 'conflict'
299
+ ? 'principal_mapping_conflict'
300
+ : 'approver_identity_untrusted';
301
+ return { status: 'revoked', reason };
302
+ }
303
+ if (actor.principalId !== pending.approverPrincipalId) {
304
+ return { status: 'revoked', reason: 'principal_mapping_changed' };
305
+ }
306
+ const owners = new Set(result.owners.filter(Boolean));
307
+ const admins = new Set(result.admins.filter(Boolean));
308
+ const eligible = pending.requiredApproverRole === 'owner'
309
+ ? owners.has(actor.principalId)
310
+ : owners.has(actor.principalId) || admins.has(actor.principalId);
311
+ if (!eligible) {
312
+ const stillAssigned = owners.has(actor.principalId) || admins.has(actor.principalId);
313
+ return {
314
+ status: 'revoked',
315
+ reason: stillAssigned ? 'approver_role_changed' : 'approver_revoked',
316
+ };
317
+ }
318
+ if (pending.originPrincipalId !== undefined
319
+ && result.originPrincipalId !== pending.originPrincipalId) {
320
+ return { status: 'revoked', reason: 'origin_authorization_changed' };
321
+ }
322
+ if (pending.originAuthRevision !== undefined
323
+ && result.originAuthRevision !== pending.originAuthRevision) {
324
+ return { status: 'revoked', reason: 'origin_authorization_changed' };
325
+ }
326
+ return { status: 'authorized' };
327
+ }
328
+ /** Legacy diagnostics alias. Grants are no longer tool-wide or permanent. */
329
+ getAlwaysAllowList() {
330
+ this.pruneTemporaryGrants(Date.now());
331
+ return [...this.temporaryGrants.keys()];
332
+ }
333
+ resolveCrossSessionPermission(sessionId, requestId, action, _values, operatorId, channelKey) {
334
+ const pending = this.crossSessionPending.get(requestId);
335
+ if (!pending || pending.sessionId !== sessionId)
336
+ return false;
337
+ const authorization = this.authorizePendingResponse(pending, operatorId, channelKey);
338
+ if (authorization.status === 'identity_mismatch')
339
+ return false;
340
+ if (authorization.status === 'revoked') {
341
+ logger.info(`[PermissionGateway] approval_revoked request=${requestId} policy=${pending.approverPolicy} reason=${authorization.reason}`);
342
+ this.expireCrossSessionPermission(sessionId, requestId, authorization.reason);
343
+ return false;
344
+ }
345
+ this.clearPendingResources(pending);
346
+ pending.interactionRouter?.cancel(requestId);
347
+ this.crossSessionPending.delete(requestId);
348
+ this.syncManagementRevalidationTimer();
349
+ const sessionGrant = action === 'approve_session_30m';
350
+ const approved = action === 'approve_once' || sessionGrant || action === 'allow';
351
+ const decision = approved ? 'allow' : 'deny';
352
+ const decisionCausation = deriveCausation(pending.causation);
353
+ if (sessionGrant)
354
+ this.addTemporaryGrant(pending, decisionCausation);
355
+ pending.resolve(decision);
356
+ void this.notifyPermissionState(pending, approved
357
+ ? { state: 'approved', requestId, toolName: pending.toolName }
358
+ : {
359
+ state: 'failed',
360
+ requestId,
361
+ toolName: pending.toolName,
362
+ reason: 'denied',
363
+ message: 'owner 已拒绝授权,任务已停止。',
364
+ });
365
+ recordCausationSpan(decisionCausation, 'permission.decision', {
366
+ status: approved ? 'completed' : 'failed',
367
+ refs: { permissionRequestId: requestId, sessionId },
368
+ reason: approved ? undefined : 'denied',
369
+ });
370
+ this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, approved, causation: decisionCausation });
371
+ return true;
372
+ }
373
+ expireCrossSessionPermission(sessionId, requestId, reason, notifyState = true, cancelled = reason === 'cancelled') {
374
+ const pending = this.crossSessionPending.get(requestId);
375
+ if (!pending || pending.sessionId !== sessionId)
376
+ return false;
377
+ this.clearPendingResources(pending);
378
+ pending.interactionRouter?.cancel(requestId, reason);
379
+ this.crossSessionPending.delete(requestId);
380
+ this.syncManagementRevalidationTimer();
381
+ pending.resolve('deny');
382
+ if (notifyState) {
383
+ const failure = permissionFailure(reason);
384
+ void this.notifyPermissionState(pending, {
385
+ state: 'failed',
386
+ requestId,
387
+ toolName: pending.toolName,
388
+ reason,
389
+ message: failure,
390
+ });
391
+ }
392
+ const decisionCausation = deriveCausation(pending.causation);
393
+ recordCausationSpan(decisionCausation, 'permission.decision', {
394
+ status: 'failed',
395
+ refs: { permissionRequestId: requestId, sessionId },
396
+ reason,
397
+ });
398
+ if (cancelled) {
399
+ this.eventBus?.publish({
400
+ type: 'permission:cancelled',
401
+ sessionId,
402
+ requestId,
403
+ toolName: pending.toolName,
404
+ reason,
405
+ causation: decisionCausation,
406
+ });
407
+ }
408
+ else {
409
+ this.eventBus?.publish({
410
+ type: 'permission:resolved',
411
+ sessionId,
412
+ requestId,
413
+ toolName: pending.toolName,
414
+ reason,
415
+ approved: false,
416
+ causation: decisionCausation,
417
+ });
418
+ }
419
+ return true;
420
+ }
421
+ async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation) {
422
+ const approval = context.approvalRouting;
423
+ const interactionRouter = context.interactionRouter;
424
+ if (!approval || !interactionRouter) {
425
+ await sendPrompt('当前会话权限不足,且没有可用的 owner 审批通道。');
426
+ return 'deny';
427
+ }
428
+ const { id: requestId, toolName, toolInput, summary: displaySummary, reason } = challenge;
429
+ const ttlMs = approval.approvalTtlMs && approval.approvalTtlMs > 0
430
+ ? approval.approvalTtlMs
431
+ : 20 * 60 * 1000;
432
+ const expiresAt = Date.now() + ttlMs;
433
+ const originRole = approval.originRole || context.role || 'none';
434
+ const requesterId = approval.originPeerId || approval.originChannelId || 'unknown';
435
+ const requester = approval.originPeerName
436
+ ? `${approval.originPeerName} (${requesterId})`
437
+ : requesterId;
438
+ const originChannel = approval.originChannel || context.channel || 'unknown';
439
+ const distinctSource = approval.originChannelId
440
+ && approval.originChannelId !== requesterId
441
+ ? approval.originChannelId
442
+ : undefined;
443
+ const approvalDeadline = new Date(expiresAt).toLocaleString('zh-CN', { hour12: false });
444
+ const target = formatApprovalTarget(toolName, toolInput);
445
+ const body = [
446
+ '**申请信息**',
447
+ '',
448
+ `- **申请主体**:${approvalInlineCode(requester)} · role ${approvalInlineCode(originRole)} · via ${approvalInlineCode(originChannel)}`,
449
+ ...(distinctSource ? [`- **来源会话**:${approvalInlineCode(distinctSource)}`] : []),
450
+ '',
451
+ '**请求执行**',
452
+ '',
453
+ `- **申请能力**:${approvalInlineCode(`tool:${toolName}`)}`,
454
+ `- **预期动作**:${escapeApprovalMarkdown(displaySummary)}`,
455
+ `- **申请原因**:${escapeApprovalMarkdown(reason || '工具运行时要求人工授权')}`,
456
+ '',
457
+ '**目标 / 参数**',
458
+ '',
459
+ approvalCodeBlock(target),
460
+ '',
461
+ '**风险与有效期**',
462
+ '',
463
+ '- **风险:中** · 跨会话临时授权,请核对目标和参数',
464
+ `- **审批截止**:${escapeApprovalMarkdown(approvalDeadline)}(约 ${Math.round(ttlMs / 60000)} 分钟)`,
465
+ ].join('\n');
466
+ const interaction = {
467
+ type: 'interaction',
468
+ id: requestId,
469
+ kind: {
470
+ kind: 'action',
471
+ title: '临时授权申请',
472
+ body,
473
+ bodyFormat: 'markdown',
474
+ buttons: [
475
+ { key: 'approve_once', label: '批准本次', style: 'primary' },
476
+ { key: 'approve_session_30m', label: '本会话 30 分钟', style: 'default' },
477
+ { key: 'deny', label: '拒绝', style: 'danger' },
478
+ ],
479
+ },
480
+ channelId: route.approverOperatorId,
481
+ sessionId,
482
+ initiatorId: route.approverOperatorId || undefined,
483
+ expiresAt,
484
+ fallback: {
485
+ command: 'perm',
486
+ buttonArgMap: {
487
+ approve_once: `${requestId} allow`,
488
+ approve_session_30m: `${requestId} always`,
489
+ deny: `${requestId} deny`,
490
+ },
491
+ },
492
+ };
493
+ // Register before delivery. Some adapters can surface a user response as
494
+ // soon as send() completes; registering afterwards creates a narrow race
495
+ // where a valid approval is visible to the user but dropped by the router.
496
+ const decisionPromise = new Promise((resolve) => {
497
+ const pending = {
498
+ sessionId,
499
+ requestId,
500
+ toolName,
501
+ displaySummary,
502
+ reason,
503
+ resolve,
504
+ inputFingerprint: permissionInputFingerprint(toolInput),
505
+ grantScope,
506
+ approverPolicy: challenge.approverPolicy,
507
+ approvalRouteKind: route.kind,
508
+ requiredApproverRole: requiredApproverRole(challenge.approverPolicy, route.kind),
509
+ approverOperatorId: route.approverOperatorId,
510
+ approverChannelKey: route.approverChannelKey,
511
+ approverPrincipalId: route.approverPrincipalId,
512
+ selfAid: approval.selfAid,
513
+ originPrincipalId: approval.originPrincipalId,
514
+ originAuthRevision: approval.originAuthRevision,
515
+ reauthorizeApproval: approval.reauthorizeApproval,
516
+ interactionRouter,
517
+ causation: requestCausation,
518
+ permissionStateChanged: context.permissionStateChanged,
519
+ };
520
+ this.crossSessionPending.set(requestId, pending);
521
+ this.syncManagementRevalidationTimer();
522
+ interactionRouter.register(requestId, sessionId, (action, values, operatorId, channelKey) => {
523
+ return this.resolveCrossSessionPermission(sessionId, requestId, action, values, operatorId, channelKey);
524
+ }, {
525
+ initiatorId: route.approverOperatorId,
526
+ initiatorChannelKey: route.approverChannelKey,
527
+ timeoutMs: ttlMs,
528
+ timeoutCancelReason: 'expired',
529
+ onTimeout: () => this.expireCrossSessionPermission(sessionId, requestId, 'expired'),
530
+ onChannelCancel: reason => this.expireCrossSessionPermission(sessionId, requestId, reason),
531
+ onCancel: reason => route.adapter.invalidateInteraction?.(requestId, reason),
532
+ fallbackCommand: 'perm',
533
+ });
534
+ });
535
+ const crossPending = this.crossSessionPending.get(requestId);
536
+ if (!crossPending)
537
+ return decisionPromise;
538
+ if (this.bindAbortSignal(crossPending, context.abortSignal, () => this.expireCrossSessionPermission(sessionId, requestId, 'cancelled'))) {
539
+ return decisionPromise;
540
+ }
541
+ if (context.flushPending) {
542
+ try {
543
+ await context.flushPending();
544
+ }
545
+ catch {
546
+ // flush 失败不阻断审批请求
547
+ }
548
+ }
549
+ // The origin flush can yield long enough for an alias/config watcher or
550
+ // periodic revalidation to revoke this request. Do not deliver a stale card.
551
+ if (this.crossSessionPending.get(requestId) !== crossPending) {
552
+ return decisionPromise;
553
+ }
554
+ const currentAuthorization = this.authorizePendingResponse(crossPending, crossPending.approverOperatorId, crossPending.approverChannelKey);
555
+ if (currentAuthorization.status !== 'authorized') {
556
+ this.expireCrossSessionPermission(sessionId, requestId, currentAuthorization.status === 'revoked'
557
+ ? currentAuthorization.reason
558
+ : 'approver_identity_mismatch');
559
+ return decisionPromise;
560
+ }
561
+ const ownerReplyContext = {
562
+ metadata: {
563
+ source: 'handoff',
564
+ chatmode: 'interactive',
565
+ causation: crossPending.causation,
566
+ },
567
+ };
568
+ const envelope = buildEnvelope({
569
+ taskId: context.taskId,
570
+ sessionId,
571
+ channel: route.adapter.channelName,
572
+ channelId: route.approverOperatorId,
573
+ agentName: context.agentName,
574
+ chatmode: 'interactive',
575
+ replyContext: ownerReplyContext,
576
+ causation: crossPending.causation,
577
+ });
578
+ let sent = false;
579
+ try {
580
+ sent = !!await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), ownerReplyContext);
581
+ }
582
+ catch (error) {
583
+ logger.warn(`[PermissionGateway] Owner approval delivery failed: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
584
+ }
585
+ if (this.crossSessionPending.get(requestId) !== crossPending) {
586
+ return decisionPromise;
587
+ }
588
+ if (!sent) {
589
+ this.expireCrossSessionPermission(sessionId, requestId, 'failed');
590
+ return decisionPromise;
591
+ }
592
+ void this.notifyPermissionState(this.crossSessionPending.get(requestId), {
593
+ state: 'waiting',
594
+ requestId,
595
+ toolName,
596
+ expiresAt,
597
+ });
598
+ return decisionPromise;
599
+ }
600
+ async notifyPermissionState(pending, event) {
601
+ if (!pending?.permissionStateChanged)
602
+ return;
603
+ try {
604
+ await pending.permissionStateChanged(event);
605
+ }
606
+ catch (error) {
607
+ logger.warn(`[PermissionGateway] permission state delivery failed: request=${event.requestId} state=${event.state} error=${error instanceof Error ? error.message : String(error)}`);
608
+ }
609
+ }
610
+ failPendingPermission(sessionId, requestId, reason) {
611
+ const pending = this.pending.get(requestId);
612
+ if (!pending || pending.sessionId !== sessionId)
613
+ return false;
614
+ this.clearPendingResources(pending);
615
+ pending.interactionRouter?.cancel(requestId, reason);
616
+ this.pending.delete(requestId);
617
+ this.syncManagementRevalidationTimer();
618
+ pending.resolve('deny');
619
+ const decisionCausation = deriveCausation(pending.causation);
620
+ recordCausationSpan(decisionCausation, 'permission.decision', {
621
+ status: 'failed',
622
+ refs: { permissionRequestId: requestId, sessionId },
623
+ reason,
624
+ });
625
+ this.eventBus?.publish({
626
+ type: 'permission:resolved',
627
+ sessionId,
628
+ requestId,
629
+ toolName: pending.toolName,
630
+ reason,
631
+ approved: false,
632
+ causation: decisionCausation,
633
+ });
634
+ return true;
635
+ }
636
+ /**
637
+ * 请求人工审批。返回三态决策。
638
+ */
639
+ async requestPermission(sessionId, toolName, toolInput, sendPrompt, context, summary, reason, grantScope = 'default', approverPolicy) {
640
+ const effectiveApproverPolicy = approverPolicy
641
+ ?? context?.approvalRouting?.approverPolicy
642
+ ?? 'requester';
643
+ if (context?.approvalInteractionPolicy === 'deny') {
644
+ const anomaly = {
645
+ code: 'runtime_permission_denied',
646
+ severity: 'warning',
647
+ phase: 'execution',
648
+ occurredAt: Date.now(),
649
+ toolName,
650
+ policy: effectiveApproverPolicy,
651
+ summary: structuredApprovalAuditSummary(toolInput),
652
+ effect: 'operation_skipped',
653
+ };
654
+ try {
655
+ await context.recordExecutionAnomaly?.(anomaly);
656
+ }
657
+ catch (error) {
658
+ logger.warn(`[PermissionGateway] Failed to record unattended denial: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
659
+ }
660
+ logger.info(`[PermissionGateway] Unattended runtime permission denied: session=${sessionId} tool=${toolName} policy=${effectiveApproverPolicy}`);
661
+ return 'deny';
662
+ }
663
+ if (!context?.userId) {
664
+ await sendPrompt('当前操作需要授权,但无法验证申请人身份。');
665
+ return 'deny';
666
+ }
667
+ const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
668
+ if (temporaryGrant) {
669
+ const consumeCausation = deriveCausation(normalizeCausation(temporaryGrant.causation)
670
+ ?? normalizeCausation(context?.causation)
671
+ ?? createRootCausation());
672
+ recordCausationSpan(consumeCausation, 'permission.consume', {
673
+ status: 'completed',
674
+ refs: { taskId: context?.taskId, sessionId },
675
+ reason: 'temporary_grant',
676
+ });
677
+ return 'allow';
678
+ }
679
+ const requestId = `perm-${randomUUID()}`;
680
+ const displaySummary = summary || summarizeToolInput(toolName, toolInput);
681
+ const reasonLine = reason ? `\n原因:${reason}` : '';
682
+ const challenge = {
683
+ id: requestId,
684
+ sessionId,
685
+ toolName,
686
+ toolInput,
687
+ summary: displaySummary,
688
+ reason,
689
+ grantable: true,
690
+ approverPolicy: effectiveApproverPolicy,
691
+ createdAt: Date.now(),
692
+ };
693
+ const requestCausation = deriveCausation(normalizeCausation(context.causation) ?? createRootCausation());
694
+ recordCausationSpan(requestCausation, 'permission.request', {
695
+ status: 'started',
696
+ refs: { permissionRequestId: requestId, taskId: context.taskId, sessionId },
697
+ });
698
+ this.eventBus?.publish({ type: 'permission:requested', sessionId, requestId, toolName, input: displaySummary, causation: requestCausation });
699
+ const route = planApprovalRoute(challenge, context);
700
+ logger.info(`[PermissionGateway] approval_route request=${requestId} policy=${challenge.approverPolicy}`
701
+ + ` route=${route.kind}`
702
+ + ` operator=${route.kind === 'unavailable' ? '<none>' : route.approverOperatorId}`
703
+ + ` channel=${route.kind === 'unavailable' ? '<none>' : (route.approverChannelKey ?? '<legacy>')}`
704
+ + ` principal=${route.kind === 'unavailable' ? '<none>' : (route.approverPrincipalId ?? '<unmapped>')}`
705
+ + `${route.kind === 'unavailable' ? ` reason=${route.reason}` : ''}`);
706
+ if (route.kind === 'unavailable') {
707
+ await sendPrompt(`当前操作需要授权,但审批人不可用(${route.reason})。`);
708
+ const decisionCausation = deriveCausation(requestCausation);
709
+ recordCausationSpan(decisionCausation, 'permission.decision', {
710
+ status: 'failed',
711
+ refs: { permissionRequestId: requestId, sessionId },
712
+ reason: route.reason,
713
+ });
714
+ this.eventBus?.publish({
715
+ type: 'permission:resolved',
716
+ sessionId,
717
+ requestId,
718
+ toolName,
719
+ reason: route.reason,
720
+ approved: false,
721
+ causation: decisionCausation,
722
+ });
723
+ return 'deny';
724
+ }
725
+ if (route.kind === 'handoff') {
726
+ return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation);
727
+ }
728
+ // 构造 ActionInteraction
729
+ const interaction = {
730
+ type: 'interaction',
731
+ id: requestId,
732
+ kind: {
733
+ kind: 'action',
734
+ title: '🔐 权限请求',
735
+ body: `工具:${toolName}\n操作:${displaySummary}${reasonLine}`,
736
+ buttons: [
737
+ { key: 'allow', label: '✅ 允许本次', style: 'primary' },
738
+ { key: 'always', label: '⏱ 同操作 30 分钟', style: 'default' },
739
+ { key: 'deny', label: '❌ 拒绝', style: 'danger' },
740
+ ],
741
+ },
742
+ channelId: context?.channelId || '',
743
+ sessionId,
744
+ initiatorId: route.approverOperatorId,
745
+ expiresAt: Date.now() + LOCAL_APPROVAL_TTL_MS,
746
+ fallback: {
747
+ command: 'perm',
748
+ buttonArgMap: {
749
+ allow: `${requestId} allow`,
750
+ always: `${requestId} always`,
751
+ deny: `${requestId} deny`,
752
+ },
753
+ },
754
+ };
755
+ // Register before delivery so an immediate card/text response cannot beat
756
+ // the pending approval handler. The UUID keeps this pre-delivery slot from
757
+ // being practically guessable.
758
+ const decisionPromise = new Promise((resolve) => {
759
+ const pending = {
760
+ sessionId,
761
+ toolName,
762
+ inputFingerprint: permissionInputFingerprint(toolInput),
763
+ grantScope,
764
+ approverPolicy: challenge.approverPolicy,
765
+ approvalRouteKind: route.kind,
766
+ requiredApproverRole: requiredApproverRole(challenge.approverPolicy, route.kind),
767
+ approverOperatorId: route.approverOperatorId,
768
+ approverChannelKey: route.approverChannelKey,
769
+ approverPrincipalId: route.approverPrincipalId,
770
+ selfAid: context?.approvalRouting?.selfAid,
771
+ originPrincipalId: context?.approvalRouting?.originPrincipalId,
772
+ originAuthRevision: context?.approvalRouting?.originAuthRevision,
773
+ reauthorizeApproval: context?.approvalRouting?.reauthorizeApproval,
774
+ resolve,
775
+ interactionRouter: context?.interactionRouter,
776
+ causation: requestCausation,
777
+ permissionStateChanged: context?.permissionStateChanged,
778
+ };
779
+ pending.timeout = setTimeout(() => {
780
+ this.failPendingPermission(sessionId, requestId, 'expired');
781
+ }, LOCAL_APPROVAL_TTL_MS);
782
+ pending.timeout.unref?.();
783
+ this.pending.set(requestId, pending);
784
+ this.syncManagementRevalidationTimer();
785
+ if (context?.interactionRouter) {
786
+ context.interactionRouter.register(requestId, sessionId, (action, _values, operatorId, channelKey) => {
787
+ return this.resolvePermission(sessionId, requestId, action, operatorId, channelKey);
788
+ }, {
789
+ initiatorId: route.approverOperatorId || undefined,
790
+ initiatorChannelKey: route.approverChannelKey,
791
+ onChannelCancel: reason => this.failPendingPermission(sessionId, requestId, reason),
792
+ onCancel: reason => context?.adapter?.invalidateInteraction?.(requestId, reason),
793
+ fallbackCommand: 'perm',
794
+ });
795
+ }
796
+ });
797
+ const localPending = this.pending.get(requestId);
798
+ if (localPending && this.bindAbortSignal(localPending, context?.abortSignal, () => this.failPendingPermission(sessionId, requestId, 'tool_cancelled'))) {
799
+ return decisionPromise;
800
+ }
801
+ // 尝试富交互(走统一 adapter.send 入口)
802
+ let interactionSent = false;
803
+ if (context?.flushPending) {
804
+ try {
805
+ await context.flushPending();
806
+ }
807
+ catch {
808
+ // flush 失败不应阻断权限请求发送
809
+ }
810
+ }
811
+ if (this.pending.get(requestId) !== localPending) {
812
+ return decisionPromise;
813
+ }
814
+ if (localPending && localPending.requiredApproverRole !== 'requester') {
815
+ const currentAuthorization = this.authorizePendingResponse(localPending, localPending.approverOperatorId, localPending.approverChannelKey);
816
+ if (currentAuthorization.status !== 'authorized') {
817
+ this.failPendingPermission(sessionId, requestId, currentAuthorization.status === 'revoked'
818
+ ? currentAuthorization.reason
819
+ : 'approver_identity_mismatch');
820
+ return decisionPromise;
821
+ }
822
+ }
823
+ if (context?.adapter && context.channelId) {
824
+ try {
825
+ const envelope = buildEnvelope({
826
+ taskId: context.taskId,
827
+ channel: context.channel ?? context.adapter.channelName,
828
+ channelId: context.channelId,
829
+ agentName: context.agentName,
830
+ chatmode: context.chatmode,
831
+ replyContext: context.replyContext,
832
+ causation: requestCausation,
833
+ });
834
+ const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always 同操作授权 30 分钟 / /perm ${requestId} deny 拒绝`;
835
+ const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
836
+ interactionSent = !!result;
837
+ }
838
+ catch (err) {
839
+ // sendInteractionPayload 已内部捕获,但保险起见再 try/catch
840
+ }
841
+ }
842
+ // Delivery can yield while the request is cancelled or resolved. Never
843
+ // emit a stale text fallback after the registered pending slot is gone.
844
+ if (this.pending.get(requestId) !== localPending) {
845
+ return decisionPromise;
846
+ }
847
+ // fallback 到文本
848
+ if (!interactionSent) {
849
+ try {
850
+ await sendPrompt(renderActionAsText(interaction));
851
+ }
852
+ catch (error) {
853
+ logger.warn(`[PermissionGateway] Approval delivery failed: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
854
+ this.failPendingPermission(sessionId, requestId, 'delivery_failed');
855
+ }
856
+ }
857
+ return decisionPromise;
858
+ }
859
+ resolvePermission(sessionId, requestId, decision, operatorId, channelKey) {
860
+ const pending = this.pending.get(requestId);
861
+ if (!pending || pending.sessionId !== sessionId)
862
+ return false;
863
+ const authorization = this.authorizePendingResponse(pending, operatorId, channelKey);
864
+ if (authorization.status === 'identity_mismatch')
865
+ return false;
866
+ if (authorization.status === 'revoked') {
867
+ logger.info(`[PermissionGateway] approval_revoked request=${requestId} policy=${pending.approverPolicy} reason=${authorization.reason}`);
868
+ this.failPendingPermission(sessionId, requestId, authorization.reason);
869
+ return false;
870
+ }
871
+ const normalizedDecision = decision === 'allow' || decision === 'always'
872
+ ? decision
873
+ : 'deny';
874
+ // Legacy "always" now means this exact operation in this session for 30 minutes.
875
+ // The backend receives a one-shot allow. Future identical operations must
876
+ // return through this gateway so the exact fingerprint and TTL are checked.
877
+ this.clearPendingResources(pending);
878
+ pending.interactionRouter?.cancel(requestId);
879
+ pending.resolve(normalizedDecision === 'deny' ? 'deny' : 'allow');
880
+ this.pending.delete(requestId);
881
+ this.syncManagementRevalidationTimer();
882
+ const decisionCausation = deriveCausation(pending.causation);
883
+ if (normalizedDecision === 'always') {
884
+ this.addTemporaryGrant(pending, decisionCausation);
885
+ }
886
+ recordCausationSpan(decisionCausation, 'permission.decision', {
887
+ status: normalizedDecision === 'deny' ? 'failed' : 'completed',
888
+ refs: { permissionRequestId: requestId, sessionId },
889
+ reason: normalizedDecision,
890
+ });
891
+ this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, approved: normalizedDecision !== 'deny', causation: decisionCausation });
892
+ return true;
893
+ }
894
+ /**
895
+ * Resolve an explicitly named request from any channel/session. The request
896
+ * remains bound to its authenticated approver, so knowing a UUID alone is
897
+ * never sufficient to approve it.
898
+ */
899
+ resolvePermissionByRequestId(requestId, decision, operatorId, channelKey) {
900
+ const local = this.pending.get(requestId);
901
+ if (local) {
902
+ return this.resolvePermission(local.sessionId, requestId, decision, operatorId, channelKey);
903
+ }
904
+ const crossSession = this.crossSessionPending.get(requestId);
905
+ if (!crossSession)
906
+ return false;
907
+ const action = decision === 'always'
908
+ ? 'approve_session_30m'
909
+ : decision === 'allow'
910
+ ? 'approve_once'
911
+ : 'deny';
912
+ return this.resolveCrossSessionPermission(crossSession.sessionId, requestId, action, undefined, operatorId, channelKey);
913
+ }
914
+ /**
915
+ * Proactively cancel management approvals after an identity or Agent config
916
+ * write. Callers should invoke this for contact/owners/admins changes; the
917
+ * response path repeats the same check as a fail-closed backstop.
918
+ */
919
+ revalidatePendingApprovals(selfAid, source = 'config_write') {
920
+ let cancelled = 0;
921
+ for (const [requestId, pending] of [...this.pending.entries()]) {
922
+ if (pending.requiredApproverRole === 'requester')
923
+ continue;
924
+ if (selfAid && pending.selfAid !== selfAid)
925
+ continue;
926
+ const authorization = this.authorizePendingResponse(pending, pending.approverOperatorId, pending.approverChannelKey);
927
+ if (authorization.status !== 'revoked')
928
+ continue;
929
+ logger.info(`[PermissionGateway] approval_revoked request=${requestId} policy=${pending.approverPolicy} route=${pending.approvalRouteKind} required_role=${pending.requiredApproverRole} reason=${authorization.reason} source=${source}`);
930
+ if (this.failPendingPermission(pending.sessionId, requestId, authorization.reason))
931
+ cancelled++;
932
+ }
933
+ for (const [requestId, pending] of [...this.crossSessionPending.entries()]) {
934
+ if (pending.requiredApproverRole === 'requester')
935
+ continue;
936
+ if (selfAid && pending.selfAid !== selfAid)
937
+ continue;
938
+ const authorization = this.authorizePendingResponse(pending, pending.approverOperatorId, pending.approverChannelKey);
939
+ if (authorization.status !== 'revoked')
940
+ continue;
941
+ logger.info(`[PermissionGateway] approval_revoked request=${requestId} policy=${pending.approverPolicy} route=${pending.approvalRouteKind} required_role=${pending.requiredApproverRole} reason=${authorization.reason} source=${source}`);
942
+ if (this.expireCrossSessionPermission(pending.sessionId, requestId, authorization.reason))
943
+ cancelled++;
944
+ }
945
+ return cancelled;
946
+ }
947
+ /** 中断时取消指定会话的所有 pending 权限请求 */
948
+ cancelAll(sessionId, reason = 'cancelled', notifyState = true) {
949
+ for (const [requestId, pending] of this.pending.entries()) {
950
+ if (pending.sessionId === sessionId) {
951
+ this.clearPendingResources(pending);
952
+ pending.interactionRouter?.cancel(requestId, reason);
953
+ pending.resolve('deny');
954
+ this.pending.delete(requestId);
955
+ const decisionCausation = deriveCausation(pending.causation);
956
+ recordCausationSpan(decisionCausation, 'permission.decision', {
957
+ status: 'failed',
958
+ refs: { permissionRequestId: requestId, sessionId },
959
+ reason,
960
+ });
961
+ this.eventBus?.publish({
962
+ type: 'permission:cancelled',
963
+ sessionId,
964
+ requestId,
965
+ toolName: pending.toolName,
966
+ reason,
967
+ causation: decisionCausation,
968
+ });
969
+ }
970
+ }
971
+ for (const requestId of [...this.crossSessionPending.keys()]) {
972
+ this.expireCrossSessionPermission(sessionId, requestId, reason, notifyState, true);
973
+ }
974
+ this.syncManagementRevalidationTimer();
975
+ }
976
+ async cancelAllPending(reason = 'daemon_restart') {
977
+ const crossSession = [...this.crossSessionPending.values()];
978
+ for (const pending of crossSession)
979
+ void this.notifyPermissionState(pending, {
980
+ state: 'failed',
981
+ requestId: pending.requestId,
982
+ toolName: pending.toolName,
983
+ reason,
984
+ message: reason === 'daemon_restart'
985
+ ? 'daemon 已重启,审批任务已终止。'
986
+ : '审批任务已终止。',
987
+ });
988
+ const sessionIds = new Set([
989
+ ...[...this.pending.values()].map(pending => pending.sessionId),
990
+ ...crossSession.map(pending => pending.sessionId),
991
+ ]);
992
+ for (const sessionId of sessionIds)
993
+ this.cancelAll(sessionId, reason, false);
994
+ }
995
+ /** 获取指定会话的所有 pending requestId */
996
+ getPendingRequests(sessionId) {
997
+ const ids = [];
998
+ for (const [requestId, pending] of this.pending.entries()) {
999
+ if (pending.sessionId === sessionId) {
1000
+ ids.push(requestId);
1001
+ }
1002
+ }
1003
+ for (const [requestId, pending] of this.crossSessionPending.entries()) {
1004
+ if (pending.sessionId === sessionId) {
1005
+ ids.push(requestId);
1006
+ }
1007
+ }
1008
+ return ids;
1009
+ }
1010
+ }
1011
+ function permissionFailure(reason) {
1012
+ if (reason === 'expired')
1013
+ return 'owner 审批超时,任务已停止。';
1014
+ if (reason === 'cancelled')
1015
+ return 'owner 审批已取消,任务已停止。';
1016
+ return '向 owner 发送授权申请失败,任务已停止。';
1017
+ }