botmux 3.14.0 → 3.16.0

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 (812) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/.runtime-build-id +1 -1
  4. package/dist/adapters/backend/destroy-result.d.ts +159 -0
  5. package/dist/adapters/backend/destroy-result.d.ts.map +1 -0
  6. package/dist/adapters/backend/destroy-result.js +223 -0
  7. package/dist/adapters/backend/destroy-result.js.map +1 -0
  8. package/dist/adapters/backend/herdr-backend.d.ts +4 -0
  9. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  10. package/dist/adapters/backend/herdr-backend.js +23 -1
  11. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  12. package/dist/adapters/backend/mojo-backend.d.ts +577 -0
  13. package/dist/adapters/backend/mojo-backend.d.ts.map +1 -0
  14. package/dist/adapters/backend/mojo-backend.js +2471 -0
  15. package/dist/adapters/backend/mojo-backend.js.map +1 -0
  16. package/dist/adapters/backend/mojo-budgets.d.ts +21 -0
  17. package/dist/adapters/backend/mojo-budgets.d.ts.map +1 -0
  18. package/dist/adapters/backend/mojo-budgets.js +22 -0
  19. package/dist/adapters/backend/mojo-budgets.js.map +1 -0
  20. package/dist/adapters/backend/mojo-isolated-workspace.d.ts +69 -0
  21. package/dist/adapters/backend/mojo-isolated-workspace.d.ts.map +1 -0
  22. package/dist/adapters/backend/mojo-isolated-workspace.js +213 -0
  23. package/dist/adapters/backend/mojo-isolated-workspace.js.map +1 -0
  24. package/dist/adapters/backend/mojo-process-tree.d.ts +292 -0
  25. package/dist/adapters/backend/mojo-process-tree.d.ts.map +1 -0
  26. package/dist/adapters/backend/mojo-process-tree.js +486 -0
  27. package/dist/adapters/backend/mojo-process-tree.js.map +1 -0
  28. package/dist/adapters/backend/mojo-types.d.ts +542 -0
  29. package/dist/adapters/backend/mojo-types.d.ts.map +1 -0
  30. package/dist/adapters/backend/mojo-types.js +682 -0
  31. package/dist/adapters/backend/mojo-types.js.map +1 -0
  32. package/dist/adapters/backend/reproduce-command.js +3 -3
  33. package/dist/adapters/backend/reproduce-command.js.map +1 -1
  34. package/dist/adapters/backend/riff-backend.js +1 -1
  35. package/dist/adapters/backend/riff-backend.js.map +1 -1
  36. package/dist/adapters/backend/sandbox.d.ts +44 -7
  37. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  38. package/dist/adapters/backend/sandbox.js +30 -7
  39. package/dist/adapters/backend/sandbox.js.map +1 -1
  40. package/dist/adapters/backend/session-backend-selector.d.ts +14 -1
  41. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  42. package/dist/adapters/backend/session-backend-selector.js +82 -10
  43. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  44. package/dist/adapters/backend/tmux-pipe-backend.d.ts +30 -0
  45. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  46. package/dist/adapters/backend/tmux-pipe-backend.js +60 -0
  47. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  48. package/dist/adapters/backend/types.d.ts +108 -6
  49. package/dist/adapters/backend/types.d.ts.map +1 -1
  50. package/dist/adapters/backend/types.js.map +1 -1
  51. package/dist/adapters/backend/zmx-backend.d.ts +11 -0
  52. package/dist/adapters/backend/zmx-backend.d.ts.map +1 -1
  53. package/dist/adapters/backend/zmx-backend.js +16 -1
  54. package/dist/adapters/backend/zmx-backend.js.map +1 -1
  55. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  56. package/dist/adapters/cli/codex-app.js +2 -2
  57. package/dist/adapters/cli/codex-app.js.map +1 -1
  58. package/dist/adapters/cli/codex.d.ts.map +1 -1
  59. package/dist/adapters/cli/codex.js +7 -0
  60. package/dist/adapters/cli/codex.js.map +1 -1
  61. package/dist/adapters/cli/copilot.d.ts +3 -1
  62. package/dist/adapters/cli/copilot.d.ts.map +1 -1
  63. package/dist/adapters/cli/copilot.js +23 -5
  64. package/dist/adapters/cli/copilot.js.map +1 -1
  65. package/dist/adapters/cli/cursor.d.ts.map +1 -1
  66. package/dist/adapters/cli/cursor.js +26 -4
  67. package/dist/adapters/cli/cursor.js.map +1 -1
  68. package/dist/adapters/cli/dsh.d.ts.map +1 -1
  69. package/dist/adapters/cli/dsh.js +5 -1
  70. package/dist/adapters/cli/dsh.js.map +1 -1
  71. package/dist/adapters/cli/grok.d.ts.map +1 -1
  72. package/dist/adapters/cli/grok.js +24 -5
  73. package/dist/adapters/cli/grok.js.map +1 -1
  74. package/dist/adapters/cli/kimi.d.ts.map +1 -1
  75. package/dist/adapters/cli/kimi.js +19 -1
  76. package/dist/adapters/cli/kimi.js.map +1 -1
  77. package/dist/adapters/cli/mojo.d.ts +28 -0
  78. package/dist/adapters/cli/mojo.d.ts.map +1 -0
  79. package/dist/adapters/cli/mojo.js +58 -0
  80. package/dist/adapters/cli/mojo.js.map +1 -0
  81. package/dist/adapters/cli/pi.d.ts +20 -5
  82. package/dist/adapters/cli/pi.d.ts.map +1 -1
  83. package/dist/adapters/cli/pi.js +27 -5
  84. package/dist/adapters/cli/pi.js.map +1 -1
  85. package/dist/adapters/cli/read-isolation.d.ts +251 -1
  86. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  87. package/dist/adapters/cli/read-isolation.js +254 -1
  88. package/dist/adapters/cli/read-isolation.js.map +1 -1
  89. package/dist/adapters/cli/registry.d.ts +13 -1
  90. package/dist/adapters/cli/registry.d.ts.map +1 -1
  91. package/dist/adapters/cli/registry.js +19 -1
  92. package/dist/adapters/cli/registry.js.map +1 -1
  93. package/dist/adapters/cli/runner-input.d.ts +3 -3
  94. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  95. package/dist/adapters/cli/runner-input.js +4 -3
  96. package/dist/adapters/cli/runner-input.js.map +1 -1
  97. package/dist/adapters/cli/shared-hints.d.ts +3 -1
  98. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  99. package/dist/adapters/cli/shared-hints.js +51 -30
  100. package/dist/adapters/cli/shared-hints.js.map +1 -1
  101. package/dist/adapters/cli/types.d.ts +33 -4
  102. package/dist/adapters/cli/types.d.ts.map +1 -1
  103. package/dist/bot-registry.d.ts +42 -0
  104. package/dist/bot-registry.d.ts.map +1 -1
  105. package/dist/bot-registry.js +54 -4
  106. package/dist/bot-registry.js.map +1 -1
  107. package/dist/cli/arg-utils.d.ts +5 -0
  108. package/dist/cli/arg-utils.d.ts.map +1 -1
  109. package/dist/cli/arg-utils.js +7 -0
  110. package/dist/cli/arg-utils.js.map +1 -1
  111. package/dist/cli/bots-list-output.d.ts +130 -2
  112. package/dist/cli/bots-list-output.d.ts.map +1 -1
  113. package/dist/cli/bots-list-output.js +173 -27
  114. package/dist/cli/bots-list-output.js.map +1 -1
  115. package/dist/cli/daemon-lifecycle-env.d.ts +10 -3
  116. package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
  117. package/dist/cli/daemon-lifecycle-env.js +38 -11
  118. package/dist/cli/daemon-lifecycle-env.js.map +1 -1
  119. package/dist/cli/dashboard-command.d.ts +15 -0
  120. package/dist/cli/dashboard-command.d.ts.map +1 -1
  121. package/dist/cli/dashboard-command.js +22 -0
  122. package/dist/cli/dashboard-command.js.map +1 -1
  123. package/dist/cli/log-tail.d.ts +54 -0
  124. package/dist/cli/log-tail.d.ts.map +1 -0
  125. package/dist/cli/log-tail.js +167 -0
  126. package/dist/cli/log-tail.js.map +1 -0
  127. package/dist/cli/pm2-env.d.ts +16 -0
  128. package/dist/cli/pm2-env.d.ts.map +1 -0
  129. package/dist/cli/pm2-env.js +63 -0
  130. package/dist/cli/pm2-env.js.map +1 -0
  131. package/dist/cli/pm2-fleet-lock.d.ts +26 -0
  132. package/dist/cli/pm2-fleet-lock.d.ts.map +1 -0
  133. package/dist/cli/pm2-fleet-lock.js +50 -0
  134. package/dist/cli/pm2-fleet-lock.js.map +1 -0
  135. package/dist/cli/pm2-god-retirement.d.ts +59 -0
  136. package/dist/cli/pm2-god-retirement.d.ts.map +1 -0
  137. package/dist/cli/pm2-god-retirement.js +95 -0
  138. package/dist/cli/pm2-god-retirement.js.map +1 -0
  139. package/dist/cli/session-list-wake.d.ts +41 -0
  140. package/dist/cli/session-list-wake.d.ts.map +1 -0
  141. package/dist/cli/session-list-wake.js +102 -0
  142. package/dist/cli/session-list-wake.js.map +1 -0
  143. package/dist/cli/workflow.d.ts.map +1 -1
  144. package/dist/cli/workflow.js +26 -0
  145. package/dist/cli/workflow.js.map +1 -1
  146. package/dist/cli.d.ts.map +1 -1
  147. package/dist/cli.js +941 -118
  148. package/dist/cli.js.map +1 -1
  149. package/dist/codex-app-runner.js +261 -49
  150. package/dist/codex-app-runner.js.map +1 -1
  151. package/dist/core/cli-runtime-update.d.ts +8 -0
  152. package/dist/core/cli-runtime-update.d.ts.map +1 -1
  153. package/dist/core/cli-runtime-update.js +127 -8
  154. package/dist/core/cli-runtime-update.js.map +1 -1
  155. package/dist/core/close-residual.d.ts +64 -0
  156. package/dist/core/close-residual.d.ts.map +1 -0
  157. package/dist/core/close-residual.js +93 -0
  158. package/dist/core/close-residual.js.map +1 -0
  159. package/dist/core/closed-session-card.d.ts.map +1 -1
  160. package/dist/core/closed-session-card.js +11 -3
  161. package/dist/core/closed-session-card.js.map +1 -1
  162. package/dist/core/command-discovery.d.ts.map +1 -1
  163. package/dist/core/command-discovery.js +4 -1
  164. package/dist/core/command-discovery.js.map +1 -1
  165. package/dist/core/command-handler.d.ts.map +1 -1
  166. package/dist/core/command-handler.js +129 -27
  167. package/dist/core/command-handler.js.map +1 -1
  168. package/dist/core/cost-calculator.d.ts +3 -1
  169. package/dist/core/cost-calculator.d.ts.map +1 -1
  170. package/dist/core/cost-calculator.js +230 -1
  171. package/dist/core/cost-calculator.js.map +1 -1
  172. package/dist/core/daemon-background-close.d.ts +19 -0
  173. package/dist/core/daemon-background-close.d.ts.map +1 -0
  174. package/dist/core/daemon-background-close.js +39 -0
  175. package/dist/core/daemon-background-close.js.map +1 -0
  176. package/dist/core/dashboard-command/overview.d.ts +4 -0
  177. package/dist/core/dashboard-command/overview.d.ts.map +1 -1
  178. package/dist/core/dashboard-command/overview.js +8 -1
  179. package/dist/core/dashboard-command/overview.js.map +1 -1
  180. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  181. package/dist/core/dashboard-ipc-server.js +415 -42
  182. package/dist/core/dashboard-ipc-server.js.map +1 -1
  183. package/dist/core/dashboard-rows.d.ts +4 -0
  184. package/dist/core/dashboard-rows.d.ts.map +1 -1
  185. package/dist/core/dashboard-rows.js +3 -0
  186. package/dist/core/dashboard-rows.js.map +1 -1
  187. package/dist/core/dashboard-url.d.ts +37 -0
  188. package/dist/core/dashboard-url.d.ts.map +1 -1
  189. package/dist/core/dashboard-url.js +71 -0
  190. package/dist/core/dashboard-url.js.map +1 -1
  191. package/dist/core/deferred-schedule-settlement.d.ts +15 -1
  192. package/dist/core/deferred-schedule-settlement.d.ts.map +1 -1
  193. package/dist/core/deferred-schedule-settlement.js +4 -1
  194. package/dist/core/deferred-schedule-settlement.js.map +1 -1
  195. package/dist/core/device-isolation-daemon.d.ts +91 -1
  196. package/dist/core/device-isolation-daemon.d.ts.map +1 -1
  197. package/dist/core/device-isolation-daemon.js +346 -2
  198. package/dist/core/device-isolation-daemon.js.map +1 -1
  199. package/dist/core/local-terminal-opener.d.ts.map +1 -1
  200. package/dist/core/local-terminal-opener.js +5 -1
  201. package/dist/core/local-terminal-opener.js.map +1 -1
  202. package/dist/core/loopback-target.d.ts +14 -0
  203. package/dist/core/loopback-target.d.ts.map +1 -0
  204. package/dist/core/loopback-target.js +20 -0
  205. package/dist/core/loopback-target.js.map +1 -0
  206. package/dist/core/maintenance.d.ts.map +1 -1
  207. package/dist/core/maintenance.js +16 -2
  208. package/dist/core/maintenance.js.map +1 -1
  209. package/dist/core/mojo-containment-command.d.ts +20 -0
  210. package/dist/core/mojo-containment-command.d.ts.map +1 -0
  211. package/dist/core/mojo-containment-command.js +266 -0
  212. package/dist/core/mojo-containment-command.js.map +1 -0
  213. package/dist/core/mojo-containment.d.ts +575 -0
  214. package/dist/core/mojo-containment.d.ts.map +1 -0
  215. package/dist/core/mojo-containment.js +1158 -0
  216. package/dist/core/mojo-containment.js.map +1 -0
  217. package/dist/core/mojo-launcher-env-quarantine.d.ts +30 -0
  218. package/dist/core/mojo-launcher-env-quarantine.d.ts.map +1 -0
  219. package/dist/core/mojo-launcher-env-quarantine.js +175 -0
  220. package/dist/core/mojo-launcher-env-quarantine.js.map +1 -0
  221. package/dist/core/mojo-session-identity.d.ts +3 -0
  222. package/dist/core/mojo-session-identity.d.ts.map +1 -0
  223. package/dist/core/mojo-session-identity.js +126 -0
  224. package/dist/core/mojo-session-identity.js.map +1 -0
  225. package/dist/core/persistent-backend.d.ts +32 -8
  226. package/dist/core/persistent-backend.d.ts.map +1 -1
  227. package/dist/core/persistent-backend.js +55 -17
  228. package/dist/core/persistent-backend.js.map +1 -1
  229. package/dist/core/plugins/mcp/gateway.d.ts +14 -1
  230. package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
  231. package/dist/core/plugins/mcp/gateway.js +70 -3
  232. package/dist/core/plugins/mcp/gateway.js.map +1 -1
  233. package/dist/core/plugins/mcp/host.d.ts +2 -0
  234. package/dist/core/plugins/mcp/host.d.ts.map +1 -1
  235. package/dist/core/plugins/mcp/host.js +2 -0
  236. package/dist/core/plugins/mcp/host.js.map +1 -1
  237. package/dist/core/plugins/pm2.d.ts.map +1 -1
  238. package/dist/core/plugins/pm2.js +16 -3
  239. package/dist/core/plugins/pm2.js.map +1 -1
  240. package/dist/core/plugins/service-manager.d.ts +9 -0
  241. package/dist/core/plugins/service-manager.d.ts.map +1 -1
  242. package/dist/core/plugins/service-manager.js +33 -16
  243. package/dist/core/plugins/service-manager.js.map +1 -1
  244. package/dist/core/preview-port-owner.d.ts +109 -0
  245. package/dist/core/preview-port-owner.d.ts.map +1 -0
  246. package/dist/core/preview-port-owner.js +320 -0
  247. package/dist/core/preview-port-owner.js.map +1 -0
  248. package/dist/core/remote-cli-ids.d.ts +5 -0
  249. package/dist/core/remote-cli-ids.d.ts.map +1 -0
  250. package/dist/core/remote-cli-ids.js +24 -0
  251. package/dist/core/remote-cli-ids.js.map +1 -0
  252. package/dist/core/{riff-shutdown-detach.d.ts → remote-shutdown-detach.d.ts} +40 -39
  253. package/dist/core/remote-shutdown-detach.d.ts.map +1 -0
  254. package/dist/core/{riff-shutdown-detach.js → remote-shutdown-detach.js} +85 -83
  255. package/dist/core/remote-shutdown-detach.js.map +1 -0
  256. package/dist/core/{riff-worker-shutdown-readiness.d.ts → remote-worker-shutdown-readiness.d.ts} +5 -4
  257. package/dist/core/remote-worker-shutdown-readiness.d.ts.map +1 -0
  258. package/dist/core/{riff-worker-shutdown-readiness.js → remote-worker-shutdown-readiness.js} +4 -3
  259. package/dist/core/remote-worker-shutdown-readiness.js.map +1 -0
  260. package/dist/core/reply-target.d.ts +16 -1
  261. package/dist/core/reply-target.d.ts.map +1 -1
  262. package/dist/core/reply-target.js +61 -0
  263. package/dist/core/reply-target.js.map +1 -1
  264. package/dist/core/session-group-birth.d.ts +26 -0
  265. package/dist/core/session-group-birth.d.ts.map +1 -1
  266. package/dist/core/session-group-birth.js +81 -4
  267. package/dist/core/session-group-birth.js.map +1 -1
  268. package/dist/core/session-manager.d.ts +15 -2
  269. package/dist/core/session-manager.d.ts.map +1 -1
  270. package/dist/core/session-manager.js +259 -5
  271. package/dist/core/session-manager.js.map +1 -1
  272. package/dist/core/session-model.d.ts +46 -0
  273. package/dist/core/session-model.d.ts.map +1 -0
  274. package/dist/core/session-model.js +42 -0
  275. package/dist/core/session-model.js.map +1 -0
  276. package/dist/core/session-owner-reminder.d.ts +63 -0
  277. package/dist/core/session-owner-reminder.d.ts.map +1 -0
  278. package/dist/core/session-owner-reminder.js +196 -0
  279. package/dist/core/session-owner-reminder.js.map +1 -0
  280. package/dist/core/session-preview-registry.d.ts +53 -0
  281. package/dist/core/session-preview-registry.d.ts.map +1 -0
  282. package/dist/core/session-preview-registry.js +82 -0
  283. package/dist/core/session-preview-registry.js.map +1 -0
  284. package/dist/core/session-preview.d.ts +114 -0
  285. package/dist/core/session-preview.d.ts.map +1 -0
  286. package/dist/core/session-preview.js +184 -0
  287. package/dist/core/session-preview.js.map +1 -0
  288. package/dist/core/session-rename.d.ts.map +1 -1
  289. package/dist/core/session-rename.js +5 -3
  290. package/dist/core/session-rename.js.map +1 -1
  291. package/dist/core/session-wake-deadline.d.ts +5 -0
  292. package/dist/core/session-wake-deadline.d.ts.map +1 -0
  293. package/dist/core/session-wake-deadline.js +13 -0
  294. package/dist/core/session-wake-deadline.js.map +1 -0
  295. package/dist/core/shutdown-budgets.d.ts +7 -6
  296. package/dist/core/shutdown-budgets.d.ts.map +1 -1
  297. package/dist/core/shutdown-budgets.js +11 -10
  298. package/dist/core/shutdown-budgets.js.map +1 -1
  299. package/dist/core/skills/frontmatter.d.ts.map +1 -1
  300. package/dist/core/skills/frontmatter.js +6 -1
  301. package/dist/core/skills/frontmatter.js.map +1 -1
  302. package/dist/core/terminal-control-grant.d.ts +74 -0
  303. package/dist/core/terminal-control-grant.d.ts.map +1 -0
  304. package/dist/core/terminal-control-grant.js +183 -0
  305. package/dist/core/terminal-control-grant.js.map +1 -0
  306. package/dist/core/terminal-proxy.d.ts.map +1 -1
  307. package/dist/core/terminal-proxy.js +4 -5
  308. package/dist/core/terminal-proxy.js.map +1 -1
  309. package/dist/core/terminal-write-auth.d.ts +4 -11
  310. package/dist/core/terminal-write-auth.d.ts.map +1 -1
  311. package/dist/core/terminal-write-auth.js +19 -16
  312. package/dist/core/terminal-write-auth.js.map +1 -1
  313. package/dist/core/trigger-session.d.ts.map +1 -1
  314. package/dist/core/trigger-session.js +36 -29
  315. package/dist/core/trigger-session.js.map +1 -1
  316. package/dist/core/types.d.ts +88 -10
  317. package/dist/core/types.d.ts.map +1 -1
  318. package/dist/core/types.js +9 -7
  319. package/dist/core/types.js.map +1 -1
  320. package/dist/core/workbench-link.d.ts +81 -0
  321. package/dist/core/workbench-link.d.ts.map +1 -0
  322. package/dist/core/workbench-link.js +109 -0
  323. package/dist/core/workbench-link.js.map +1 -0
  324. package/dist/core/worker-pool.d.ts +312 -11
  325. package/dist/core/worker-pool.d.ts.map +1 -1
  326. package/dist/core/worker-pool.js +2515 -217
  327. package/dist/core/worker-pool.js.map +1 -1
  328. package/dist/daemon.d.ts +6 -0
  329. package/dist/daemon.d.ts.map +1 -1
  330. package/dist/daemon.js +447 -116
  331. package/dist/daemon.js.map +1 -1
  332. package/dist/dashboard/aggregator.d.ts +33 -3
  333. package/dist/dashboard/aggregator.d.ts.map +1 -1
  334. package/dist/dashboard/aggregator.js +93 -17
  335. package/dist/dashboard/aggregator.js.map +1 -1
  336. package/dist/dashboard/auth-session-connections.d.ts +59 -0
  337. package/dist/dashboard/auth-session-connections.d.ts.map +1 -0
  338. package/dist/dashboard/auth-session-connections.js +109 -0
  339. package/dist/dashboard/auth-session-connections.js.map +1 -0
  340. package/dist/dashboard/auth.d.ts +99 -1
  341. package/dist/dashboard/auth.d.ts.map +1 -1
  342. package/dist/dashboard/auth.js +176 -64
  343. package/dist/dashboard/auth.js.map +1 -1
  344. package/dist/dashboard/bot-payload.d.ts +5 -0
  345. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  346. package/dist/dashboard/bot-payload.js +4 -0
  347. package/dist/dashboard/bot-payload.js.map +1 -1
  348. package/dist/dashboard/control-audit.d.ts +112 -0
  349. package/dist/dashboard/control-audit.d.ts.map +1 -0
  350. package/dist/dashboard/control-audit.js +195 -0
  351. package/dist/dashboard/control-audit.js.map +1 -0
  352. package/dist/dashboard/control-csrf.d.ts +123 -0
  353. package/dist/dashboard/control-csrf.d.ts.map +1 -0
  354. package/dist/dashboard/control-csrf.js +392 -0
  355. package/dist/dashboard/control-csrf.js.map +1 -0
  356. package/dist/dashboard/daemon-reconcile.d.ts +26 -0
  357. package/dist/dashboard/daemon-reconcile.d.ts.map +1 -0
  358. package/dist/dashboard/daemon-reconcile.js +46 -0
  359. package/dist/dashboard/daemon-reconcile.js.map +1 -0
  360. package/dist/dashboard/debug-terminal.d.ts.map +1 -1
  361. package/dist/dashboard/debug-terminal.js +18 -4
  362. package/dist/dashboard/debug-terminal.js.map +1 -1
  363. package/dist/dashboard/events-sse.d.ts +60 -0
  364. package/dist/dashboard/events-sse.d.ts.map +1 -0
  365. package/dist/dashboard/events-sse.js +63 -0
  366. package/dist/dashboard/events-sse.js.map +1 -0
  367. package/dist/dashboard/h5-auth.d.ts +256 -0
  368. package/dist/dashboard/h5-auth.d.ts.map +1 -0
  369. package/dist/dashboard/h5-auth.js +825 -0
  370. package/dist/dashboard/h5-auth.js.map +1 -0
  371. package/dist/dashboard/managed-spawn.d.ts +36 -0
  372. package/dist/dashboard/managed-spawn.d.ts.map +1 -0
  373. package/dist/dashboard/managed-spawn.js +169 -0
  374. package/dist/dashboard/managed-spawn.js.map +1 -0
  375. package/dist/dashboard/preview-content-capability.d.ts +41 -0
  376. package/dist/dashboard/preview-content-capability.d.ts.map +1 -0
  377. package/dist/dashboard/preview-content-capability.js +127 -0
  378. package/dist/dashboard/preview-content-capability.js.map +1 -0
  379. package/dist/dashboard/preview-contract.d.ts +85 -0
  380. package/dist/dashboard/preview-contract.d.ts.map +1 -0
  381. package/dist/dashboard/preview-contract.js +198 -0
  382. package/dist/dashboard/preview-contract.js.map +1 -0
  383. package/dist/dashboard/preview-guard-page.d.ts +52 -0
  384. package/dist/dashboard/preview-guard-page.d.ts.map +1 -0
  385. package/dist/dashboard/preview-guard-page.js +162 -0
  386. package/dist/dashboard/preview-guard-page.js.map +1 -0
  387. package/dist/dashboard/preview-interaction.d.ts +57 -0
  388. package/dist/dashboard/preview-interaction.d.ts.map +1 -0
  389. package/dist/dashboard/preview-interaction.js +198 -0
  390. package/dist/dashboard/preview-interaction.js.map +1 -0
  391. package/dist/dashboard/preview-proxy.d.ts +113 -0
  392. package/dist/dashboard/preview-proxy.d.ts.map +1 -0
  393. package/dist/dashboard/preview-proxy.js +543 -0
  394. package/dist/dashboard/preview-proxy.js.map +1 -0
  395. package/dist/dashboard/public-redact.d.ts +23 -0
  396. package/dist/dashboard/public-redact.d.ts.map +1 -1
  397. package/dist/dashboard/public-redact.js +90 -21
  398. package/dist/dashboard/public-redact.js.map +1 -1
  399. package/dist/dashboard/request-identity.d.ts +77 -0
  400. package/dist/dashboard/request-identity.d.ts.map +1 -0
  401. package/dist/dashboard/request-identity.js +72 -0
  402. package/dist/dashboard/request-identity.js.map +1 -0
  403. package/dist/dashboard/session-cleanup.d.ts +5 -0
  404. package/dist/dashboard/session-cleanup.d.ts.map +1 -1
  405. package/dist/dashboard/session-cleanup.js +4 -0
  406. package/dist/dashboard/session-cleanup.js.map +1 -1
  407. package/dist/dashboard/settings-write-applier.d.ts +5 -1
  408. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  409. package/dist/dashboard/settings-write-applier.js +14 -0
  410. package/dist/dashboard/settings-write-applier.js.map +1 -1
  411. package/dist/dashboard/standing-link.d.ts +76 -0
  412. package/dist/dashboard/standing-link.d.ts.map +1 -0
  413. package/dist/dashboard/standing-link.js +111 -0
  414. package/dist/dashboard/standing-link.js.map +1 -0
  415. package/dist/dashboard/terminal-control.d.ts +105 -0
  416. package/dist/dashboard/terminal-control.d.ts.map +1 -0
  417. package/dist/dashboard/terminal-control.js +296 -0
  418. package/dist/dashboard/terminal-control.js.map +1 -0
  419. package/dist/dashboard/terminal-front-proxy.d.ts +55 -0
  420. package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -0
  421. package/dist/dashboard/terminal-front-proxy.js +397 -0
  422. package/dist/dashboard/terminal-front-proxy.js.map +1 -0
  423. package/dist/dashboard/terminal-view-capability.d.ts +97 -0
  424. package/dist/dashboard/terminal-view-capability.d.ts.map +1 -0
  425. package/dist/dashboard/terminal-view-capability.js +145 -0
  426. package/dist/dashboard/terminal-view-capability.js.map +1 -0
  427. package/dist/dashboard/web/agent-workbench-api.d.ts +59 -0
  428. package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -0
  429. package/dist/dashboard/web/agent-workbench-api.js +227 -0
  430. package/dist/dashboard/web/agent-workbench-api.js.map +1 -0
  431. package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts +59 -0
  432. package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts.map +1 -0
  433. package/dist/dashboard/web/agent-workbench-appearance-menu.js +195 -0
  434. package/dist/dashboard/web/agent-workbench-appearance-menu.js.map +1 -0
  435. package/dist/dashboard/web/agent-workbench-appearance.d.ts +222 -0
  436. package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -0
  437. package/dist/dashboard/web/agent-workbench-appearance.js +499 -0
  438. package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -0
  439. package/dist/dashboard/web/agent-workbench-capabilities.d.ts +13 -0
  440. package/dist/dashboard/web/agent-workbench-capabilities.d.ts.map +1 -0
  441. package/dist/dashboard/web/agent-workbench-capabilities.js +24 -0
  442. package/dist/dashboard/web/agent-workbench-capabilities.js.map +1 -0
  443. package/dist/dashboard/web/agent-workbench-chat.d.ts +90 -0
  444. package/dist/dashboard/web/agent-workbench-chat.d.ts.map +1 -0
  445. package/dist/dashboard/web/agent-workbench-chat.js +168 -0
  446. package/dist/dashboard/web/agent-workbench-chat.js.map +1 -0
  447. package/dist/dashboard/web/agent-workbench-dock-page.d.ts +3 -0
  448. package/dist/dashboard/web/agent-workbench-dock-page.d.ts.map +1 -0
  449. package/dist/dashboard/web/agent-workbench-dock-page.js +23 -0
  450. package/dist/dashboard/web/agent-workbench-dock-page.js.map +1 -0
  451. package/dist/dashboard/web/agent-workbench-dock-view.d.ts +22 -0
  452. package/dist/dashboard/web/agent-workbench-dock-view.d.ts.map +1 -0
  453. package/dist/dashboard/web/agent-workbench-dock-view.js +76 -0
  454. package/dist/dashboard/web/agent-workbench-dock-view.js.map +1 -0
  455. package/dist/dashboard/web/agent-workbench-model.d.ts +196 -0
  456. package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -0
  457. package/dist/dashboard/web/agent-workbench-model.js +529 -0
  458. package/dist/dashboard/web/agent-workbench-model.js.map +1 -0
  459. package/dist/dashboard/web/agent-workbench-page.d.ts +3 -0
  460. package/dist/dashboard/web/agent-workbench-page.d.ts.map +1 -0
  461. package/dist/dashboard/web/agent-workbench-page.js +27 -0
  462. package/dist/dashboard/web/agent-workbench-page.js.map +1 -0
  463. package/dist/dashboard/web/agent-workbench-panes.d.ts +49 -0
  464. package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -0
  465. package/dist/dashboard/web/agent-workbench-panes.js +468 -0
  466. package/dist/dashboard/web/agent-workbench-panes.js.map +1 -0
  467. package/dist/dashboard/web/agent-workbench-session-list.d.ts +42 -0
  468. package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -0
  469. package/dist/dashboard/web/agent-workbench-session-list.js +353 -0
  470. package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -0
  471. package/dist/dashboard/web/agent-workbench-storage.d.ts +49 -0
  472. package/dist/dashboard/web/agent-workbench-storage.d.ts.map +1 -0
  473. package/dist/dashboard/web/agent-workbench-storage.js +197 -0
  474. package/dist/dashboard/web/agent-workbench-storage.js.map +1 -0
  475. package/dist/dashboard/web/agent-workbench-touch.d.ts +46 -0
  476. package/dist/dashboard/web/agent-workbench-touch.d.ts.map +1 -0
  477. package/dist/dashboard/web/agent-workbench-touch.js +96 -0
  478. package/dist/dashboard/web/agent-workbench-touch.js.map +1 -0
  479. package/dist/dashboard/web/agent-workbench-view.d.ts +34 -0
  480. package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -0
  481. package/dist/dashboard/web/agent-workbench-view.js +439 -0
  482. package/dist/dashboard/web/agent-workbench-view.js.map +1 -0
  483. package/dist/dashboard/web/app.d.ts.map +1 -1
  484. package/dist/dashboard/web/app.js +63 -2
  485. package/dist/dashboard/web/app.js.map +1 -1
  486. package/dist/dashboard/web/bot-defaults-page.d.ts +18 -0
  487. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  488. package/dist/dashboard/web/bot-defaults-page.js +362 -36
  489. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  490. package/dist/dashboard/web/bot-defaults.d.ts +8 -0
  491. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  492. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  493. package/dist/dashboard/web/control-csrf.d.ts +4 -0
  494. package/dist/dashboard/web/control-csrf.d.ts.map +1 -0
  495. package/dist/dashboard/web/control-csrf.js +26 -0
  496. package/dist/dashboard/web/control-csrf.js.map +1 -0
  497. package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
  498. package/dist/dashboard/web/dashboard-routes.js +2 -0
  499. package/dist/dashboard/web/dashboard-routes.js.map +1 -1
  500. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  501. package/dist/dashboard/web/groups-page.js +11 -2
  502. package/dist/dashboard/web/groups-page.js.map +1 -1
  503. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  504. package/dist/dashboard/web/i18n.js +140 -6
  505. package/dist/dashboard/web/i18n.js.map +1 -1
  506. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  507. package/dist/dashboard/web/overview-page.js +3 -2
  508. package/dist/dashboard/web/overview-page.js.map +1 -1
  509. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  510. package/dist/dashboard/web/sessions-page.js +41 -1
  511. package/dist/dashboard/web/sessions-page.js.map +1 -1
  512. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  513. package/dist/dashboard/web/sessions.js +5 -1
  514. package/dist/dashboard/web/sessions.js.map +1 -1
  515. package/dist/dashboard/web/settings-page.d.ts +3 -0
  516. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  517. package/dist/dashboard/web/settings-page.js +56 -10
  518. package/dist/dashboard/web/settings-page.js.map +1 -1
  519. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  520. package/dist/dashboard/web/skills-page.js +4 -2
  521. package/dist/dashboard/web/skills-page.js.map +1 -1
  522. package/dist/dashboard/web/store.d.ts +27 -2
  523. package/dist/dashboard/web/store.d.ts.map +1 -1
  524. package/dist/dashboard/web/store.js +43 -4
  525. package/dist/dashboard/web/store.js.map +1 -1
  526. package/dist/dashboard/web/ui.d.ts +3 -0
  527. package/dist/dashboard/web/ui.d.ts.map +1 -1
  528. package/dist/dashboard/web/ui.js +11 -0
  529. package/dist/dashboard/web/ui.js.map +1 -1
  530. package/dist/dashboard/web/update-action.d.ts +12 -0
  531. package/dist/dashboard/web/update-action.d.ts.map +1 -1
  532. package/dist/dashboard/web/update-action.js +11 -0
  533. package/dist/dashboard/web/update-action.js.map +1 -1
  534. package/dist/dashboard/workbench-doctor.d.ts +114 -0
  535. package/dist/dashboard/workbench-doctor.d.ts.map +1 -0
  536. package/dist/dashboard/workbench-doctor.js +620 -0
  537. package/dist/dashboard/workbench-doctor.js.map +1 -0
  538. package/dist/dashboard/workbench-ticket.d.ts +165 -0
  539. package/dist/dashboard/workbench-ticket.d.ts.map +1 -0
  540. package/dist/dashboard/workbench-ticket.js +495 -0
  541. package/dist/dashboard/workbench-ticket.js.map +1 -0
  542. package/dist/dashboard-web/app.js +1 -1
  543. package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +1 -0
  544. package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +1 -0
  545. package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +10 -0
  546. package/dist/dashboard-web/chunks/chunk-22WIADUT.js +2 -0
  547. package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +1 -0
  548. package/dist/dashboard-web/chunks/{chunk-6O7KXFD2.js → chunk-2W6SCN3M.js} +1 -1
  549. package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +1 -0
  550. package/dist/dashboard-web/chunks/{chunk-Q2MT4VWH.js → chunk-F6KNPF4R.js} +11 -9
  551. package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +1 -0
  552. package/dist/dashboard-web/chunks/chunk-G5ZS2YUS.js +1 -0
  553. package/dist/dashboard-web/chunks/chunk-KTQ7B6ZR.js +1 -0
  554. package/dist/dashboard-web/chunks/{chunk-U3ZFSO7E.js → chunk-NACP7BMR.js} +1 -1
  555. package/dist/dashboard-web/chunks/{connectors-page-2KZO5KSJ.js → connectors-page-VOISXXII.js} +1 -1
  556. package/dist/dashboard-web/chunks/{feedback-page-CLVTOLLO.js → feedback-page-RN6HRZMS.js} +1 -1
  557. package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +8 -0
  558. package/dist/dashboard-web/chunks/{insights-page-4RPU2CRU.js → insights-page-HJFDYR5N.js} +3 -3
  559. package/dist/dashboard-web/chunks/{monitor-room-OTKQ2CXK.js → monitor-room-V7PCP47V.js} +1 -1
  560. package/dist/dashboard-web/chunks/{monitoring-page-IJPUB6JT.js → monitoring-page-NKEKVQAP.js} +1 -1
  561. package/dist/dashboard-web/chunks/{office-page-QSR3J44Y.js → office-page-GVJF73XW.js} +1 -1
  562. package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +1 -0
  563. package/dist/dashboard-web/chunks/{plugin-page-VEGTCEAS.js → plugin-page-J3OF5IY4.js} +1 -1
  564. package/dist/dashboard-web/chunks/{roles-page-6NWJMOB5.js → roles-page-WQIRSOLZ.js} +1 -1
  565. package/dist/dashboard-web/chunks/{schedules-page-6I3LNXD4.js → schedules-page-J2DEY2X6.js} +1 -1
  566. package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +8 -0
  567. package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +2 -0
  568. package/dist/dashboard-web/chunks/{skills-page-M2Q6L5CB.js → skills-page-HGIFR3IU.js} +2 -2
  569. package/dist/dashboard-web/chunks/{team-federation-page-7I7FDE3B.js → team-federation-page-MR5ZPL4B.js} +1 -1
  570. package/dist/dashboard-web/chunks/{v3-page-HQXRL65L.js → v3-page-GBR5JWPE.js} +2 -2
  571. package/dist/dashboard-web/chunks/{whiteboards-page-LQKC4TAZ.js → whiteboards-page-QAZM4BN3.js} +1 -1
  572. package/dist/dashboard-web/index.html +8 -1
  573. package/dist/dashboard-web/style.css +2006 -10
  574. package/dist/dashboard.js +1017 -264
  575. package/dist/dashboard.js.map +1 -1
  576. package/dist/dsh-runner.js +8 -1
  577. package/dist/dsh-runner.js.map +1 -1
  578. package/dist/global-config.d.ts +30 -0
  579. package/dist/global-config.d.ts.map +1 -1
  580. package/dist/global-config.js +33 -0
  581. package/dist/global-config.js.map +1 -1
  582. package/dist/i18n/en.d.ts.map +1 -1
  583. package/dist/i18n/en.js +46 -27
  584. package/dist/i18n/en.js.map +1 -1
  585. package/dist/i18n/zh.d.ts.map +1 -1
  586. package/dist/i18n/zh.js +43 -27
  587. package/dist/i18n/zh.js.map +1 -1
  588. package/dist/im/lark/card-builder.d.ts +1 -1
  589. package/dist/im/lark/card-builder.d.ts.map +1 -1
  590. package/dist/im/lark/card-builder.js +22 -6
  591. package/dist/im/lark/card-builder.js.map +1 -1
  592. package/dist/im/lark/card-handler.d.ts.map +1 -1
  593. package/dist/im/lark/card-handler.js +118 -24
  594. package/dist/im/lark/card-handler.js.map +1 -1
  595. package/dist/im/lark/client.d.ts +15 -0
  596. package/dist/im/lark/client.d.ts.map +1 -1
  597. package/dist/im/lark/client.js +30 -0
  598. package/dist/im/lark/client.js.map +1 -1
  599. package/dist/im/lark/event-dispatcher.d.ts +48 -0
  600. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  601. package/dist/im/lark/event-dispatcher.js +138 -41
  602. package/dist/im/lark/event-dispatcher.js.map +1 -1
  603. package/dist/im/lark/identity-cache.d.ts +14 -6
  604. package/dist/im/lark/identity-cache.d.ts.map +1 -1
  605. package/dist/im/lark/identity-cache.js +69 -26
  606. package/dist/im/lark/identity-cache.js.map +1 -1
  607. package/dist/im/lark/lark-hosts.d.ts +13 -0
  608. package/dist/im/lark/lark-hosts.d.ts.map +1 -1
  609. package/dist/im/lark/lark-hosts.js +16 -0
  610. package/dist/im/lark/lark-hosts.js.map +1 -1
  611. package/dist/im/lark/overview-card.d.ts +22 -0
  612. package/dist/im/lark/overview-card.d.ts.map +1 -1
  613. package/dist/im/lark/overview-card.js +53 -3
  614. package/dist/im/lark/overview-card.js.map +1 -1
  615. package/dist/im/lark/sessions-card.d.ts.map +1 -1
  616. package/dist/im/lark/sessions-card.js +26 -3
  617. package/dist/im/lark/sessions-card.js.map +1 -1
  618. package/dist/im/lark/workflow-slash-command.d.ts +6 -0
  619. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
  620. package/dist/im/lark/workflow-slash-command.js +7 -0
  621. package/dist/im/lark/workflow-slash-command.js.map +1 -1
  622. package/dist/index-daemon.js +27 -4
  623. package/dist/index-daemon.js.map +1 -1
  624. package/dist/index-dashboard.d.ts +3 -0
  625. package/dist/index-dashboard.d.ts.map +1 -0
  626. package/dist/index-dashboard.js +94 -0
  627. package/dist/index-dashboard.js.map +1 -0
  628. package/dist/platform/team-agents-client.d.ts +199 -0
  629. package/dist/platform/team-agents-client.d.ts.map +1 -0
  630. package/dist/platform/team-agents-client.js +263 -0
  631. package/dist/platform/team-agents-client.js.map +1 -0
  632. package/dist/platform/tunnel-client.d.ts +6 -0
  633. package/dist/platform/tunnel-client.d.ts.map +1 -1
  634. package/dist/platform/tunnel-client.js.map +1 -1
  635. package/dist/services/async-trigger-state.d.ts +7 -3
  636. package/dist/services/async-trigger-state.d.ts.map +1 -1
  637. package/dist/services/async-trigger-state.js +28 -0
  638. package/dist/services/async-trigger-state.js.map +1 -1
  639. package/dist/services/async-trigger-store.d.ts +13 -4
  640. package/dist/services/async-trigger-store.d.ts.map +1 -1
  641. package/dist/services/async-trigger-store.js +55 -8
  642. package/dist/services/async-trigger-store.js.map +1 -1
  643. package/dist/services/bot-config-store.d.ts +1 -11
  644. package/dist/services/bot-config-store.d.ts.map +1 -1
  645. package/dist/services/bot-config-store.js +27 -3
  646. package/dist/services/bot-config-store.js.map +1 -1
  647. package/dist/services/bot-profile-store.d.ts +17 -2
  648. package/dist/services/bot-profile-store.d.ts.map +1 -1
  649. package/dist/services/bot-profile-store.js +93 -12
  650. package/dist/services/bot-profile-store.js.map +1 -1
  651. package/dist/services/bridge-fallback-gate.d.ts +15 -7
  652. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  653. package/dist/services/bridge-fallback-gate.js +43 -16
  654. package/dist/services/bridge-fallback-gate.js.map +1 -1
  655. package/dist/services/bridge-turn-queue.d.ts +10 -1
  656. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  657. package/dist/services/bridge-turn-queue.js +29 -9
  658. package/dist/services/bridge-turn-queue.js.map +1 -1
  659. package/dist/services/claude-transcript.d.ts +19 -0
  660. package/dist/services/claude-transcript.d.ts.map +1 -1
  661. package/dist/services/claude-transcript.js +57 -5
  662. package/dist/services/claude-transcript.js.map +1 -1
  663. package/dist/services/codex-app-threads.d.ts +2 -0
  664. package/dist/services/codex-app-threads.d.ts.map +1 -1
  665. package/dist/services/codex-app-threads.js +1 -1
  666. package/dist/services/codex-app-threads.js.map +1 -1
  667. package/dist/services/codex-reasoning-effort.d.ts +8 -0
  668. package/dist/services/codex-reasoning-effort.d.ts.map +1 -1
  669. package/dist/services/codex-reasoning-effort.js +24 -0
  670. package/dist/services/codex-reasoning-effort.js.map +1 -1
  671. package/dist/services/cursor-resume-policy.d.ts.map +1 -1
  672. package/dist/services/cursor-resume-policy.js +15 -3
  673. package/dist/services/cursor-resume-policy.js.map +1 -1
  674. package/dist/services/open-platform-rename.d.ts.map +1 -1
  675. package/dist/services/open-platform-rename.js +3 -2
  676. package/dist/services/open-platform-rename.js.map +1 -1
  677. package/dist/services/ordinary-turn-recovery.d.ts +76 -0
  678. package/dist/services/ordinary-turn-recovery.d.ts.map +1 -0
  679. package/dist/services/ordinary-turn-recovery.js +349 -0
  680. package/dist/services/ordinary-turn-recovery.js.map +1 -0
  681. package/dist/services/peer-cross-ref-store.d.ts +8 -0
  682. package/dist/services/peer-cross-ref-store.d.ts.map +1 -0
  683. package/dist/services/peer-cross-ref-store.js +95 -0
  684. package/dist/services/peer-cross-ref-store.js.map +1 -0
  685. package/dist/services/pi-transcript.d.ts.map +1 -1
  686. package/dist/services/pi-transcript.js +10 -6
  687. package/dist/services/pi-transcript.js.map +1 -1
  688. package/dist/services/resume-fresh-policy.d.ts +17 -0
  689. package/dist/services/resume-fresh-policy.d.ts.map +1 -0
  690. package/dist/services/resume-fresh-policy.js +24 -0
  691. package/dist/services/resume-fresh-policy.js.map +1 -0
  692. package/dist/services/session-group-title.d.ts +1 -0
  693. package/dist/services/session-group-title.d.ts.map +1 -1
  694. package/dist/services/session-group-title.js +16 -1
  695. package/dist/services/session-group-title.js.map +1 -1
  696. package/dist/services/session-groups-store.d.ts +30 -0
  697. package/dist/services/session-groups-store.d.ts.map +1 -1
  698. package/dist/services/session-groups-store.js +26 -4
  699. package/dist/services/session-groups-store.js.map +1 -1
  700. package/dist/services/session-owner-notification.d.ts +9 -0
  701. package/dist/services/session-owner-notification.d.ts.map +1 -0
  702. package/dist/services/session-owner-notification.js +10 -0
  703. package/dist/services/session-owner-notification.js.map +1 -0
  704. package/dist/services/session-owner-reminder-config-store.d.ts +9 -0
  705. package/dist/services/session-owner-reminder-config-store.d.ts.map +1 -0
  706. package/dist/services/session-owner-reminder-config-store.js +24 -0
  707. package/dist/services/session-owner-reminder-config-store.js.map +1 -0
  708. package/dist/services/session-owner-reminder-store.d.ts +5 -0
  709. package/dist/services/session-owner-reminder-store.d.ts.map +1 -0
  710. package/dist/services/session-owner-reminder-store.js +54 -0
  711. package/dist/services/session-owner-reminder-store.js.map +1 -0
  712. package/dist/services/session-store.d.ts +98 -21
  713. package/dist/services/session-store.d.ts.map +1 -1
  714. package/dist/services/session-store.js +406 -48
  715. package/dist/services/session-store.js.map +1 -1
  716. package/dist/services/structured-bridge-clis.d.ts +15 -2
  717. package/dist/services/structured-bridge-clis.d.ts.map +1 -1
  718. package/dist/services/structured-bridge-clis.js +16 -1
  719. package/dist/services/structured-bridge-clis.js.map +1 -1
  720. package/dist/services/transcript-resolver.d.ts +1 -1
  721. package/dist/services/transcript-resolver.d.ts.map +1 -1
  722. package/dist/services/transcript-resolver.js +6 -1
  723. package/dist/services/transcript-resolver.js.map +1 -1
  724. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  725. package/dist/setup/bot-config-editor.js +7 -2
  726. package/dist/setup/bot-config-editor.js.map +1 -1
  727. package/dist/setup/ensure-herdr-integrations.d.ts +13 -0
  728. package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -1
  729. package/dist/setup/ensure-herdr-integrations.js +21 -3
  730. package/dist/setup/ensure-herdr-integrations.js.map +1 -1
  731. package/dist/setup/open-platform-automation.d.ts +1 -0
  732. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  733. package/dist/setup/open-platform-automation.js +107 -7
  734. package/dist/setup/open-platform-automation.js.map +1 -1
  735. package/dist/skills/definitions.d.ts +17 -0
  736. package/dist/skills/definitions.d.ts.map +1 -1
  737. package/dist/skills/definitions.js +78 -8
  738. package/dist/skills/definitions.js.map +1 -1
  739. package/dist/skills/injection-mode.d.ts +24 -3
  740. package/dist/skills/injection-mode.d.ts.map +1 -1
  741. package/dist/skills/injection-mode.js +64 -15
  742. package/dist/skills/injection-mode.js.map +1 -1
  743. package/dist/skills/installer.d.ts +18 -0
  744. package/dist/skills/installer.d.ts.map +1 -1
  745. package/dist/skills/installer.js +45 -1
  746. package/dist/skills/installer.js.map +1 -1
  747. package/dist/types.d.ts +222 -10
  748. package/dist/types.d.ts.map +1 -1
  749. package/dist/utils/bot-routing.d.ts +1 -1
  750. package/dist/utils/bot-routing.d.ts.map +1 -1
  751. package/dist/utils/bot-routing.js.map +1 -1
  752. package/dist/utils/child-env.d.ts +92 -2
  753. package/dist/utils/child-env.d.ts.map +1 -1
  754. package/dist/utils/child-env.js +195 -1
  755. package/dist/utils/child-env.js.map +1 -1
  756. package/dist/utils/cli-usage-limit.d.ts +25 -14
  757. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  758. package/dist/utils/cli-usage-limit.js +25 -15
  759. package/dist/utils/cli-usage-limit.js.map +1 -1
  760. package/dist/utils/daemon-rejection-guard.d.ts +35 -0
  761. package/dist/utils/daemon-rejection-guard.d.ts.map +1 -0
  762. package/dist/utils/daemon-rejection-guard.js +101 -0
  763. package/dist/utils/daemon-rejection-guard.js.map +1 -0
  764. package/dist/utils/dashboard-env.d.ts +53 -0
  765. package/dist/utils/dashboard-env.d.ts.map +1 -0
  766. package/dist/utils/dashboard-env.js +116 -0
  767. package/dist/utils/dashboard-env.js.map +1 -0
  768. package/dist/utils/global-install.d.ts.map +1 -1
  769. package/dist/utils/global-install.js +61 -5
  770. package/dist/utils/global-install.js.map +1 -1
  771. package/dist/utils/install-diagnostics.d.ts +14 -7
  772. package/dist/utils/install-diagnostics.d.ts.map +1 -1
  773. package/dist/utils/install-diagnostics.js +20 -11
  774. package/dist/utils/install-diagnostics.js.map +1 -1
  775. package/dist/utils/local-dev-update.d.ts +132 -0
  776. package/dist/utils/local-dev-update.d.ts.map +1 -0
  777. package/dist/utils/local-dev-update.js +176 -0
  778. package/dist/utils/local-dev-update.js.map +1 -0
  779. package/dist/utils/pending-input-queue.d.ts +12 -1
  780. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  781. package/dist/utils/pending-input-queue.js.map +1 -1
  782. package/dist/worker.js +1517 -236
  783. package/dist/worker.js.map +1 -1
  784. package/dist/workflows/definition.d.ts +68 -68
  785. package/dist/workflows/events/payloads.d.ts +14 -14
  786. package/dist/workflows/events/schema.d.ts +312 -312
  787. package/dist/workflows/migration/v2-run-archive-schema.d.ts +40 -40
  788. package/dist/workflows/v3/cli-run.d.ts.map +1 -1
  789. package/dist/workflows/v3/cli-run.js +7 -0
  790. package/dist/workflows/v3/cli-run.js.map +1 -1
  791. package/dist/workflows/v3/goal-cli.d.ts +3 -0
  792. package/dist/workflows/v3/goal-cli.d.ts.map +1 -1
  793. package/dist/workflows/v3/goal-cli.js +11 -0
  794. package/dist/workflows/v3/goal-cli.js.map +1 -1
  795. package/dist/workflows/v3/run-envelope.d.ts +144 -144
  796. package/package.json +7 -2
  797. package/dist/cli/pm2-god-admission.d.ts +0 -8
  798. package/dist/cli/pm2-god-admission.d.ts.map +0 -1
  799. package/dist/cli/pm2-god-admission.js +0 -25
  800. package/dist/cli/pm2-god-admission.js.map +0 -1
  801. package/dist/core/riff-shutdown-detach.d.ts.map +0 -1
  802. package/dist/core/riff-shutdown-detach.js.map +0 -1
  803. package/dist/core/riff-worker-shutdown-readiness.d.ts.map +0 -1
  804. package/dist/core/riff-worker-shutdown-readiness.js.map +0 -1
  805. package/dist/dashboard-web/chunks/bot-defaults-page-4RENHE7J.js +0 -10
  806. package/dist/dashboard-web/chunks/chunk-2WQDS65D.js +0 -1
  807. package/dist/dashboard-web/chunks/chunk-G5XGBCM6.js +0 -1
  808. package/dist/dashboard-web/chunks/chunk-WLIVBPJC.js +0 -1
  809. package/dist/dashboard-web/chunks/groups-page-HLASRTSY.js +0 -8
  810. package/dist/dashboard-web/chunks/overview-page-6OK63V4V.js +0 -1
  811. package/dist/dashboard-web/chunks/sessions-page-B2TXGJDO.js +0 -8
  812. package/dist/dashboard-web/chunks/settings-page-6KJADSC6.js +0 -2
package/dist/dashboard.js CHANGED
@@ -1,23 +1,32 @@
1
1
  // src/dashboard.ts
2
- import { createServer, get as httpGet, request as httpRequest } from 'node:http';
3
- import { createServer as createTcpServer, connect as netConnect } from 'node:net';
2
+ import { createServer, get as httpGet } from 'node:http';
3
+ import { createServer as createTcpServer } from 'node:net';
4
4
  import { readFileSync, existsSync, mkdirSync, readdirSync, statSync, createReadStream, realpathSync, } from 'node:fs';
5
5
  import { atomicWriteFileSync } from './utils/atomic-write.js';
6
6
  import { join, dirname, extname, resolve, relative, isAbsolute } from 'node:path';
7
7
  import { homedir } from 'node:os';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { randomBytes } from 'node:crypto';
9
+ import { createHmac, randomBytes } from 'node:crypto';
10
10
  import { logger } from './utils/logger.js';
11
11
  import { gracefulProcessExitCode } from './pm2-graceful-exit.js';
12
12
  import { config, isWildcardBindHost } from './config.js';
13
13
  import { listenWithProbe } from './utils/listen-with-probe.js';
14
- import { parseCookie, buildSetCookie, verifyHmac, cliAuthBind, decideDashboardAuth, loadPersistedToken, loadOrCreatePersistedToken, rotatePersistedToken, loadDashboardSecret, loadOrCreateDashboardSecret, describeDashboardTokenError, } from './dashboard/auth.js';
14
+ import { parseCookie, buildSetCookie, verifyHmac, cliAuthBind, projectWorkbenchOperationCapabilities, previewInteractionWriteAllowed, loadPersistedToken, loadOrCreatePersistedToken, rotatePersistedToken, loadDashboardSecret, loadOrCreateDashboardSecret, describeDashboardTokenError, } from './dashboard/auth.js';
15
+ import { resolveDashboardIdentity, resolveDashboardRequestGate, } from './dashboard/request-identity.js';
16
+ import { AuthSessionConnectionRegistry } from './dashboard/auth-session-connections.js';
17
+ import { createDashboardEventsStream } from './dashboard/events-sse.js';
18
+ import { ControlCsrfTokens, guardControlRequest, injectControlCsrfMeta, managementUpgradeOrigin, } from './dashboard/control-csrf.js';
15
19
  import { DaemonRegistry, botsRosterSignature } from './dashboard/registry.js';
16
20
  import { Aggregator, subscribeDaemon } from './dashboard/aggregator.js';
21
+ import { reconcileDaemonSnapshot } from './dashboard/daemon-reconcile.js';
17
22
  import { createSessionPresentationCoordinator } from './dashboard/session-presentation.js';
18
23
  import { compactGroupsMatrix, createGroupsMatrixSnapshot, enrichSessionsWithGroupNames, roleWriteShouldInvalidate, } from './dashboard/groups-matrix-snapshot.js';
19
24
  import { parseDashboardAskAnswerRequest, proxyDashboardAskAnswer, } from './dashboard/desktop-asks.js';
20
25
  import { createDebugTerminalManager } from './dashboard/debug-terminal.js';
26
+ import { createSessionPreviewProxy } from './dashboard/preview-proxy.js';
27
+ import { mintPreviewContentCapability, verifyPreviewContentCapability, } from './dashboard/preview-content-capability.js';
28
+ import { previewDescriptorFromRow, previewTeardownForDaemonEvent, projectSessionDetailForBrowser, projectSessionPreviewEventForBrowser, projectSessionPreviewsForBrowser, resolveSessionPreviewForProxy, } from './dashboard/preview-contract.js';
29
+ import { sameSessionPreviewTarget, sessionPreviewTargetStillOwned, } from './core/session-preview.js';
21
30
  import { pickCreatorForGroup } from './dashboard/operator-selector.js';
22
31
  import { buildTeamGroupCreatePayload, planGroupCreator } from './dashboard/team-group.js';
23
32
  import { jsonRes } from './dashboard/http.js';
@@ -26,7 +35,7 @@ import { defaultRunsDir as v3RunsDir } from './workflows/v3/ops-projection.js';
26
35
  import { verifyWorkflowDaemonIpcResponse, workflowDaemonIpcHeaders, WORKFLOW_DAEMON_IPC_ROUTE_PREFIX, } from './workflows/v3/daemon-ipc-auth.js';
27
36
  import { handleDashboardTriggerApi } from './dashboard/trigger-api.js';
28
37
  import { handleConnectorApi } from './dashboard/connector-api.js';
29
- import { redactGroupsForPublic, redactSchedulesForPublic, redactSessionEventForPublic, redactSessionsForPublic, redactSettingsForPublic, } from './dashboard/public-redact.js';
38
+ import { projectSessionEventForAudience, projectSessionsForAudience, redactGroupsForPublic, redactSchedulesForPublic, redactSettingsForPublic, sessionBoardAudienceFor, } from './dashboard/public-redact.js';
30
39
  import { handleWebhookRoute } from './dashboard/webhook-routes.js';
31
40
  import { handleFeedbackAnalyticsApi } from './dashboard/feedback-analytics-api.js';
32
41
  import { FeedbackAnalyticsService } from './services/feedback-analytics.js';
@@ -36,33 +45,48 @@ import { resolveLiveBotTransport } from './services/team-roster.js';
36
45
  import { handleFederationSpokeApi, syncAllMemberships, autoBindOwnerIfUnambiguous } from './dashboard/federation-spoke-api.js';
37
46
  import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
38
47
  import { FeishuLoginManager } from './dashboard/feishu-login.js';
48
+ import { createDashboardH5AuthController, DashboardSessionStore, resolveDashboardH5AuthConfig, } from './dashboard/h5-auth.js';
49
+ import { FileControlAuditSink } from './dashboard/control-audit.js';
50
+ import { TerminalControlManager, terminalControlTtlFromEnv, } from './dashboard/terminal-control.js';
51
+ import { PreviewInteractionManager } from './dashboard/preview-interaction.js';
52
+ import { createPreviewGuardPage } from './dashboard/preview-guard-page.js';
53
+ import { handleWorkbenchDoctor } from './dashboard/workbench-doctor.js';
54
+ import { handleWorkbenchTicketRedemption, revokeWorkbenchTicketsOutsideGeneration, workbenchTicketGeneration, } from './dashboard/workbench-ticket.js';
55
+ import { handleWorkbenchStandingLink } from './dashboard/standing-link.js';
56
+ import { createTerminalFrontProxy } from './dashboard/terminal-front-proxy.js';
57
+ import { centralViewLinkPath, mintTerminalViewCapability, terminalViewCapabilityAuthSession, terminalViewForwardProof, upstreamWorkerViewGeneration, } from './dashboard/terminal-view-capability.js';
39
58
  import { CLI_SELECT_OPTIONS, resolveCliSelection, isTtadkWrapper, ttadkAcceptsModel, TTADK_DEFAULT_MODEL, TTADK_MODEL_SUGGESTIONS, } from './setup/cli-selection.js';
40
59
  import { checkCliAvailability } from './setup/cli-availability.js';
41
60
  import { invalidWorkingDirs } from './utils/working-dir.js';
42
61
  import { invalidateGlobalConfigCache, mergeDashboardConfig, mergeGlobalConfig, readGlobalConfig } from './global-config.js';
43
62
  import { hostLocalTimeZone, scheduleTimeZone } from './utils/timezone.js';
44
- import { buildDashboardUrls, buildPlatformDashboardLoginUrl, } from './core/dashboard-url.js';
63
+ import { buildDashboardUrls, buildPlatformDashboardLoginUrl, workbenchEntryUrl, } from './core/dashboard-url.js';
45
64
  import { resolveBotmuxDataDir } from './core/data-dir.js';
65
+ import { parseCloseResidual } from './core/close-residual.js';
46
66
  import { dashboardSecretPath } from './core/dashboard-secret.js';
47
67
  import { getGitRepoInfo } from './core/session-row-enrichment.js';
48
68
  import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
49
- import { isLocalDevInstall, botmuxVersionAt, botmuxCliEntry, botmuxInstallRoot } from './utils/install-info.js';
50
- import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
69
+ import { isLocalDevInstall, botmuxVersionAt, botmuxCliEntryAt, botmuxInstallRoot } from './utils/install-info.js';
70
+ import { checkNode, detectBotmuxInstalls, resolveCurrentVersion, resolveCurrentVersionAt } from './utils/install-diagnostics.js';
51
71
  import { fetchLatestVersion, fetchReleasesSince, fetchRollbackVersions, compareVersions, isCanonicalStableVersion, isNewerVersion, } from './core/update-check.js';
52
72
  import { GITHUB_REPO } from './core/restart-report.js';
53
73
  import { DEFAULT_OVERLOAD_THRESHOLDS } from './core/host-overload-alert.js';
54
- import { spawnDetachedRestart, globalInstallUpdateLockTarget, globalInstallUpdateCwd } from './core/maintenance.js';
74
+ import { spawnDetachedRestart, globalInstallUpdateLockTarget } from './core/maintenance.js';
75
+ import { resolveLocalDevCheckoutDir, resolveLocalDevRestartTarget, isGitWorktree, gitPorcelainStatus, gitHeadSha, localDevUpdateSteps, } from './utils/local-dev-update.js';
55
76
  import { detectGlobalInstallManager, formatGlobalInstallCommand, resolveGlobalInstallPlan, tryResolveGlobalInstallPlan, withGlobalInstallRegistry, UnsupportedGlobalInstallError, } from './utils/global-install.js';
56
77
  import { filterCliRuntimeUpdateEntriesForTargets, listCliRuntimeUpdateEntries, selectCodexRuntimeUpdateTargets, } from './core/cli-runtime-update.js';
57
78
  import { claimRestartLease, clearRestartIntent, clearRestartLease, hasActiveRestartLease, writeManualIntentIfAbsent, writeRestartIntent, } from './services/restart-intent-store.js';
58
79
  import { withFileLock } from './utils/file-lock.js';
59
80
  import { evaluateRestartShutdownPreflight } from './cli/restart-shutdown-preflight.js';
60
- import { spawn } from 'node:child_process';
81
+ // Host children the dashboard forks (start/stop-bot, global install). They live
82
+ // in their own module because every one of them must run on a REDACTED env —
83
+ // see dashboard/managed-spawn.ts.
84
+ import { runGlobalInstall, runLocalDevStep, spawnStartBotLive, spawnStopBotLive } from './dashboard/managed-spawn.js';
61
85
  import { applySettingsWrite, defaultSettingsWriteApplierDeps, hasResolvedCodexNotifierRecipient, resolveCodexNotifierRecipientView, } from './dashboard/settings-write-applier.js';
62
86
  import { addBotsToGroup, bindOncall, disbandGroup, leaveGroup, unbindOncall, } from './dashboard/groups-action-helpers.js';
63
87
  import { createDaemonInternalApi } from './dashboard/daemon-internal-api.js';
64
88
  import { listTeamReports, readTeamBoard, setTeamBoardEntry } from './services/team-board-store.js';
65
- import { createCliAdapterSync } from './adapters/cli/registry.js';
89
+ import { ALL_CLI_IDS, createCliAdapterSync } from './adapters/cli/registry.js';
66
90
  import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
67
91
  import { buildSkillInstallAuditSummary, installLocalSkillLinks, readSkillRegistry, removeInstalledSkill, removeInstalledSkills, sweepStoreTrash, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
68
92
  import { readSkillPackRegistry } from './services/skill-pack-store.js';
@@ -90,6 +114,7 @@ import { readPlatformBinding } from './platform/binding.js';
90
114
  import { startPlatformTunnelClient } from './platform/tunnel-client.js';
91
115
  import { applyPlatformTeamSync, getPlatformTeamSyncRev, listPlatformTeams } from './services/platform-team-store.js';
92
116
  import { getBotUnionId } from './services/bot-union-ids-store.js';
117
+ import { getBotSpecialties } from './services/bot-profile-store.js';
93
118
  import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
94
119
  import { compatMachineIdForAuthenticatedRequest, handleDesktopCompat, } from './dashboard/compat.js';
95
120
  import { isDashboardChunkJsPath, missingDashboardChunkModule } from './dashboard/stale-chunk-module.js';
@@ -171,6 +196,116 @@ function currentDashboardToken() {
171
196
  // EADDRINUSE probe). Used for token-bearing URLs and persisted for the CLI.
172
197
  let boundDashboardPort = config.dashboard.port;
173
198
  const SECRET = loadOrCreateSecret();
199
+ const dashboardControlAudit = new FileControlAuditSink();
200
+ const dashboardH5AuthConfig = resolveDashboardH5AuthConfig();
201
+ const dashboardSessions = new DashboardSessionStore({ ttlMs: dashboardH5AuthConfig.sessionTtlMs });
202
+ const dashboardH5Auth = createDashboardH5AuthController({
203
+ config: dashboardH5AuthConfig,
204
+ sessions: dashboardSessions,
205
+ audit: dashboardControlAudit,
206
+ });
207
+ const terminalControl = new TerminalControlManager({
208
+ secret: SECRET,
209
+ audit: dashboardControlAudit,
210
+ ttlMs: terminalControlTtlFromEnv(),
211
+ });
212
+ const previewInteraction = new PreviewInteractionManager({ audit: dashboardControlAudit });
213
+ function legacyDashboardAuthSessionId(token) {
214
+ return createHmac('sha256', SECRET)
215
+ .update('botmux-legacy-dashboard-session-id-v1\0')
216
+ .update(token)
217
+ .digest('base64url');
218
+ }
219
+ /** Stable per-machine scope for platform-dashboard actors. Shared between
220
+ * identity resolution and the read-capability liveness check so the two can
221
+ * never drift apart on the authSessionId format. */
222
+ function platformDashboardActorScope(machineId) {
223
+ return createHmac('sha256', SECRET)
224
+ .update('botmux-platform-dashboard-actor-v1\0')
225
+ .update(machineId)
226
+ .digest('base64url');
227
+ }
228
+ /**
229
+ * P1-5 liveness for a bound terminal read capability: `false` means the auth
230
+ * session it was minted under is over (H5 logout/expiry, dashboard token
231
+ * rotation, platform unbind), so the front proxy refuses the capability even
232
+ * though its signature/expiry would still verify at the worker.
233
+ */
234
+ function terminalAuthSessionLive(authSessionId) {
235
+ if (dashboardSessions.liveAuthSession(authSessionId))
236
+ return true;
237
+ const activeToken = currentDashboardToken();
238
+ if (activeToken && authSessionId === legacyDashboardAuthSessionId(activeToken))
239
+ return true;
240
+ const binding = readPlatformBinding();
241
+ if (binding) {
242
+ const scope = platformDashboardActorScope(binding.machineId);
243
+ if (authSessionId === `${scope}:owner`
244
+ || authSessionId === `${scope}:teammate`
245
+ || authSessionId === `${scope}:guest`) {
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+ /** P1-8:预览内容凭据所属的认证会话(校验失败 → null,不予绑定)。 */
252
+ function previewContentCapabilityAuthSession(capability, sessionId) {
253
+ const verified = verifyPreviewContentCapability(SECRET, capability, sessionId);
254
+ return verified.ok ? verified.claims.authSessionId : null;
255
+ }
256
+ /** P1-7:身份判定只此一处(legacy > 平台角色 > H5),门禁选择也只读它的结论。 */
257
+ function dashboardRequestIdentity(req) {
258
+ return resolveDashboardIdentity({
259
+ legacyCookie: parseCookie(req.headers.cookie),
260
+ // The persisted file is the active-token authority (currentDashboardToken);
261
+ // there is no module-level mirror to compare against any more.
262
+ activeToken: currentDashboardToken(),
263
+ roleHeader: req.headers['x-botmux-role'],
264
+ platformMachineId: readPlatformBinding()?.machineId ?? null,
265
+ platformActorScope: platformDashboardActorScope,
266
+ legacyAuthSessionId: legacyDashboardAuthSessionId,
267
+ h5: dashboardH5Auth.resolve(req),
268
+ });
269
+ }
270
+ /** P1-8:authSession → 已建立的长连接(/events SSE、Preview SSE/长响应、
271
+ * Preview WS)。身份一结束就遍历关闭,不等对端自己断。 */
272
+ const authSessionConnections = new AuthSessionConnectionRegistry();
273
+ /** P1-11:控制类端点的一次性 CSRF 票据(页面加载现签、绑定认证会话)。 */
274
+ const controlCsrfTokens = new ControlCsrfTokens();
275
+ /**
276
+ * 身份结束的统一收口:H5 logout/到期、legacy token rotate、平台解绑三条来源都
277
+ * 走这里,少走一条就等于留一扇后窗(P1-5 关写租约/读 socket,P1-8 关其余长连接,
278
+ * P1-11 作废该会话签出的 CSRF 票据)。
279
+ */
280
+ function endDashboardAuthSession(authSessionId) {
281
+ terminalControl.releaseByAuthSession(authSessionId);
282
+ previewInteraction.relockAuthSession(authSessionId);
283
+ authSessionConnections.closeAuthSession(authSessionId);
284
+ controlCsrfTokens.revokeAuthSession(authSessionId);
285
+ }
286
+ /**
287
+ * P1-8 平台解绑/改绑:`botmux bind|unbind` 先写 platform.json 再捅
288
+ * `/__cli/reload-binding`,所以进入 handler 时磁盘上已经是**新**值——想知道刚
289
+ * 才被吊销的是谁,只能由本进程自己记住上一次认可的 machineId。
290
+ */
291
+ let observedPlatformMachineId = readPlatformBinding()?.machineId ?? null;
292
+ function syncPlatformBindingRevocation() {
293
+ const current = readPlatformBinding()?.machineId ?? null;
294
+ if (observedPlatformMachineId && observedPlatformMachineId !== current) {
295
+ const scope = platformDashboardActorScope(observedPlatformMachineId);
296
+ for (const role of ['owner', 'teammate', 'guest']) {
297
+ endDashboardAuthSession(`${scope}:${role}`);
298
+ }
299
+ }
300
+ observedPlatformMachineId = current;
301
+ }
302
+ dashboardSessions.onEnd(identity => {
303
+ // Ends BOTH capabilities of the authentication (P1-5): write leases AND every
304
+ // read socket the auth session opened (bound view-link capabilities included,
305
+ // via the front proxy's read-socket index). New connections with a capability
306
+ // minted under this authSessionId are refused by terminalAuthSessionLive.
307
+ endDashboardAuthSession(identity.authSessionId);
308
+ });
174
309
  function tcpPortAvailable(host, port) {
175
310
  return new Promise((resolve) => {
176
311
  const probe = createTcpServer();
@@ -222,6 +357,154 @@ function verifyDashboardBinding(port) {
222
357
  mkdirSync(REGISTRY_DIR, { recursive: true });
223
358
  const registry = new DaemonRegistry(REGISTRY_DIR);
224
359
  const aggregator = new Aggregator();
360
+ /**
361
+ * P1-13:一个会话的预览目标失效时的收口动作(本进程侧)。
362
+ *
363
+ * 「失效」= 目标没了或换了主人:worker 换代 / 切 CLI / 会话关闭 / 端口被别的进程接管。
364
+ * 这三件事必须一起做,少一件就留一扇后窗:
365
+ * 1. 断掉该会话已经建立的预览 SSE / 长响应 / WebSocket——它们握手时是合法的,此刻
366
+ * 仍在把一个**不再属于这个会话**的进程的内容送进浏览器;
367
+ * 2. 收回该会话上的交互租约(resume 出来的新一代不得继承旧的「交互模式」授权);
368
+ * 3. 让持有会话的 daemon 清掉 previewTarget 并广播 `preview: null`——代理进程只有
369
+ * procfs 的只读视角,改不了会话行。
370
+ */
371
+ const previewInvalidationsInFlight = new Set();
372
+ function teardownSessionPreview(sessionId) {
373
+ authSessionConnections.closeSessionStreams(sessionId);
374
+ previewInteraction.relockSession(sessionId);
375
+ }
376
+ function invalidateStalePreviewTarget(sessionId, ownerLarkAppId, staleTarget) {
377
+ teardownSessionPreview(sessionId);
378
+ if (!ownerLarkAppId || previewInvalidationsInFlight.has(sessionId))
379
+ return;
380
+ // 每个会话同一时刻只发一次清理请求:刷新一次页面就是几十条子资源请求,逐条捅
381
+ // daemon 会把一次失效放大成一场风暴。拒绝本身已经生效,这里只是让状态收敛。
382
+ previewInvalidationsInFlight.add(sessionId);
383
+ // P1-3:指名要作废的是判定失效的**那一次注册**。这条 DELETE 跨进程飞过去的途中,
384
+ // 会话完全可以合法地重注册一个新目标;不带 revision 的清空会把它一起抹掉。
385
+ const path = `/api/sessions/${encodeURIComponent(sessionId)}/preview`
386
+ + `?expectedRegisteredAt=${encodeURIComponent(staleTarget.registeredAt)}`;
387
+ void proxyToDaemon(ownerLarkAppId, path, { method: 'DELETE' })
388
+ .catch(() => { })
389
+ .finally(() => previewInvalidationsInFlight.delete(sessionId));
390
+ }
391
+ /**
392
+ * 每一跳预览请求的判定入口(HTTP、WebSocket 升级、guard 页面共用同一条)。
393
+ *
394
+ * P1-12:归属复核只读 /proc/net/tcp{,6} 与 /proc/<pid>/stat(全体可读),所以即便
395
+ * 代理进程与 daemon 不是同一个用户也能在每次落地前重新核验。
396
+ */
397
+ function resolveDashboardSessionPreview(sessionId) {
398
+ const owner = aggregator.ownerOf(sessionId);
399
+ return resolveSessionPreviewForProxy({
400
+ row: aggregator.getSession(sessionId),
401
+ sessionId,
402
+ ownerLarkAppId: owner,
403
+ daemonOnline: !!owner && !!registry.getByAppId(owner),
404
+ isTargetOwned: target => sessionPreviewTargetStillOwned(target),
405
+ onStaleTarget: (staleSessionId, staleTarget) => invalidateStalePreviewTarget(staleSessionId, owner, staleTarget),
406
+ });
407
+ }
408
+ const sessionPreviewProxy = createSessionPreviewProxy({
409
+ // Preview HTTP/WS never accepts ?t=. The user must first establish either
410
+ // the legacy management cookie or an allow-listed short H5 session.
411
+ authenticated: req => dashboardRequestIdentity(req) !== null,
412
+ resolve: resolveDashboardSessionPreview,
413
+ // P0: the sandboxed content stream is an opaque origin, so it carries no
414
+ // cookie of any kind. Its path-scoped capability is the only credential —
415
+ // signature + session binding + expiry here, plus central revocation
416
+ // (logout / token rotation / platform unbind) through the same auth-session
417
+ // liveness the bound terminal read capability uses.
418
+ verifyContentCapability: (capability, sessionId) => {
419
+ const verified = verifyPreviewContentCapability(SECRET, capability, sessionId);
420
+ return verified.ok && terminalAuthSessionLive(verified.claims.authSessionId);
421
+ },
422
+ // P1-8: preview SSE / long responses / WebSocket bridges outlive the handshake
423
+ // that authorised them. Index each stream under its auth session so logout /
424
+ // rotation / unbind tears it down immediately. The content path carries no
425
+ // cookie, so its owner is the capability's own authSessionId.
426
+ // P1-13:同一条流再按 sessionId 建第二个索引。预览目标失效(换代 / 关闭 / 端口易主)
427
+ // 时身份仍然有效,只能靠这个索引定点断流。
428
+ // P1-4:这里同时是「登记点」和「最后一次判定点」。授权发生在拨号之前,而 dev
429
+ // server 的握手最长可以拖 45 秒;这段窗口里的登出/到期/rotate/解绑扫描不到一条
430
+ // 还没入索引的流。所以登记前把身份重新解一遍并复核存活:解不出身份(cookie 那条
431
+ // 路的会话已经没了)或已不存活,一律 fail closed,由代理销毁上游、不回 101/200。
432
+ // P1-1:同一段窗口里换靶也要 fail closed。身份没变、目标却已经换代 / 切 CLI /
433
+ // 端口易主时,旧流握完手照样能拿到 200/101,还会被**重新登记**进索引——换靶那一刻
434
+ // 的 teardown 扫的是索引,扫不到一条还没入索引的流。所以登记前把目标重解一遍
435
+ // (`resolveDashboardSessionPreview` 内含每跳的 owner 复核)并与拨号时那个比指纹:
436
+ // 不是同一次注册就不登记、不回 101/200,由代理销毁上游。
437
+ bindStream: (req, ctx, close) => {
438
+ const authSessionId = ctx.contentCapability
439
+ ? previewContentCapabilityAuthSession(ctx.contentCapability, ctx.sessionId)
440
+ : dashboardRequestIdentity(req)?.authSessionId ?? null;
441
+ if (!authSessionId || !terminalAuthSessionLive(authSessionId))
442
+ return false;
443
+ const current = resolveDashboardSessionPreview(ctx.sessionId);
444
+ if (!current.ok || !sameSessionPreviewTarget(current.target, ctx.target))
445
+ return false;
446
+ return authSessionConnections.register(authSessionId, close, ctx.sessionId);
447
+ },
448
+ });
449
+ /**
450
+ * P1-13:daemon 侧生命周期事件 → 预览收口。
451
+ *
452
+ * daemon 在每个权威换代边界广播 `preview: null`(worker 换代 / suspend / exit /
453
+ * close),会话彻底结束时广播 `session.exited`。中央 Dashboard 订阅这些事件,把本地
454
+ * 还挂着的预览长连接断掉、交互租约收回——否则「服务端已经没有目标了,浏览器那条流
455
+ * 还在流」,而且 resume 之后旧的交互授权会直接落到新一代 CLI 上。
456
+ *
457
+ * P1-1:`session.spawned` 也要认。daemon 重启期间广播的 `preview: null` 是丢的(事件
458
+ * 总线没有 replay buffer),重连之后只以 spawned 重放形式补齐。判据与记忆都在
459
+ * `previewTeardownForDaemonEvent` 里——只有目标指纹**确实变了**才收口,否则每次 SSE
460
+ * 重连都会误杀全部预览长连接。
461
+ */
462
+ const lastSeenPreviewFingerprints = new Map();
463
+ aggregator.on(ev => {
464
+ const sessionId = previewTeardownForDaemonEvent(ev, lastSeenPreviewFingerprints);
465
+ if (sessionId)
466
+ teardownSessionPreview(sessionId);
467
+ });
468
+ const previewGuardPage = createPreviewGuardPage({
469
+ authenticated: req => dashboardRequestIdentity(req) !== null,
470
+ resolve: resolveDashboardSessionPreview,
471
+ mintContentCapability: (req, sessionId) => {
472
+ const identity = dashboardRequestIdentity(req);
473
+ return identity
474
+ ? mintPreviewContentCapability(SECRET, sessionId, {
475
+ userId: identity.userId,
476
+ authSessionId: identity.authSessionId,
477
+ expiresAt: identity.expiresAt,
478
+ })
479
+ : null;
480
+ },
481
+ // P1-11: the guard shell is same-origin with the dashboard and POSTs
482
+ // unlock/activity/lock itself, so it needs its own control ticket.
483
+ mintCsrfToken: req => {
484
+ const identity = dashboardRequestIdentity(req);
485
+ return identity ? controlCsrfTokens.mint(identity.authSessionId) : null;
486
+ },
487
+ // P2:解锁按钮按能力渲染,与工作台面板用同一份投影(canInteract)。平台
488
+ // teammate/guest 这类 previewCapability=readonly 的身份,解锁 POST 本来就会被
489
+ // 下面的 preview-interaction 路由 403;壳里不再画那个按钮,避免「点了才知道
490
+ // 没权限」。这只是不渲染一个必然失败的入口,服务端门禁一分未松。
491
+ canInteract: req => projectWorkbenchOperationCapabilities(dashboardRequestIdentity(req)).canInteract,
492
+ });
493
+ const terminalFrontProxy = createTerminalFrontProxy({
494
+ resolvePort: sessionId => aggregator.terminalProxyPortOf(sessionId),
495
+ resolveActor: dashboardRequestIdentity,
496
+ control: terminalControl,
497
+ // P1-5: bound `?viewToken=` capabilities are refused once the auth session
498
+ // they were minted under ended, and their bridged sockets are indexed so
499
+ // logout/expiry closes them immediately (see dashboardSessions.onEnd).
500
+ viewCapabilityAuthSession: (sessionId, viewToken) => terminalViewCapabilityAuthSession(SECRET, sessionId, viewToken),
501
+ isAuthSessionLive: terminalAuthSessionLive,
502
+ // P1-5: this proxy is the ONLY consumer allowed to spend a view capability.
503
+ // The countersignature proves the loopback hop passed through here — and
504
+ // therefore through the liveness check above — so a raw view URL aimed at the
505
+ // worker port or the daemon's own `/s/` proxy is refused by the worker.
506
+ viewCapabilityForwardProof: viewToken => terminalViewForwardProof(SECRET, viewToken),
507
+ });
225
508
  const sessionPresentation = createSessionPresentationCoordinator(aggregator, getGitRepoInfo);
226
509
  const groupsMatrixSnapshot = createGroupsMatrixSnapshot(buildGroupsMatrix, {
227
510
  onRefreshError: error => logger.warn(`[dashboard] groups matrix refresh failed: ${String(error)}`),
@@ -328,119 +611,6 @@ function resolveScheduleOwner(id) {
328
611
  const primary = registry.list().find(d => d.botIndex === 0);
329
612
  return primary?.larkAppId;
330
613
  }
331
- /**
332
- * Bring a freshly-onboarded bot online without a fleet-wide restart by spawning
333
- * `botmux start-bot <appId> --json` (see cli.ts:ensureBotDaemonStarted). The new
334
- * daemon is forked+supervised by pm2 (reparented off this process), self-registers
335
- * and opens its Feishu WSClient, then publishes a descriptor the DaemonRegistry
336
- * auto-discovers — so no dashboard reload is needed either. Runs `botmux` on the
337
- * SAME host as the dashboard (shared pm2 home / bots.json — the documented
338
- * dashboard↔daemon co-location assumption). Resolves best-effort; the caller
339
- * falls back to the restart hint on failure.
340
- */
341
- function spawnStartBotLive(appId) {
342
- return new Promise((resolve) => {
343
- let out = '';
344
- let err = '';
345
- let settled = false;
346
- const done = (r) => { if (!settled) {
347
- settled = true;
348
- resolve(r);
349
- } };
350
- try {
351
- const child = spawn(process.execPath, [botmuxCliEntry(), 'start-bot', appId, '--json'], {
352
- stdio: ['ignore', 'pipe', 'pipe'],
353
- env: process.env,
354
- // Run from HOME, not the dashboard's cwd (pm2 `cwd: PKG_ROOT`): a global
355
- // package update replaces that dir, so a still-running dashboard would spawn
356
- // start-bot in a deleted directory (uv_cwd/ENOENT). See globalInstallUpdateCwd.
357
- cwd: globalInstallUpdateCwd(),
358
- });
359
- const timer = setTimeout(() => {
360
- try {
361
- child.kill('SIGKILL');
362
- }
363
- catch { /* already gone */ }
364
- done({ ok: false, message: 'start-bot 超时(30s)' });
365
- }, 30_000);
366
- timer.unref?.();
367
- child.stdout?.on('data', (d) => { out += String(d); });
368
- child.stderr?.on('data', (d) => { err += String(d); });
369
- child.on('error', (e) => {
370
- clearTimeout(timer);
371
- done({ ok: false, message: e instanceof Error ? e.message : String(e) });
372
- });
373
- child.on('exit', (code) => {
374
- clearTimeout(timer);
375
- // `start-bot --json` prints a single StartBotLiveResult object; prefer its
376
- // own message/processName over the raw exit code.
377
- let parsed;
378
- try {
379
- parsed = JSON.parse(out.trim());
380
- }
381
- catch { /* non-JSON → fall through */ }
382
- if (code === 0) {
383
- done({ ok: true, message: parsed?.processName ? `${parsed.processName} 已上线` : undefined });
384
- }
385
- else {
386
- done({ ok: false, message: parsed?.message || err.trim() || `start-bot 退出码 ${code}` });
387
- }
388
- });
389
- }
390
- catch (e) {
391
- done({ ok: false, message: e instanceof Error ? e.message : String(e) });
392
- }
393
- });
394
- }
395
- function spawnStopBotLive(appId) {
396
- return new Promise((resolve) => {
397
- let out = '';
398
- let err = '';
399
- let settled = false;
400
- const done = (r) => { if (!settled) {
401
- settled = true;
402
- resolve(r);
403
- } };
404
- try {
405
- const child = spawn(process.execPath, [botmuxCliEntry(), 'stop-bot', appId, '--json'], {
406
- stdio: ['ignore', 'pipe', 'pipe'],
407
- env: process.env,
408
- cwd: globalInstallUpdateCwd(),
409
- });
410
- const timer = setTimeout(() => {
411
- try {
412
- child.kill('SIGKILL');
413
- }
414
- catch { /* already gone */ }
415
- done({ ok: false, message: 'stop-bot 超时(30s)' });
416
- }, 30_000);
417
- timer.unref?.();
418
- child.stdout?.on('data', (d) => { out += String(d); });
419
- child.stderr?.on('data', (d) => { err += String(d); });
420
- child.on('error', (e) => {
421
- clearTimeout(timer);
422
- done({ ok: false, message: e instanceof Error ? e.message : String(e) });
423
- });
424
- child.on('exit', (code) => {
425
- clearTimeout(timer);
426
- let parsed;
427
- try {
428
- parsed = JSON.parse(out.trim());
429
- }
430
- catch { /* non-JSON */ }
431
- if (code === 0) {
432
- done({ ok: true, message: parsed?.processName ? `${parsed.processName} 已停止` : undefined });
433
- }
434
- else {
435
- done({ ok: false, message: parsed?.message || err.trim() || `stop-bot 退出码 ${code}` });
436
- }
437
- });
438
- }
439
- catch (e) {
440
- done({ ok: false, message: e instanceof Error ? e.message : String(e) });
441
- }
442
- });
443
- }
444
614
  const botOnboarding = new BotOnboardingManager({
445
615
  botsJsonPath: BOTS_JSON_PATH,
446
616
  stopBotLive: spawnStopBotLive,
@@ -1025,6 +1195,7 @@ function resolveDashboardSettings() {
1025
1195
  localDevInstall: isLocalDevInstall(),
1026
1196
  autoUpdateSupported: lastSuccessfulUpdatePlan !== undefined || tryResolveGlobalInstallPlan() !== null,
1027
1197
  whiteboard: { enabled: global.whiteboard?.enabled === true },
1198
+ workflow: { enabled: global.workflow?.enabled !== false }, // default ON
1028
1199
  remoteAccess: global.remoteAccess === true,
1029
1200
  scheduleTimeZone: global.scheduleTimeZone ?? null,
1030
1201
  hostTimeZone: hostLocalTimeZone(),
@@ -1102,6 +1273,14 @@ let updateInFlight = false;
1102
1273
  // the successful plan (including its stable package root) so follow-up status,
1103
1274
  // update, and restart requests do not reuse the removed old runtime realpath.
1104
1275
  let lastSuccessfulUpdatePlan;
1276
+ // Local-dev counterpart: the checkout a successful /api/update/run built, and
1277
+ // its post-build HEAD. Pinned so the follow-up /api/update/restart applies THIS
1278
+ // build's target — not a wrapper that a concurrent `pnpm use:here` in another
1279
+ // worktree may have re-pointed between the two requests (run builds B, wrapper
1280
+ // flips to C, restart would otherwise restart C or fall back to A). Cleared
1281
+ // once consumed by a restart. A plain "restart" (no preceding run) still
1282
+ // resolves the wrapper live.
1283
+ let pendingLocalDevRestart;
1105
1284
  // Cache the upstream version/changelog lookups so the nav-badge check + the
1106
1285
  // Settings card don't hammer the npm registry / GitHub on every page load.
1107
1286
  // GitHub's unauthenticated API is only 60 req/h per IP, so caching the changelog
@@ -1181,54 +1360,72 @@ function currentInstalledVersion() {
1181
1360
  return version === '0.0.0' ? resolveCurrentVersion() : version;
1182
1361
  }
1183
1362
  /**
1184
- * Run the ownership-aware npm/pnpm/Bun update for the manual-update flow WITHOUT blocking
1185
- * the event loop (async spawn, not execSync the dashboard must keep serving
1186
- * during the ~10-30s install). Resolves on exit 0; rejects with the tail of
1187
- * stdout/stderr on a non-zero exit, spawn error, or 3-minute timeout. Args are
1188
- * a fixed literal no shell interpolation of untrusted input.
1363
+ * Local-dev update: git-clean check (fail closed) git pull --ff-only
1364
+ * pnpm build, all in the checkout the global wrapper points at. Mirrors the CLI
1365
+ * `cmdUpgradeLocalDev` via the shared local-dev-update helpers. Returns the
1366
+ * checkout dir, its version before/after, and whether HEAD advanced; the caller
1367
+ * applies the restart through the existing lease/intent path. A successful
1368
+ * build always requires a restart to take effect (dist/ is regenerated), which
1369
+ * the caller signals independently of `changed`. Rejects with a stable `code`
1370
+ * on the recoverable, UI-actionable failures.
1189
1371
  */
1190
- function runGlobalInstall(plan) {
1191
- return new Promise((resolve, reject) => {
1192
- const child = spawn(plan.command, plan.args, {
1193
- cwd: globalInstallUpdateCwd(),
1194
- env: { ...process.env, ...plan.env },
1195
- stdio: ['ignore', 'pipe', 'pipe'],
1196
- shell: process.platform === 'win32', // resolve npm.cmd / pnpm.cmd / bun.exe
1197
- });
1198
- let tail = '';
1199
- const capture = (d) => { tail = (tail + d.toString()).slice(-2000); };
1200
- child.stdout?.on('data', capture);
1201
- child.stderr?.on('data', capture);
1202
- const timer = setTimeout(() => {
1203
- child.kill('SIGKILL');
1204
- reject(new Error(`${plan.manager} install timed out after 180s`));
1205
- }, 180_000);
1206
- child.on('error', (e) => { clearTimeout(timer); reject(e); });
1207
- child.on('exit', (code) => {
1208
- clearTimeout(timer);
1209
- if (code === 0)
1210
- resolve();
1211
- else
1212
- reject(new Error(`${plan.manager} exited ${code}: ${tail.trim().slice(-500)}`));
1372
+ async function runLocalDevUpdate() {
1373
+ const dir = resolveLocalDevCheckoutDir();
1374
+ if (!isGitWorktree(dir)) {
1375
+ throw Object.assign(new Error(`${dir} is not a git worktree`), { code: 'not_a_worktree', dir });
1376
+ }
1377
+ let status;
1378
+ try {
1379
+ status = gitPorcelainStatus(dir);
1380
+ }
1381
+ catch (e) {
1382
+ throw Object.assign(new Error(e instanceof Error ? e.message : String(e)), { code: 'git_status_failed', dir });
1383
+ }
1384
+ if (status) {
1385
+ throw Object.assign(new Error('working tree has uncommitted changes'), {
1386
+ code: 'dirty_worktree', dir, status,
1213
1387
  });
1214
- });
1388
+ }
1389
+ const before = gitHeadSha(dir);
1390
+ const oldVersion = resolveCurrentVersionAt(dir);
1391
+ for (const { command, args } of localDevUpdateSteps()) {
1392
+ await runLocalDevStep(dir, command, args);
1393
+ }
1394
+ const after = gitHeadSha(dir);
1395
+ const newVersion = resolveCurrentVersionAt(dir);
1396
+ return { dir, changed: before === '' || after === '' ? true : before !== after, oldVersion, newVersion, head: after };
1215
1397
  }
1216
1398
  /**
1217
1399
  * Attach to one daemon: hydrate its sessions/schedules into the aggregator,
1218
- * THEN open the SSE subscription. Order matters — hydrating after subscribe
1219
- * would let snapshot data clobber events that arrived between subscribe and
1220
- * the snapshot fetch.
1400
+ * THEN open the SSE subscription.
1401
+ *
1402
+ * The subscription runs a snapshot barrier (subscribeDaemon's `onConnected`):
1403
+ * after every stream establishment — the first included — and BEFORE any
1404
+ * frame is read, we install a fresh authoritative snapshot while incoming
1405
+ * frames stay queued in the stream; frames then apply on top in order. This
1406
+ * gives two guarantees at once:
1407
+ *
1408
+ * 1. No reverse clobber: the barrier snapshot is installed before any frame
1409
+ * is applied, so a slow snapshot response can never overwrite state that
1410
+ * a faster SSE event already delivered (a naive post-subscribe hydrate
1411
+ * would).
1412
+ * 2. No forward gap: events fired between step 1 below and the stream
1413
+ * handshake are picked up by the barrier snapshot, and events missed
1414
+ * during a drop are recovered by the barrier re-run on reconnect.
1415
+ *
1416
+ * The blocking hydrate in step 1 still matters: it populates the cache
1417
+ * before the dashboard starts serving, and keeps a daemon's last-known
1418
+ * state visible even if its SSE stream never connects.
1221
1419
  *
1222
1420
  * Idempotent: a second call for the same daemon while one is in flight is a
1223
- * no-op; a call after attach finished re-hydrates (useful when a daemon
1224
- * restarts and we want to refresh its slice of the cache).
1421
+ * no-op; the subscription itself is installed once.
1225
1422
  */
1226
1423
  async function attachDaemon(d) {
1227
1424
  if (attaching.has(d.larkAppId))
1228
1425
  return;
1229
1426
  attaching.add(d.larkAppId);
1230
1427
  try {
1231
- // 1. Hydrate snapshot (blocking — completes before we wire SSE)
1428
+ // 1. Blocking snapshot (see above)
1232
1429
  try {
1233
1430
  const [sRes, schRes] = await Promise.all([
1234
1431
  fetchDaemonIpc(d.ipcPort, '/api/sessions'),
@@ -1240,20 +1437,42 @@ async function attachDaemon(d) {
1240
1437
  aggregator.hydrateSessions(d.larkAppId, rows);
1241
1438
  for (const row of rows)
1242
1439
  sessionPresentation.schedule(d.larkAppId, row);
1243
- aggregator.hydrateSchedules(sch.schedules ?? []);
1440
+ aggregator.hydrateSchedules(d.larkAppId, sch.schedules ?? []);
1244
1441
  }
1245
1442
  catch (e) {
1246
1443
  logger.warn(`[dashboard] hydrate ${d.larkAppId}: ${e.message ?? e}`);
1247
1444
  }
1248
- // 2. Open SSE subscription if not already (idempotent)
1445
+ // 2. Open SSE subscription if not already (idempotent). The barrier
1446
+ // below runs inside subscribeDaemon, after the stream is established.
1249
1447
  if (!subs.has(d.larkAppId)) {
1250
- subs.set(d.larkAppId, subscribeDaemon(d, aggregator, e => logger.warn(`[aggregator] ${d.larkAppId}: ${e.message}`), (_url, init) => fetchDaemonIpc(d.ipcPort, '/api/events', init)));
1448
+ subs.set(d.larkAppId, subscribeDaemon(d, aggregator, e => logger.warn(`[aggregator] ${d.larkAppId}: ${e.message}`), (_url, init) => fetchDaemonIpc(d.ipcPort, '/api/events', init),
1449
+ // Snapshot barrier: install an authoritative snapshot before any
1450
+ // frame is read. Frames arriving during this fetch stay queued in
1451
+ // the stream and apply afterwards, so the snapshot can never
1452
+ // clobber fresher SSE state; on reconnect it recovers missed
1453
+ // events. The subscription signal is the generation arbitration:
1454
+ // if aborted mid-flight (daemon offline, newer generation), the
1455
+ // snapshot is discarded instead of clobbering the new generation.
1456
+ signal => reconcileDaemon(d, signal)));
1251
1457
  }
1252
1458
  }
1253
1459
  finally {
1254
1460
  attaching.delete(d.larkAppId);
1255
1461
  }
1256
1462
  }
1463
+ /**
1464
+ * Reconcile one daemon's snapshot into the aggregator (subscribeDaemon
1465
+ * barrier). Thin wrapper over reconcileDaemonSnapshot that also schedules
1466
+ * presentation enrichment for the session rows.
1467
+ */
1468
+ async function reconcileDaemon(d, signal) {
1469
+ const snapshot = await reconcileDaemonSnapshot(d, aggregator, signal);
1470
+ if (!snapshot)
1471
+ return;
1472
+ for (const row of snapshot.sessions) {
1473
+ sessionPresentation.schedule(d.larkAppId, row);
1474
+ }
1475
+ }
1257
1476
  function syncSubscriptions() {
1258
1477
  const daemons = registry.list();
1259
1478
  const online = new Set(daemons.map(d => d.larkAppId));
@@ -1396,7 +1615,7 @@ function injectDevReload(html) {
1396
1615
  const snippet = devReloadSnippet();
1397
1616
  return html.includes('</body>') ? html.replace('</body>', `${snippet}\n</body>`) : `${html}\n${snippet}`;
1398
1617
  }
1399
- function serveStatic(req, res, pathname) {
1618
+ function serveStatic(req, res, pathname, options = {}) {
1400
1619
  const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
1401
1620
  const fp = resolve(WEB_DIR, rel);
1402
1621
  const webRoot = resolve(WEB_DIR);
@@ -1413,13 +1632,17 @@ function serveStatic(req, res, pathname) {
1413
1632
  // and can be cached immutably once the current app.js points at them.
1414
1633
  const immutableChunk = relToRoot.startsWith('chunks/') || relToRoot.startsWith('chunks\\');
1415
1634
  const etag = `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
1416
- const devIndex = relToRoot === 'index.html' && dashboardDevReloadEnabled();
1635
+ const isIndex = relToRoot === 'index.html';
1636
+ const devIndex = isIndex && dashboardDevReloadEnabled();
1637
+ // 注入 CSRF 票据的壳是每次请求现生成的:ETag 只反映磁盘文件,走 304 会让浏览
1638
+ // 器复用上一次(可能已随认证结束作废)的票据,所以这条路径不缓存、不 304。
1639
+ const dynamicIndex = devIndex || (isIndex && !!options.injectHtml);
1417
1640
  const headers = {
1418
1641
  'content-type': MIME[extname(fp)] ?? 'application/octet-stream',
1419
- 'cache-control': devIndex ? 'no-store' : immutableChunk ? 'public, max-age=31536000, immutable' : 'no-cache',
1642
+ 'cache-control': dynamicIndex ? 'no-store' : immutableChunk ? 'public, max-age=31536000, immutable' : 'no-cache',
1420
1643
  etag,
1421
1644
  };
1422
- if (!devIndex && req.headers['if-none-match'] === etag) {
1645
+ if (!dynamicIndex && req.headers['if-none-match'] === etag) {
1423
1646
  res.writeHead(304, headers);
1424
1647
  res.end();
1425
1648
  return true;
@@ -1429,8 +1652,13 @@ function serveStatic(req, res, pathname) {
1429
1652
  res.end();
1430
1653
  return true;
1431
1654
  }
1432
- if (devIndex) {
1433
- res.end(injectDevReload(readFileSync(fp, 'utf8')));
1655
+ if (dynamicIndex) {
1656
+ let html = readFileSync(fp, 'utf8');
1657
+ if (options.injectHtml)
1658
+ html = options.injectHtml(html);
1659
+ if (devIndex)
1660
+ html = injectDevReload(html);
1661
+ res.end(html);
1434
1662
  }
1435
1663
  else {
1436
1664
  res.end(readFileSync(fp));
@@ -1911,6 +2139,7 @@ function configuredBotAgentFields() {
1911
2139
  wrapperCli: b.wrapperCli,
1912
2140
  model: b.model,
1913
2141
  reasoningEffort: b.reasoningEffort,
2142
+ turnTimeoutMs: b.turnTimeoutMs,
1914
2143
  }]));
1915
2144
  }
1916
2145
  catch {
@@ -1928,6 +2157,7 @@ function withConfiguredCliId(bot, ids) {
1928
2157
  wrapperCli: bot.wrapperCli || fallback?.wrapperCli,
1929
2158
  model: bot.model || fallback?.model,
1930
2159
  reasoningEffort: bot.reasoningEffort || fallback?.reasoningEffort,
2160
+ turnTimeoutMs: bot.turnTimeoutMs ?? fallback?.turnTimeoutMs,
1931
2161
  };
1932
2162
  }
1933
2163
  function liveBots() {
@@ -2205,9 +2435,11 @@ async function closeSessionsMatching(pred) {
2205
2435
  body = JSON.parse(text);
2206
2436
  }
2207
2437
  catch { /* tolerate */ }
2438
+ const residual = body?.ok ? parseCloseResidual(body) : undefined;
2208
2439
  return {
2209
2440
  sessionId: s.sessionId,
2210
2441
  ok: !!body?.ok,
2442
+ ...(residual ? { residual } : {}),
2211
2443
  error: body?.ok ? undefined : (body?.error ?? `http_${upstream.status}`),
2212
2444
  };
2213
2445
  }
@@ -2331,8 +2563,9 @@ function dashboardSkillCliIds() {
2331
2563
  const ids = new Set();
2332
2564
  // Always scan all known CLI skill dirs, not just configured bots — users may
2333
2565
  // want to discover codex/trae/... skills even before creating a bot for them.
2334
- const allCliIds = ['claude-code', 'seed', 'relay', 'aiden', 'coco', 'codex', 'codex-app', 'cursor', 'gemini', 'genius', 'opencode', 'opencode2', 'antigravity', 'mtr', 'hermes', 'mira', 'mir', 'traex', 'pi', 'copilot', 'oh-my-pi', 'kimi', 'grok', 'kiro-cli', 'riff', 'reasonix', 'dsh'];
2335
- for (const cliId of allCliIds)
2566
+ // Derived from the closed Record<CliId,…> in the registry a hand-typed
2567
+ // literal here silently omitted reasonix and mojo, hiding their skill dirs.
2568
+ for (const cliId of ALL_CLI_IDS)
2336
2569
  ids.add(cliId);
2337
2570
  try {
2338
2571
  for (const cliId of configuredCliIds().values())
@@ -2492,14 +2725,51 @@ async function dashboardSkillReferencesMany(skillNames) {
2492
2725
  async function dashboardSkillReferences(skillName) {
2493
2726
  return (await dashboardSkillReferencesMany([skillName])).get(skillName) ?? { bots: [], packs: [] };
2494
2727
  }
2495
- /** Extract the sessionId from a terminal path `/s/<sessionId>[/...]`. Returns
2496
- * the first path segment after `/s/` (stops at the next `/`; query/hash are
2497
- * already stripped by URL.pathname). undefined when there's no segment. */
2498
- function parseTerminalSessionId(pathname) {
2499
- if (!pathname.startsWith('/s/'))
2500
- return undefined;
2501
- const seg = pathname.slice(3).split('/')[0];
2502
- return seg || undefined;
2728
+ function dashboardControlJson(res, status, body) {
2729
+ res.writeHead(status, {
2730
+ 'content-type': 'application/json; charset=utf-8',
2731
+ 'cache-control': 'no-store',
2732
+ 'referrer-policy': 'no-referrer',
2733
+ });
2734
+ res.end(JSON.stringify(body));
2735
+ }
2736
+ /**
2737
+ * P1-11:控制类端点的跨站伪造门禁。返回 false 表示已经写完 403 响应,调用方直接
2738
+ * return。只作用于「有副作用且可被无 body 表单触发」的 POST:终端接管/释放、
2739
+ * 预览交互 unlock/activity/lock、话题定位。
2740
+ *
2741
+ * GET 不进门禁(读没有副作用,且壳/前端的状态轮询本身就是 GET);Preview 自身的
2742
+ * 不透明来源请求走 preview 专用路径的路径内凭据,不经过这里。
2743
+ */
2744
+ function enforceControlCsrf(req, res, identity) {
2745
+ if ((req.method ?? 'GET').toUpperCase() === 'GET')
2746
+ return true;
2747
+ const verdict = guardControlRequest({
2748
+ headers: req.headers,
2749
+ authSessionId: identity.authSessionId,
2750
+ tokens: controlCsrfTokens,
2751
+ });
2752
+ if (verdict.ok)
2753
+ return true;
2754
+ dashboardControlJson(res, verdict.status, { ok: false, error: verdict.error });
2755
+ return false;
2756
+ }
2757
+ function terminalControlAvailability(sessionId) {
2758
+ const row = aggregator.getSession(sessionId);
2759
+ if (!row || !aggregator.ownerOf(sessionId))
2760
+ return { ok: false, status: 404, error: 'unknown_session' };
2761
+ if (row.status === 'closed')
2762
+ return { ok: false, status: 409, error: 'session_not_active' };
2763
+ if (typeof row.riffAccessUrl === 'string' && row.riffAccessUrl) {
2764
+ return { ok: false, status: 409, error: 'terminal_external_only' };
2765
+ }
2766
+ const owner = aggregator.ownerOf(sessionId);
2767
+ if (!owner || !registry.getByAppId(owner))
2768
+ return { ok: false, status: 503, error: 'daemon_offline' };
2769
+ if (!aggregator.terminalProxyPortOf(sessionId) || typeof row.webPort !== 'number') {
2770
+ return { ok: false, status: 409, error: 'terminal_unavailable' };
2771
+ }
2772
+ return { ok: true };
2503
2773
  }
2504
2774
  /**
2505
2775
  * Read every currently-online daemon directly for the public dashboard
@@ -2578,6 +2848,19 @@ const server = createServer(async (req, res) => {
2578
2848
  handleDesktopCompat(req, res, url, { machineId: compatMachineId ?? undefined });
2579
2849
  return;
2580
2850
  }
2851
+ // Configurable Feishu/Lark H5 passwordless entry and code exchange. It is
2852
+ // self-authenticating and must run before the ordinary Dashboard cookie
2853
+ // gate; no authorization code or session capability is ever put in a URL.
2854
+ if (await dashboardH5Auth.handle(req, res, url))
2855
+ return;
2856
+ // Session Web preview is an authenticated, same-origin reverse proxy to an
2857
+ // agent-registered literal loopback target. It owns its auth gate because
2858
+ // WebSocket upgrades do not pass through decideDashboardAuth; using one
2859
+ // manager for HTTP + WS keeps the cookie/ownership/SSRF contract identical.
2860
+ if (previewGuardPage.handle(req, res, url))
2861
+ return;
2862
+ if (await sessionPreviewProxy.handleHttp(req, res, url))
2863
+ return;
2581
2864
  // Web terminal reverse-proxy: `/s/<sessionId>/*` → the owning bot daemon's
2582
2865
  // terminal proxy. The central platform only tunnels the dashboard port, so
2583
2866
  // terminal links served under the machine subdomain
@@ -2588,25 +2871,13 @@ const server = createServer(async (req, res) => {
2588
2871
  // response straight back. Mounted before the dashboard auth gate because the
2589
2872
  // worker independently requires a view/write capability or authenticated
2590
2873
  // dashboard cookie before serving either HTTP or WebSocket terminal data.
2591
- if (url.pathname === '/s' || url.pathname.startsWith('/s/')) {
2592
- const sessionId = parseTerminalSessionId(url.pathname);
2593
- const tport = sessionId ? aggregator.terminalProxyPortOf(sessionId) : undefined;
2594
- if (!tport) {
2595
- res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
2596
- return res.end('session terminal not available');
2597
- }
2598
- const upstream = httpRequest({ host: '127.0.0.1', port: tport, method: req.method, path: req.url, headers: req.headers }, (up) => {
2599
- res.writeHead(up.statusCode ?? 502, up.headers);
2600
- up.pipe(res);
2601
- });
2602
- upstream.on('error', () => {
2603
- if (!res.headersSent)
2604
- res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
2605
- res.end('terminal proxy error');
2606
- });
2607
- req.pipe(upstream);
2874
+ // Since P1-5 this hop is also LOAD-BEARING for revocation, not just
2875
+ // reachability: a signed view capability is checked here against live auth
2876
+ // sessions and countersigned for the worker, which refuses one that arrived
2877
+ // any other way. That is why the view-link API hands out a same-origin path
2878
+ // instead of the daemon/worker origin — this is the only door.
2879
+ if (terminalFrontProxy.handleHttp(req, res, url))
2608
2880
  return;
2609
- }
2610
2881
  if (await handleWebhookRoute(req, res, url, {
2611
2882
  proxyToDaemon,
2612
2883
  createLifecycleGroup: createLifecycleGroupForWebhook,
@@ -2677,8 +2948,27 @@ const server = createServer(async (req, res) => {
2677
2948
  const gate = verifyCliRequest(req, url.pathname);
2678
2949
  if (!gate.ok)
2679
2950
  return jsonRes(res, gate.status, gate.body);
2951
+ const previousToken = currentDashboardToken();
2680
2952
  try {
2681
2953
  const token = rotatePersistedToken(TOKEN_PATH);
2954
+ // The previous link is dead once the write lands: drop terminal control
2955
+ // leases, re-lock preview interaction, and (P1-8) close every long-lived
2956
+ // connection the old link opened — otherwise the old management SSE
2957
+ // keeps streaming rows minted AFTER the rotation, including freshly
2958
+ // issued `riffAccessUrl` write credentials.
2959
+ // Ordered after the durable write so a failed rotation keeps both the
2960
+ // old token and its live grants intact.
2961
+ if (previousToken && previousToken !== token) {
2962
+ endDashboardAuthSession(legacyDashboardAuthSessionId(previousToken));
2963
+ }
2964
+ // P1-6: the same reasoning covers the workbench entry tickets sitting in
2965
+ // Feishu card history. They redeem into "whatever token is active now",
2966
+ // so without this a ticket leaked BEFORE the rotation would hand out the
2967
+ // freshly minted management cookie — rotation would protect nothing in
2968
+ // exactly the case it is used for. Ticket verification independently
2969
+ // requires the bound generation to still be current, so this call is the
2970
+ // cleanup (drop dead rows from the shared file), not the guarantee.
2971
+ revokeWorkbenchTicketsOutsideGeneration(workbenchTicketGeneration(token));
2682
2972
  return jsonRes(res, 200, dashboardUrlsFor(token));
2683
2973
  }
2684
2974
  catch (e) {
@@ -2737,29 +3027,60 @@ const server = createServer(async (req, res) => {
2737
3027
  /* ignore */
2738
3028
  }
2739
3029
  platformTunnel = null;
3030
+ // P1-8:解绑/改绑之后旧平台身份的短请求立刻 401,但它建立的 SSE / Preview
3031
+ // 长连接不会自己断,三个角色作用域在这里一起收口。
3032
+ syncPlatformBindingRevocation();
2740
3033
  startPlatformTunnelIfBound();
2741
3034
  return jsonRes(res, 200, { ok: true });
2742
3035
  }
2743
3036
  const activeToken = currentDashboardToken();
2744
- const presentedToken = authedToken(req, url, activeToken);
3037
+ const requestIdentity = dashboardRequestIdentity(req);
2745
3038
  const globalDashboardConfig = readGlobalConfig().dashboard;
2746
- const decision = decideDashboardAuth({
3039
+ const publicReadOnly = globalDashboardConfig?.publicReadOnly
3040
+ ?? config.dashboard.publicReadOnly;
3041
+ // P1-7:门禁选择与身份判定同源。旧代码在这里另算一遍 `h5Identity`,于是
3042
+ // 「legacy owner + H5 cookie 并存」的浏览器被判成 workbench-only,管理读写
3043
+ // 全 401,连正确的 `?t=` 也被一起清掉(详见 request-identity.ts 顶注)。
3044
+ // Only the local legacy Dashboard cookie is management authority. Platform
3045
+ // identities — owner included — retain terminal/preview capability through
3046
+ // signed proxy grants, but cannot cross into host administration APIs.
3047
+ const { legacyAuthed, workbenchOnlyIdentity, decision } = resolveDashboardRequestGate({
2747
3048
  method: req.method ?? 'GET',
2748
3049
  pathname: url.pathname,
2749
3050
  hasTokenParam: url.searchParams.has('t'),
2750
- presentedToken,
2751
- activeToken: activeToken ?? '',
2752
- publicReadOnly: globalDashboardConfig?.publicReadOnly
2753
- ?? config.dashboard.publicReadOnly,
3051
+ identity: requestIdentity,
3052
+ tokenFromRequest: authedToken(req, url, activeToken),
3053
+ activeToken,
3054
+ publicReadOnly,
3055
+ });
3056
+ // `authed` is deliberately the local management capability, not merely a
3057
+ // valid Workbench/platform identity. Privileged mutations and management
3058
+ // reads (settings, schedules, groups) therefore cannot be widened by H5
3059
+ // authentication.
3060
+ const authed = legacyAuthed;
3061
+ // The session board is the one surface where `!authed` must NOT mean
3062
+ // "anonymous". `/api/sessions` and `/events` are exactly the two paths
3063
+ // workbenchH5Capability grants as `workbench.view`, and the same identity
3064
+ // holds `preview.view`/`preview.operate`; reusing the anonymous projection
3065
+ // here deleted the `preview` descriptor those capabilities operate on, so
3066
+ // the mobile Workbench and the Dock showed 「无网页预览」 unconditionally.
3067
+ // The three-way audience keeps management/anonymous behavior byte-identical
3068
+ // and only restores display fields for an authenticated Workbench viewer —
3069
+ // the Riff sandbox bearer write URL stays stripped (see public-redact.ts).
3070
+ const sessionBoardAudience = sessionBoardAudienceFor({
3071
+ legacyAuthed,
3072
+ workbenchIdentity: workbenchOnlyIdentity,
2754
3073
  });
2755
- // `authed` is consumed by route handlers that distinguish the public-read
2756
- // carve-out from a valid management cookie (notably v3 run details).
2757
- const authed = !!presentedToken && presentedToken === activeToken && !!activeToken;
2758
3074
  if (decision.kind === 'deny401') {
2759
3075
  const loginUrl = buildPlatformDashboardLoginUrl();
2760
3076
  res.writeHead(401, {
2761
3077
  'content-type': 'text/html; charset=utf-8',
2762
3078
  'cache-control': 'no-store',
3079
+ // A valid H5/platform Workbench identity is expected to be denied by
3080
+ // management-only endpoints such as /api/settings. Let the SPA tell
3081
+ // that narrow denial from an expired identity, otherwise its global
3082
+ // fetch wrapper covers a healthy Workbench with the login overlay.
3083
+ ...(workbenchOnlyIdentity ? { 'x-botmux-auth-scope': 'workbench' } : {}),
2763
3084
  ...(loginUrl ? { 'x-botmux-login-url': loginUrl } : {}),
2764
3085
  });
2765
3086
  res.end('<h1>Token expired</h1><p>Run <code>botmux dashboard</code> to get a fresh URL.</p>');
@@ -2773,6 +3094,20 @@ const server = createServer(async (req, res) => {
2773
3094
  res.end();
2774
3095
  return;
2775
3096
  }
3097
+ // P2-1:飞书卡片「打开工作台」按钮的短时票据兑换,紧挨上面的 ?t= set-cookie
3098
+ // 流程——语义同款:验票通过就种同一个 legacy cookie,再 302 进工作台。票据由
3099
+ // /dashboard 卡片构建时现 mint(TTL 30 分钟、可多端重复打开,落盘只存 hash,
3100
+ // 见 dashboard/workbench-ticket.ts),长期管理 token 从此不再写进持久化卡片;
3101
+ // 无效/过期回一个无凭据中文提示页。该 GET 在 decideDashboardAuth 里与静态壳
3102
+ // 同级放行(票据本身就是凭证),其余方法不豁免。P1-10:正因为它在 auth gate
3103
+ // 之前放行,端点自带每 IP + 全局限流,IP 口径与 H5 兑换口共用同一份可信代理
3104
+ // 配置——否则两个公开面对同一个 `x-forwarded-for` 会得出不同结论。
3105
+ if (handleWorkbenchTicketRedemption(req, res, url, {
3106
+ activeToken: () => activeToken,
3107
+ trustedProxyHops: dashboardH5AuthConfig.trustedProxyHops,
3108
+ })) {
3109
+ return;
3110
+ }
2776
3111
  if (url.pathname === '/api/workflows' || url.pathname.startsWith('/api/workflows/')) {
2777
3112
  return jsonRes(res, 410, {
2778
3113
  ok: false,
@@ -2780,6 +3115,128 @@ const server = createServer(async (req, res) => {
2780
3115
  message: 'v2 workflow dashboard APIs are retired; use /api/v3/runs for v3 run visibility',
2781
3116
  });
2782
3117
  }
3118
+ // Authenticated, non-secret metadata used only to build Feishu appCenter
3119
+ // and >=350px sidebar AppLinks. App secret and allowlist never cross this
3120
+ // projection; the route is intentionally absent from public-read allowlists.
3121
+ if (req.method === 'GET' && url.pathname === '/api/workbench/h5-context') {
3122
+ return jsonRes(res, 200, {
3123
+ ok: true,
3124
+ h5: {
3125
+ enabled: dashboardH5AuthConfig.enabled,
3126
+ appId: dashboardH5AuthConfig.appId,
3127
+ brand: dashboardH5AuthConfig.brand,
3128
+ entryPath: dashboardH5AuthConfig.entryPath,
3129
+ },
3130
+ });
3131
+ }
3132
+ // P1-4:最小操作能力集投影。前端只据此渲染操作入口(定位 / 接管输入 / 开启
3133
+ // 交互),投影函数复算的是本文件三条真实路由的同一套门禁(路由级 auth 决策 +
3134
+ // terminalCapability/previewCapability 角色检查),见 auth.ts 的函数注释。
3135
+ // 匿名请求到不了这里(该路径不在 publicReadOnly 白名单,decideDashboardAuth
3136
+ // 已 401);前端把任何非 200/缺字段一律回落为全 false。注意 canControl 只描述
3137
+ // 无显式 token 的默认能力——显式 write token 走终端前置代理的独立授权(P1-6),
3138
+ // 与本投影无关。
3139
+ if (req.method === 'GET' && url.pathname === '/api/workbench/capabilities') {
3140
+ return jsonRes(res, 200, {
3141
+ ok: true,
3142
+ capabilities: projectWorkbenchOperationCapabilities(requestIdentity),
3143
+ });
3144
+ }
3145
+ // owner 在工作台内自取常驻链接(`<base>/workbench?t=<当前活跃 token>`)。
3146
+ // 只有本机完整管理身份能取:上面的门禁已经把 workbench-only / 平台角色 /
3147
+ // 匿名 deny401,处理器再自己判一次 kind === 'legacy-dashboard'(两层独立,
3148
+ // 见 dashboard/standing-link.ts 顶注)。同源校验 + no-store + 每次落一条
3149
+ // `auth.standing_link_issued` 审计;token 现读落盘的活跃值,所以
3150
+ // `dashboard rotate` 之后这里自然发新链接。
3151
+ if (handleWorkbenchStandingLink(req, res, url, {
3152
+ identity: requestIdentity,
3153
+ activeToken: () => activeToken,
3154
+ standingLinkUrl: token => workbenchEntryUrl(dashboardUrlsFor(token).url),
3155
+ audit: dashboardControlAudit,
3156
+ })) {
3157
+ return;
3158
+ }
3159
+ // Server-authoritative terminal control lease. The API returns only mode
3160
+ // and timestamps; its signed read/write grant stays inside the central
3161
+ // proxy and is never placed in a URL or response body.
3162
+ let controlMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/control(?:\/(takeover|release))?$/);
3163
+ if (controlMatch) {
3164
+ if (!requestIdentity) {
3165
+ return dashboardControlJson(res, 401, { ok: false, error: 'authentication_required' });
3166
+ }
3167
+ if (!enforceControlCsrf(req, res, requestIdentity))
3168
+ return;
3169
+ let sessionId;
3170
+ try {
3171
+ sessionId = decodeURIComponent(controlMatch[1]);
3172
+ }
3173
+ catch {
3174
+ return dashboardControlJson(res, 400, { ok: false, error: 'invalid_session_id' });
3175
+ }
3176
+ const availability = terminalControlAvailability(sessionId);
3177
+ if (!availability.ok) {
3178
+ return dashboardControlJson(res, availability.status, { ok: false, error: availability.error });
3179
+ }
3180
+ const action = controlMatch[2];
3181
+ if (req.method === 'GET' && !action) {
3182
+ return dashboardControlJson(res, 200, { ok: true, ...terminalControl.state(requestIdentity, sessionId) });
3183
+ }
3184
+ if (requestIdentity.terminalCapability === 'readonly') {
3185
+ return dashboardControlJson(res, 403, { ok: false, error: 'terminal_operation_forbidden' });
3186
+ }
3187
+ if (req.method === 'POST' && action === 'takeover') {
3188
+ const result = terminalControl.takeover(requestIdentity, sessionId);
3189
+ const status = result.ok ? 200 : result.error === 'control_busy' ? 409 : 401;
3190
+ return dashboardControlJson(res, status, result.ok ? { ...result, owned: true } : { ok: false, error: result.error });
3191
+ }
3192
+ if (req.method === 'POST' && action === 'release') {
3193
+ const result = terminalControl.release(requestIdentity, sessionId);
3194
+ return dashboardControlJson(res, result.ok ? 200 : 403, result.ok ? { ...result, owned: false } : { ok: false, error: result.error });
3195
+ }
3196
+ return dashboardControlJson(res, 405, { ok: false, error: 'method_not_allowed' });
3197
+ }
3198
+ // Preview interaction is separately scoped per authenticated browser
3199
+ // session. Default is always the visibly labelled preview overlay; unlock
3200
+ // and activity are explicit, and the server hard-relocks after 15m idle.
3201
+ controlMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/preview-interaction(?:\/(unlock|activity|lock))?$/);
3202
+ if (controlMatch) {
3203
+ if (!requestIdentity) {
3204
+ return dashboardControlJson(res, 401, { ok: false, error: 'authentication_required' });
3205
+ }
3206
+ if (!enforceControlCsrf(req, res, requestIdentity))
3207
+ return;
3208
+ let sessionId;
3209
+ try {
3210
+ sessionId = decodeURIComponent(controlMatch[1]);
3211
+ }
3212
+ catch {
3213
+ return dashboardControlJson(res, 400, { ok: false, error: 'invalid_session_id' });
3214
+ }
3215
+ const resolution = resolveDashboardSessionPreview(sessionId);
3216
+ if (!resolution.ok) {
3217
+ return dashboardControlJson(res, resolution.status, { ok: false, error: resolution.error });
3218
+ }
3219
+ const action = controlMatch[2];
3220
+ if (req.method === 'GET' && !action) {
3221
+ return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.state(requestIdentity, sessionId) });
3222
+ }
3223
+ // 唯一权威的角色门禁。guard 壳与工作台是否渲染解锁按钮,走的是同一个
3224
+ // previewInteractionWriteAllowed(经 canInteract 投影),所以「按能力隐藏
3225
+ // 按钮」永远只是把一次必然 403 的点击省掉,不会替代这里的检查。
3226
+ if (!previewInteractionWriteAllowed(requestIdentity)) {
3227
+ return dashboardControlJson(res, 403, { ok: false, error: 'preview_operation_forbidden' });
3228
+ }
3229
+ if (req.method === 'POST' && action === 'unlock') {
3230
+ return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.unlock(requestIdentity, sessionId) });
3231
+ }
3232
+ if (req.method === 'POST' && action === 'activity') {
3233
+ return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.activity(requestIdentity, sessionId) });
3234
+ }
3235
+ if (req.method === 'POST' && action === 'lock') {
3236
+ return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.lock(requestIdentity, sessionId) });
3237
+ }
3238
+ return dashboardControlJson(res, 405, { ok: false, error: 'method_not_allowed' });
3239
+ }
2783
3240
  if (url.pathname.startsWith('/api/feedback/analytics/')) {
2784
3241
  await handleFeedbackAnalyticsApi(req, res, url, { service: analyticsService() });
2785
3242
  return;
@@ -2815,6 +3272,10 @@ const server = createServer(async (req, res) => {
2815
3272
  if (url.pathname === '/api/debug-terminal'
2816
3273
  || url.pathname.startsWith('/api/debug-terminal/')
2817
3274
  || url.pathname.startsWith('/debug-terminal/')) {
3275
+ // H5 sessions are scoped to Dashboard/workbench control. They never
3276
+ // inherit the legacy owner's unrestricted debug shell.
3277
+ if (!legacyAuthed)
3278
+ return jsonRes(res, 403, { ok: false, error: 'legacy_owner_required' });
2818
3279
  if (debugTerminalManager.handleHttp(req, res, url))
2819
3280
  return;
2820
3281
  }
@@ -2828,6 +3289,54 @@ const server = createServer(async (req, res) => {
2828
3289
  res.end();
2829
3290
  return;
2830
3291
  }
3292
+ // Fragment-free entry points. The card's terminal AppLink works with a plain
3293
+ // `/s/<id>?token=` URL; ours carried `#/agent-workbench`, and a fragment is
3294
+ // the one structural difference between the two. Clients that re-encode or
3295
+ // truncate an AppLink's `url` lose it and land on the Dashboard home, so
3296
+ // offer a path that survives regardless.
3297
+ if ((req.method === 'GET' || req.method === 'HEAD')
3298
+ && (url.pathname === '/workbench' || url.pathname === '/workbench/dock')) {
3299
+ const target = url.pathname === '/workbench/dock' ? '#/agent-workbench-dock' : '#/agent-workbench';
3300
+ const token = url.searchParams.get('t');
3301
+ const query = token ? `?t=${encodeURIComponent(token)}` : '';
3302
+ res.writeHead(302, { location: `/${query}${target}`, 'cache-control': 'no-store' });
3303
+ res.end();
3304
+ return;
3305
+ }
3306
+ // Self-service diagnostics for the same phone that cannot render the
3307
+ // Workbench terminal. Desktop browsers and Chromium's mobile emulation both
3308
+ // succeed, so the failing device has to report its own conditions: which
3309
+ // build it cached, whether its cookie rides along, whether the terminal's
3310
+ // HTTP and WebSocket hops are reachable from its network. Zero external
3311
+ // resources and no SPA bundle — it must open precisely when the SPA cannot,
3312
+ // which is also why it is allow-listed beside the static shell in
3313
+ // `decideDashboardAuth`. It probes only the visitor's own reachability and
3314
+ // echoes no token or secret.
3315
+ if (handleWorkbenchDoctor(req, res, url))
3316
+ return;
3317
+ // Installable Workbench: Feishu has no way to pin a custom app into its
3318
+ // mobile tab bar, so the closest thing to a permanent entry is the phone's
3319
+ // own home screen. A manifest makes "add to home screen" launch straight
3320
+ // into the session list, standalone and chrome-less.
3321
+ if ((req.method === 'GET' || req.method === 'HEAD') && url.pathname === '/workbench.webmanifest') {
3322
+ const manifest = {
3323
+ name: 'Botmux Workbench',
3324
+ short_name: 'Workbench',
3325
+ start_url: '/#/agent-workbench',
3326
+ scope: '/',
3327
+ display: 'standalone',
3328
+ orientation: 'any',
3329
+ background_color: '#080b10',
3330
+ theme_color: '#080b10',
3331
+ icons: [
3332
+ { src: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
3333
+ { src: '/favicon.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
3334
+ ],
3335
+ };
3336
+ res.writeHead(200, { 'content-type': 'application/manifest+json', 'cache-control': 'no-cache' });
3337
+ res.end(JSON.stringify(manifest));
3338
+ return;
3339
+ }
2831
3340
  // ─── Static frontend (index.html + /assets/* + /game/* + root icons) ───
2832
3341
  if ((req.method === 'GET' || req.method === 'HEAD') &&
2833
3342
  (url.pathname === '/' ||
@@ -2853,7 +3362,13 @@ const server = createServer(async (req, res) => {
2853
3362
  : url.pathname === '/favicon.ico'
2854
3363
  ? '/favicon.png'
2855
3364
  : url.pathname;
2856
- if (serveStatic(req, res, lookupPath))
3365
+ if (serveStatic(req, res, lookupPath, {
3366
+ // P1-11:只给已认证身份签票据;匿名 public-read 壳不含票据,控制类端点
3367
+ // 对它本来就 401/403。
3368
+ injectHtml: requestIdentity
3369
+ ? html => injectControlCsrfMeta(html, controlCsrfTokens.mint(requestIdentity.authSessionId))
3370
+ : undefined,
3371
+ }))
2857
3372
  return;
2858
3373
  if (serveMissingDashboardChunkModule(req, res, lookupPath))
2859
3374
  return;
@@ -2904,8 +3419,9 @@ const server = createServer(async (req, res) => {
2904
3419
  ? { ...s, botName: n }
2905
3420
  : s;
2906
3421
  }), groupsMatrixSnapshot.peekPresentation());
3422
+ const browserSessions = projectSessionPreviewsForBrowser(sessions);
2907
3423
  return jsonRes(res, 200, {
2908
- sessions: authed ? sessions : redactSessionsForPublic(sessions),
3424
+ sessions: projectSessionsForAudience(browserSessions, sessionBoardAudience),
2909
3425
  });
2910
3426
  }
2911
3427
  // Desktop / operator UI: aggregate pending ask-hooks across daemons.
@@ -2987,9 +3503,14 @@ const server = createServer(async (req, res) => {
2987
3503
  // else (incl. an unparseable/missing body) as a failure rather than a
2988
3504
  // silent success.
2989
3505
  const ok = upstream.ok && parsed?.ok === true;
3506
+ // A residual is NOT a failure (the row closed) but must not be counted
3507
+ // as a clean close either: an idle/workerless mojo row can carry a
3508
+ // parked lineage, so this path really does produce them.
3509
+ const residual = ok ? parseCloseResidual(parsed) : undefined;
2990
3510
  return {
2991
3511
  sessionId: s.sessionId,
2992
3512
  ok,
3513
+ ...(residual ? { residual } : {}),
2993
3514
  error: ok ? undefined : (parsed?.error ?? `http_${upstream.status}`),
2994
3515
  };
2995
3516
  }
@@ -3115,6 +3636,7 @@ const server = createServer(async (req, res) => {
3115
3636
  ...(entry.installTarget ? { installTarget: entry.installTarget } : {}),
3116
3637
  lastCheckedAt: entry.lastCheckedAt,
3117
3638
  }));
3639
+ const localDev = isLocalDevInstall();
3118
3640
  return jsonRes(res, 200, {
3119
3641
  current,
3120
3642
  latest,
@@ -3122,7 +3644,11 @@ const server = createServer(async (req, res) => {
3122
3644
  behind: !!latest && isNewerVersion(latest, current),
3123
3645
  cliBehind: cliUpdates.some((entry) => entry.updateAvailable),
3124
3646
  cliUpdates,
3125
- localDevInstall: isLocalDevInstall(),
3647
+ localDevInstall: localDev,
3648
+ // Local-dev can self-update via git pull + build only when the checkout
3649
+ // the wrapper points at is a real git worktree; otherwise the button
3650
+ // stays disabled (there is nothing to pull).
3651
+ localDevUpdatable: localDev && isGitWorktree(resolveLocalDevCheckoutDir()),
3126
3652
  updateSupported: installPlan !== null,
3127
3653
  updateManager: installPlan?.manager ?? installManager,
3128
3654
  updateCommand: installPlan ? formatGlobalInstallCommand(installPlan) : null,
@@ -3149,8 +3675,70 @@ const server = createServer(async (req, res) => {
3149
3675
  if (req.method === 'POST' && url.pathname === '/api/update/run') {
3150
3676
  if (!authed)
3151
3677
  return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
3152
- if (isLocalDevInstall())
3153
- return jsonRes(res, 400, { ok: false, error: 'local_dev_no_update' });
3678
+ // 本地 checkout:走 git pull --ff-only + pnpm build(与 CLI cmdUpgradeLocalDev
3679
+ // 共用 local-dev-update 逻辑),而不是全局包管理器安装。重启仍走下方
3680
+ // /api/update/restart 的 lease/intent 路径。
3681
+ if (isLocalDevInstall()) {
3682
+ const node = checkNode();
3683
+ if (!node.ok)
3684
+ return jsonRes(res, 400, { ok: false, error: 'node_too_old', node });
3685
+ if (updateInFlight)
3686
+ return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
3687
+ updateInFlight = true;
3688
+ let acquired = false;
3689
+ let blockedByRestart = false;
3690
+ let result;
3691
+ try {
3692
+ await withFileLock(globalInstallUpdateLockTarget(), async () => {
3693
+ acquired = true;
3694
+ if (hasActiveRestartLease()) {
3695
+ blockedByRestart = true;
3696
+ return;
3697
+ }
3698
+ result = await runLocalDevUpdate();
3699
+ }, { maxWaitMs: 2_000 });
3700
+ }
3701
+ catch (e) {
3702
+ if (!acquired)
3703
+ return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
3704
+ const code = e.code;
3705
+ if (code === 'dirty_worktree') {
3706
+ return jsonRes(res, 409, {
3707
+ ok: false, error: 'dirty_worktree',
3708
+ detail: e.status ?? '',
3709
+ dir: e.dir ?? '',
3710
+ });
3711
+ }
3712
+ if (code === 'not_a_worktree') {
3713
+ return jsonRes(res, 400, { ok: false, error: 'not_a_worktree', dir: e.dir ?? '' });
3714
+ }
3715
+ return jsonRes(res, 500, { ok: false, error: 'install_failed', detail: e instanceof Error ? e.message : String(e) });
3716
+ }
3717
+ finally {
3718
+ updateInFlight = false;
3719
+ }
3720
+ if (blockedByRestart)
3721
+ return jsonRes(res, 409, { ok: false, error: 'restart_in_flight' });
3722
+ // Pin THIS build's checkout + HEAD so the follow-up restart applies it,
3723
+ // even if the wrapper is re-pointed by a concurrent `use:here` before the
3724
+ // user confirms the restart. Consumed (and re-verified) in /api/update/restart.
3725
+ if (result)
3726
+ pendingLocalDevRestart = { dir: result.dir, head: result.head };
3727
+ return jsonRes(res, 200, {
3728
+ ok: true,
3729
+ // Versions of the checkout we actually updated (may differ from the
3730
+ // running process's install root when wrapper→B, dashboard runs A).
3731
+ oldVersion: result?.oldVersion ?? '',
3732
+ newVersion: result?.newVersion ?? '',
3733
+ // changed = HEAD advanced (or version string changed) — for display.
3734
+ changed: result?.changed === true || result?.oldVersion !== result?.newVersion,
3735
+ // A successful build regenerates dist/, so a restart is ALWAYS needed
3736
+ // to apply it — independent of whether HEAD moved (the checkout may
3737
+ // have been pulled already and only needed a build).
3738
+ restartRequired: true,
3739
+ localDev: true,
3740
+ });
3741
+ }
3154
3742
  let installPlan;
3155
3743
  try {
3156
3744
  const packageRoot = lastSuccessfulUpdatePlan?.activePackageRoot ?? botmuxInstallRoot();
@@ -3412,6 +4000,33 @@ const server = createServer(async (req, res) => {
3412
4000
  }
3413
4001
  catch { /* empty / bad body → plain restart */ }
3414
4002
  const upd = body.update && typeof body.update === 'object' ? body.update : null;
4003
+ // Resolve the local-dev restart target BEFORE claiming the lease so a
4004
+ // fail-closed drift check can't leave a dangling lease. Prefer the plan a
4005
+ // preceding /api/update/run pinned (dir + post-build HEAD); a plain manual
4006
+ // restart (no pending plan) resolves the wrapper live. Verify the target's
4007
+ // dist/cli.js exists and — for a pinned plan — that HEAD hasn't moved since
4008
+ // the build; on drift/absence fail closed rather than restart the wrong tree.
4009
+ let localDevRestartError;
4010
+ let localDevTarget;
4011
+ if (isLocalDevInstall()) {
4012
+ const pinned = pendingLocalDevRestart;
4013
+ pendingLocalDevRestart = undefined; // consume regardless of outcome
4014
+ const decision = resolveLocalDevRestartTarget(pinned, resolveLocalDevCheckoutDir(), {
4015
+ cliEntryExists: (dir) => existsSync(botmuxCliEntryAt(dir)),
4016
+ headOf: (dir) => gitHeadSha(dir),
4017
+ });
4018
+ if (decision.action === 'fail') {
4019
+ localDevRestartError = { status: 409, body: { ok: false, error: decision.reason, dir: decision.dir } };
4020
+ }
4021
+ else if (decision.action === 'restart') {
4022
+ localDevTarget = decision.dir;
4023
+ }
4024
+ else {
4025
+ localDevTarget = undefined; // fallback-running-root
4026
+ }
4027
+ }
4028
+ if (localDevRestartError)
4029
+ return jsonRes(res, localDevRestartError.status, localDevRestartError.body);
3415
4030
  let acquired = false;
3416
4031
  let leaseId = null;
3417
4032
  let activePackageRoot;
@@ -3443,7 +4058,15 @@ const server = createServer(async (req, res) => {
3443
4058
  });
3444
4059
  return;
3445
4060
  }
3446
- activePackageRoot = (lastSuccessfulUpdatePlan ?? tryResolveGlobalInstallPlan())?.activePackageRoot;
4061
+ // Local-dev restarts from the target resolved above (pinned build's
4062
+ // checkout, verified present + at the built HEAD); undefined falls back
4063
+ // to this dashboard process's own cli.js via spawnDetachedRestart.
4064
+ if (isLocalDevInstall()) {
4065
+ activePackageRoot = localDevTarget;
4066
+ }
4067
+ else {
4068
+ activePackageRoot = (lastSuccessfulUpdatePlan ?? tryResolveGlobalInstallPlan())?.activePackageRoot;
4069
+ }
3447
4070
  // Send acknowledgement while holding the lock, then release immediately.
3448
4071
  // The lease itself prevents concurrent restarts — no need to hold the
3449
4072
  // lock across the network round-trip waiting for res.finish.
@@ -3484,7 +4107,14 @@ const server = createServer(async (req, res) => {
3484
4107
  if (req.method === 'GET' && url.pathname === '/api/skills') {
3485
4108
  return jsonRes(res, 200, dashboardSkillsPayload());
3486
4109
  }
3487
- if (req.method === 'DELETE' && url.pathname === '/api/skills') {
4110
+ // Batch skill removal. POST /api/skills/remove is the canonical route the
4111
+ // dashboard UI calls: the payload (names[], force) must travel in the body,
4112
+ // and DELETE bodies are dropped by the platform dashboard proxy (it assumes
4113
+ // DELETE carries no body, forwards content-length but never pipes the bytes,
4114
+ // so readJsonBody hangs until the outer gateway returns 504). DELETE
4115
+ // /api/skills stays as an alias for direct/scripted callers.
4116
+ if ((req.method === 'DELETE' && url.pathname === '/api/skills')
4117
+ || (req.method === 'POST' && url.pathname === '/api/skills/remove')) {
3488
4118
  let parsed;
3489
4119
  try {
3490
4120
  parsed = await readJsonBody(req);
@@ -4048,6 +4678,9 @@ const server = createServer(async (req, res) => {
4048
4678
  if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume|restart|start)$/))) {
4049
4679
  const sid = decodeURIComponent(m[1]);
4050
4680
  const op = m[2];
4681
+ // P1-11:locate 与接管/解锁同属工作台三项操作能力,同样是无 body POST。
4682
+ if (op === 'locate' && requestIdentity && !enforceControlCsrf(req, res, requestIdentity))
4683
+ return;
4051
4684
  const owner = aggregator.ownerOf(sid);
4052
4685
  if (!owner)
4053
4686
  return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
@@ -4145,9 +4778,20 @@ const server = createServer(async (req, res) => {
4145
4778
  if (!owner)
4146
4779
  return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
4147
4780
  const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}`, { method: 'GET' });
4148
- res.writeHead(upstream.status, { 'content-type': 'application/json' });
4149
- res.end(await upstream.text());
4150
- return;
4781
+ const raw = await upstream.text();
4782
+ if (!upstream.ok) {
4783
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
4784
+ res.end(raw);
4785
+ return;
4786
+ }
4787
+ let body;
4788
+ try {
4789
+ body = JSON.parse(raw);
4790
+ }
4791
+ catch {
4792
+ return jsonRes(res, 502, { ok: false, error: 'invalid_daemon_response' });
4793
+ }
4794
+ return jsonRes(res, upstream.status, projectSessionDetailForBrowser(body));
4151
4795
  }
4152
4796
  // 异步 trigger 结果轮询(asyncReturnSessionId 模式的权威查询端点)。
4153
4797
  // 四态 running/completed/failed/not_found,daemon 重启后从持久化结果兜底
@@ -4191,6 +4835,11 @@ const server = createServer(async (req, res) => {
4191
4835
  // so decideDashboardAuth has already 401'd unauthenticated callers before we
4192
4836
  // get here — the token only reaches authenticated dashboard sessions.
4193
4837
  if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/write-link$/))) {
4838
+ // Short H5 sessions use the tokenless /control/takeover lease. Returning
4839
+ // the legacy stable capability here would bypass release/expiry/disconnect
4840
+ // enforcement and leak a token into browser-visible JSON.
4841
+ if (!legacyAuthed)
4842
+ return dashboardControlJson(res, 403, { ok: false, error: 'control_takeover_required' });
4194
4843
  const sid = decodeURIComponent(m[1]);
4195
4844
  const owner = aggregator.ownerOf(sid);
4196
4845
  if (!owner)
@@ -4200,9 +4849,71 @@ const server = createServer(async (req, res) => {
4200
4849
  res.end(await upstream.text());
4201
4850
  return;
4202
4851
  }
4852
+ // Read-only web-terminal link. The Workbench terminal pane uses it so the
4853
+ // frame authenticates by capability instead of by Dashboard cookie, which
4854
+ // a Feishu WebView's WebSocket does not carry.
4855
+ //
4856
+ // Two things about the daemon's answer never reach the browser (P1-5):
4857
+ // • its `?viewToken=` is the worker's per-boot card token, unbound to the
4858
+ // requesting authentication. It is consumed here — converted to a
4859
+ // one-way worker generation id — and REPLACED with a short-lived signed
4860
+ // read grant bound to sessionId + authSessionId + expiresAt + that
4861
+ // generation + `audience: central`;
4862
+ // • its ORIGIN is the daemon terminal proxy / worker port, both network
4863
+ // reachable and both blind to logout. We answer with a same-origin
4864
+ // relative path instead, so the only entry point the browser ever learns
4865
+ // is this dashboard's front proxy — the one place that checks auth
4866
+ // session liveness and countersigns the hop for the worker.
4867
+ // A view capability can never send input, so this stays safe for any
4868
+ // identity allowed to observe the session; unauthenticated callers were
4869
+ // already rejected by the auth decision above (no public allow-list).
4870
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/view-link$/))) {
4871
+ const sid = decodeURIComponent(m[1]);
4872
+ const owner = aggregator.ownerOf(sid);
4873
+ if (!owner)
4874
+ return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
4875
+ const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/view-link`, { method: 'GET' });
4876
+ if (upstream.status !== 200) {
4877
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
4878
+ res.end(await upstream.text());
4879
+ return;
4880
+ }
4881
+ let upstreamUrl;
4882
+ try {
4883
+ upstreamUrl = JSON.parse(await upstream.text()).url;
4884
+ }
4885
+ catch {
4886
+ upstreamUrl = undefined;
4887
+ }
4888
+ // No generation ⇒ no pinned capability. Fail closed rather than mint one
4889
+ // that would outlive the worker boot it was meant for.
4890
+ const generation = upstreamWorkerViewGeneration(SECRET, upstreamUrl);
4891
+ const minted = requestIdentity && generation
4892
+ ? mintTerminalViewCapability(SECRET, sid, requestIdentity, generation)
4893
+ : null;
4894
+ const rewritten = minted ? centralViewLinkPath(sid, minted.token) : null;
4895
+ // Fail closed rather than fall back to the unbound upstream token/origin.
4896
+ if (!minted || !rewritten)
4897
+ return jsonRes(res, 502, { ok: false, error: 'view_link_unavailable' });
4898
+ return jsonRes(res, 200, { ok: true, url: rewritten, expiresAt: minted.expiresAt });
4899
+ }
4900
+ // Browser-safe preview metadata. The literal loopback host/port remains in
4901
+ // the aggregator only; this authenticated API returns a same-origin path.
4902
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/preview$/))) {
4903
+ const sid = decodeURIComponent(m[1]);
4904
+ const resolution = resolveDashboardSessionPreview(sid);
4905
+ if (!resolution.ok)
4906
+ return jsonRes(res, resolution.status, { ok: false, error: resolution.error });
4907
+ const preview = previewDescriptorFromRow(aggregator.getSession(sid));
4908
+ if (!preview)
4909
+ return jsonRes(res, 404, { ok: false, error: 'preview_not_registered' });
4910
+ return jsonRes(res, 200, { ok: true, preview });
4911
+ }
4203
4912
  // Dashboard「复现命令」:透传到 owning daemon 取该 session 的真实 CLI 调用。
4204
4913
  // 与 write-link 同样只在管理 cookie(写权限)下可达:命令含 token/凭证。
4205
4914
  if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/spawn-command$/))) {
4915
+ if (!legacyAuthed)
4916
+ return dashboardControlJson(res, 403, { ok: false, error: 'legacy_owner_required' });
4206
4917
  const sid = decodeURIComponent(m[1]);
4207
4918
  const owner = aggregator.ownerOf(sid);
4208
4919
  if (!owner)
@@ -4735,6 +5446,7 @@ const server = createServer(async (req, res) => {
4735
5446
  wrapperCli: j.wrapperCli || d.wrapperCli,
4736
5447
  model: j.model || d.model,
4737
5448
  reasoningEffort: j.reasoningEffort || d.reasoningEffort,
5449
+ turnTimeoutMs: typeof j.turnTimeoutMs === 'number' ? j.turnTimeoutMs : d.turnTimeoutMs,
4738
5450
  }, j);
4739
5451
  }
4740
5452
  catch (e) {
@@ -5378,6 +6090,24 @@ const server = createServer(async (req, res) => {
5378
6090
  message: loginRequired ? '尚未获得飞书标签权限,请点击「立即授权」按钮进行授权。' : '没有可用于读取标签的飞书机器人。',
5379
6091
  });
5380
6092
  }
6093
+ // PUT /api/bots/:appId/session-owner-reminder — per-Bot periodic owner
6094
+ // reminder policy. The owning daemon validates, persists, and hot-applies.
6095
+ let mBotOwnerReminder;
6096
+ if (req.method === 'PUT' && (mBotOwnerReminder = url.pathname.match(/^\/api\/bots\/([^/]+)\/session-owner-reminder$/))) {
6097
+ const appId = decodeURIComponent(mBotOwnerReminder[1]);
6098
+ const chunks = [];
6099
+ for await (const c of req)
6100
+ chunks.push(c);
6101
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
6102
+ const upstream = await proxyToDaemon(appId, `/api/bot-session-owner-reminder`, {
6103
+ method: 'PUT',
6104
+ headers: { 'content-type': 'application/json' },
6105
+ body: raw,
6106
+ });
6107
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
6108
+ res.end(await upstream.text());
6109
+ return;
6110
+ }
5381
6111
  // Create a new chat — pick a creator from the user-selected larkAppIds
5382
6112
  // (Feishu makes the calling bot the implicit first member, so picking
5383
6113
  // anything else would silently add an unwanted bot). Auto-invite the
@@ -5654,14 +6384,41 @@ const server = createServer(async (req, res) => {
5654
6384
  'connection': 'keep-alive',
5655
6385
  });
5656
6386
  res.write('retry: 5000\n\n');
6387
+ // P1-8:这条流的寿命必须等于建立它的那次认证的寿命。建流时登记进
6388
+ // authSession→长连接索引(logout/rotate/解绑时被主动 destroy),并在每帧
6389
+ // 推送前复核身份仍有效——否则 rotate 之后新签的 `riffAccessUrl`(Riff 沙箱
6390
+ // 写凭据)会顺着这条老连接送给上一任持有者。匿名 public-read 连接没有认证
6391
+ // 会话,行为不变。
6392
+ // P1-14 姊妹项:这条流的能力口径必须与 REST 同源。Workbench-only 身份对
6393
+ // `GET /api/schedules` 是明确的 401,`/events` 就不能把同一份排程(含
6394
+ // prompt / workingDir / fired 的 error 原文)换个管子送出去——过滤在
6395
+ // createDashboardEventsStream 内部按 audience 执行,见 events-sse.ts。
6396
+ const eventsAudience = authed
6397
+ ? 'management'
6398
+ : workbenchOnlyIdentity ? 'workbench' : 'anonymous';
6399
+ const stream = createDashboardEventsStream({
6400
+ res,
6401
+ authSessionId: requestIdentity?.authSessionId ?? null,
6402
+ audience: eventsAudience,
6403
+ isAuthSessionLive: terminalAuthSessionLive,
6404
+ bind: (authSessionId, close) => authSessionConnections.register(authSessionId, close),
6405
+ });
5657
6406
  const off = aggregator.on(ev => {
5658
- // Mirror the GET /api/schedules carve-out: schedule events carry the
5659
- // full task object strip the prompt AND workingDir for anonymous SSE
5660
- // listeners, or the REST-side scrub would be trivially bypassed by
5661
- // `/events`.
5662
- let body = authed
5663
- ? ev.body
5664
- : redactSessionEventForPublic(ev.type, ev.body);
6407
+ // Session rows follow the same three-way audience as GET /api/sessions,
6408
+ // or a Workbench viewer would receive the preview descriptor on the
6409
+ // initial REST fetch and then lose it on the first live patch.
6410
+ const projectedBody = projectSessionPreviewEventForBrowser(ev.type, ev.body);
6411
+ let body = projectSessionEventForAudience(ev.type, projectedBody, sessionBoardAudience);
6412
+ // Schedules stay on the MANAGEMENT gate, mirroring the GET
6413
+ // /api/schedules carve-out: schedule events carry the full task object
6414
+ // (prompt = business instructions, workingDir = repo/customer path) and
6415
+ // `/api/schedules` is not a Workbench capability, so widening this to
6416
+ // `sessionBoardAudience` would let an H5 identity read over `/events`
6417
+ // what the REST route refuses it.
6418
+ // Workbench-only identities never reach this redaction at all — the
6419
+ // stream drops every `schedule.*` frame for them (P1-14 sibling); this
6420
+ // branch is the ANONYMOUS publicReadOnly path, which the REST route
6421
+ // does serve, redacted the same way.
5665
6422
  if (!authed && (ev.type === 'schedule.created' || ev.type === 'schedule.updated')) {
5666
6423
  const b = body;
5667
6424
  body = {
@@ -5670,10 +6427,10 @@ const server = createServer(async (req, res) => {
5670
6427
  ...(b.patch ? { patch: { ...b.patch, prompt: undefined, workingDir: undefined } } : {}),
5671
6428
  };
5672
6429
  }
5673
- res.write(`event: ${ev.type}\ndata: ${JSON.stringify({ larkAppId: ev.larkAppId, body })}\n\n`);
6430
+ stream.write(ev.type, { larkAppId: ev.larkAppId, body });
5674
6431
  });
5675
6432
  const hb = setInterval(() => {
5676
- res.write(`event: heartbeat\ndata: ${JSON.stringify({ ts: Date.now() })}\n\n`);
6433
+ stream.write('heartbeat', { ts: Date.now() });
5677
6434
  }, 15_000);
5678
6435
  // Push a bots.changed frame whenever the online bot roster actually
5679
6436
  // changes (bot added / removed / renamed / re-indexed) so the Bot 配置
@@ -5686,9 +6443,9 @@ const server = createServer(async (req, res) => {
5686
6443
  if (sig === lastRoster)
5687
6444
  return;
5688
6445
  lastRoster = sig;
5689
- res.write(`event: bots.changed\ndata: ${JSON.stringify({ body: { signature: sig } })}\n\n`);
6446
+ stream.write('bots.changed', { body: { signature: sig } });
5690
6447
  });
5691
- res.on('close', () => { off(); offRoster(); clearInterval(hb); });
6448
+ res.on('close', () => { off(); offRoster(); clearInterval(hb); stream.dispose(); });
5692
6449
  return;
5693
6450
  }
5694
6451
  // Public API + static frontend land in Task 17 / 18. For now: 404.
@@ -5744,47 +6501,35 @@ oauthCallbackServer.listen(9768, '127.0.0.1', () => {
5744
6501
  server.on('upgrade', (req, clientSocket, head) => {
5745
6502
  try {
5746
6503
  const rawUrl = req.url ?? '/';
6504
+ // Preview 的 WS 必须第一个判:沙箱化之后它来自不透明来源(`Origin: null`),
6505
+ // 凭据是路径里的 content capability,绝不能被下面的管理类 Origin 校验误杀。
6506
+ if (sessionPreviewProxy.handleUpgrade(req, clientSocket, head))
6507
+ return;
6508
+ // P1-11:管理类 WS(终端 / 调试终端)升级不经 HTTP 门禁,浏览器对 WS 握手
6509
+ // 一定带 Origin,所以「带了但对不上(含 null)」一律拒——同站兄弟子域和
6510
+ // localhost 其它端口正是 SameSite=Lax 挡不住的那一类。
6511
+ const upgradeOrigin = managementUpgradeOrigin(req.headers);
6512
+ if (!upgradeOrigin.ok) {
6513
+ const body = JSON.stringify({ ok: false, error: upgradeOrigin.error });
6514
+ clientSocket.end([
6515
+ 'HTTP/1.1 403 Forbidden',
6516
+ 'content-type: application/json; charset=utf-8',
6517
+ 'cache-control: no-store',
6518
+ 'connection: close',
6519
+ `content-length: ${Buffer.byteLength(body)}`,
6520
+ '',
6521
+ body,
6522
+ ].join('\r\n'));
6523
+ return;
6524
+ }
5747
6525
  // 调试终端 WS(owner-only):manager 内部自校验管理 cookie。命中即接管。
5748
6526
  if (rawUrl.startsWith('/debug-terminal/')) {
5749
6527
  if (debugTerminalManager.handleUpgrade(req, clientSocket, head))
5750
6528
  return;
5751
6529
  }
5752
- if (!(rawUrl === '/s' || rawUrl.startsWith('/s/') || rawUrl.startsWith('/s?'))) {
5753
- return clientSocket.destroy();
5754
- }
5755
- // Strip query/hash before extracting the sessionId path segment.
5756
- const pathname = rawUrl.split(/[?#]/)[0];
5757
- const sessionId = parseTerminalSessionId(pathname);
5758
- const tport = sessionId ? aggregator.terminalProxyPortOf(sessionId) : undefined;
5759
- if (!tport)
5760
- return clientSocket.destroy();
5761
- const upstream = netConnect(tport, '127.0.0.1', () => {
5762
- // rawHeaders is a flat [k, v, k, v, ...] list — preserves casing/duplicates.
5763
- const lines = [`${req.method} ${req.url} HTTP/1.1`];
5764
- const rh = req.rawHeaders;
5765
- for (let i = 0; i + 1 < rh.length; i += 2)
5766
- lines.push(`${rh[i]}: ${rh[i + 1]}`);
5767
- lines.push('', '');
5768
- upstream.write(lines.join('\r\n'));
5769
- if (head?.length)
5770
- upstream.write(head);
5771
- upstream.pipe(clientSocket);
5772
- clientSocket.pipe(upstream);
5773
- });
5774
- const cleanup = () => {
5775
- try {
5776
- upstream.destroy();
5777
- }
5778
- catch { /* ignore */ }
5779
- try {
5780
- clientSocket.destroy();
5781
- }
5782
- catch { /* ignore */ }
5783
- };
5784
- upstream.on('error', cleanup);
5785
- clientSocket.on('error', cleanup);
5786
- upstream.on('close', () => clientSocket.destroy());
5787
- clientSocket.on('close', () => upstream.destroy());
6530
+ if (terminalFrontProxy.handleUpgrade(req, clientSocket, head))
6531
+ return;
6532
+ clientSocket.destroy();
5788
6533
  }
5789
6534
  catch {
5790
6535
  try {
@@ -5894,11 +6639,12 @@ function readPlatformBotsInfo() {
5894
6639
  // Merge per-bot team-visibility config (showInTeam) from bots.json by
5895
6640
  // larkAppId so the platform team page can hide bots. Default: showInTeam =
5896
6641
  // true (shown). bots.json may be unreadable from the dashboard process →
5897
- // fall back to the default.
6642
+ // fall back to the default. apiOnly is read from the same config to derive
6643
+ // `mentionable` (a core-only bot has no Feishu transport → can't be @-ed).
5898
6644
  const cfgByAppId = new Map();
5899
6645
  try {
5900
6646
  for (const cfg of loadBotConfigs()) {
5901
- cfgByAppId.set(cfg.larkAppId, { showInTeam: cfg.showInTeam });
6647
+ cfgByAppId.set(cfg.larkAppId, { showInTeam: cfg.showInTeam, apiOnly: cfg.apiOnly });
5902
6648
  }
5903
6649
  }
5904
6650
  catch {
@@ -5917,6 +6663,13 @@ function readPlatformBotsInfo() {
5917
6663
  // 自家消息回声学到的租户稳定 union_id(可能尚未学到 → undefined)。
5918
6664
  // 平台聚合团队 roster 用,见 bot-union-ids-store / platform-team-store。
5919
6665
  unionId: e.larkAppId ? getBotUnionId(config.session.dataDir, e.larkAppId) : undefined,
6666
+ // 团队维度 Agent 互查(additive,交接契约 §端点2 / register|heartbeat):
6667
+ // · specialties:owner 预配的专长标签(bot-profiles),发现/拉群匹配依据,仅展示不可信。
6668
+ // · mentionable:是否有飞书传输身份能被 @(core-only/apiOnly → false)。cfg 读不到
6669
+ // 时保守按可传输(true),与 team-bot-directory「undefined 按可传输」同源
6670
+ // (fail-open 仅在本机自报、无跨部署放大风险;真正的 no-transport 由 apiOnly 明示)。
6671
+ specialties: e.larkAppId ? getBotSpecialties(config.session.dataDir, e.larkAppId) : [],
6672
+ mentionable: cfg?.apiOnly !== true,
5920
6673
  };
5921
6674
  })
5922
6675
  .filter((b) => b.appId);