@ours.network/fleet 1.2.0-nightly.3 → 1.2.0-nightly.31

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 (458) hide show
  1. package/README.md +158 -28
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.d.ts +3 -0
  5. package/dist/agent-ours/bridge.js +178 -0
  6. package/dist/agent-ours/controller.d.ts +14 -0
  7. package/dist/agent-ours/controller.js +60 -0
  8. package/dist/agent-ours/correspondence.d.ts +10 -0
  9. package/dist/agent-ours/correspondence.js +1 -0
  10. package/dist/agent-ours/file-rpc.d.ts +23 -0
  11. package/dist/agent-ours/file-rpc.js +123 -0
  12. package/dist/agent-ours/harness.d.ts +14 -0
  13. package/dist/agent-ours/harness.js +72 -0
  14. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  15. package/dist/agent-ours/mcp-endpoint.js +111 -0
  16. package/dist/agent-ours/runtime.d.ts +59 -0
  17. package/dist/agent-ours/runtime.js +283 -0
  18. package/dist/agent-ours/service.d.ts +23 -0
  19. package/dist/agent-ours/service.js +372 -0
  20. package/dist/agent-ours/state.d.ts +45 -0
  21. package/dist/agent-ours/state.js +95 -0
  22. package/dist/agent-ours/wire.d.ts +14 -0
  23. package/dist/agent-ours/wire.js +55 -0
  24. package/dist/application/conversation-history.d.ts +25 -0
  25. package/dist/application/conversation-history.js +189 -0
  26. package/dist/application/fleet-query-service.d.ts +1 -1
  27. package/dist/application/fleet-query-service.js +2 -2
  28. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  29. package/dist/application/legacy-supervisor-identity.js +43 -0
  30. package/dist/application/model-catalog.d.ts +1 -1
  31. package/dist/application/model-catalog.js +5 -2
  32. package/dist/application/preset-provenance.d.ts +19 -0
  33. package/dist/application/preset-provenance.js +59 -0
  34. package/dist/application/room-layout-definitions.d.ts +45 -0
  35. package/dist/application/room-layout-definitions.js +130 -0
  36. package/dist/application/session-control.d.ts +18 -0
  37. package/dist/application/session-control.js +54 -3
  38. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  39. package/dist/application/supervisor-ours-tools.js +113 -0
  40. package/dist/application/task-layouts.d.ts +91 -0
  41. package/dist/application/task-layouts.js +249 -0
  42. package/dist/application/task-room-service.d.ts +99 -3
  43. package/dist/application/task-room-service.js +296 -31
  44. package/dist/atomic-file.d.ts +5 -0
  45. package/dist/atomic-file.js +62 -1
  46. package/dist/briefing.js +10 -59
  47. package/dist/build-info.json +6 -4
  48. package/dist/capabilities.d.ts +3 -1
  49. package/dist/capabilities.js +4 -0
  50. package/dist/cli.js +131 -5
  51. package/dist/client-profile.d.ts +5 -16
  52. package/dist/client-profile.js +4 -112
  53. package/dist/config.d.ts +4 -0
  54. package/dist/config.js +7 -0
  55. package/dist/creation.d.ts +1 -1
  56. package/dist/creation.js +5 -16
  57. package/dist/daemon-recovery.d.ts +1 -8
  58. package/dist/daemon-recovery.js +8 -113
  59. package/dist/docs.d.ts +3 -3
  60. package/dist/docs.js +43 -23
  61. package/dist/doctor.d.ts +1 -1
  62. package/dist/doctor.js +46 -59
  63. package/dist/erased-resources.d.ts +9 -0
  64. package/dist/erased-resources.js +87 -0
  65. package/dist/fleet-command-audit.d.ts +1 -0
  66. package/dist/fleet-command-audit.js +12 -7
  67. package/dist/harness/agent-session.d.ts +2 -0
  68. package/dist/harness/claude-code-session.js +4 -2
  69. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  70. package/dist/harness/codex-app-server-proxy.js +41 -5
  71. package/dist/harness/codex-session.js +9 -2
  72. package/dist/harness/codex.d.ts +3 -2
  73. package/dist/harness/codex.js +4 -3
  74. package/dist/harness/hermes-session.js +7 -1
  75. package/dist/init-wizard.js +20 -9
  76. package/dist/isolation/policy.js +2 -0
  77. package/dist/isolation/types.d.ts +5 -0
  78. package/dist/monitor.d.ts +1 -34
  79. package/dist/monitor.js +24 -151
  80. package/dist/notifications/fleet-producer.d.ts +18 -0
  81. package/dist/notifications/fleet-producer.js +129 -0
  82. package/dist/notifications/outbox.d.ts +31 -0
  83. package/dist/notifications/outbox.js +104 -0
  84. package/dist/owner-channel/channel.d.ts +3 -0
  85. package/dist/owner-channel/channel.js +34 -6
  86. package/dist/owner-channel/commands.d.ts +4 -0
  87. package/dist/owner-channel/commands.js +8 -0
  88. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  89. package/dist/owner-channel/ours-client.js +2 -17
  90. package/dist/preset-migration.js +15 -0
  91. package/dist/rooms-tasks/add-member.d.ts +48 -0
  92. package/dist/rooms-tasks/add-member.js +83 -0
  93. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  94. package/dist/rooms-tasks/archived-absence.js +48 -0
  95. package/dist/rooms-tasks/cli.js +34 -5
  96. package/dist/rooms-tasks/close.d.ts +5 -2
  97. package/dist/rooms-tasks/close.js +208 -28
  98. package/dist/rooms-tasks/config.js +3 -1
  99. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  100. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  101. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  102. package/dist/rooms-tasks/cowork-http.js +70 -0
  103. package/dist/rooms-tasks/deletion.d.ts +1 -1
  104. package/dist/rooms-tasks/deletion.js +74 -5
  105. package/dist/rooms-tasks/erasure.d.ts +6 -0
  106. package/dist/rooms-tasks/erasure.js +169 -0
  107. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  108. package/dist/rooms-tasks/external-worker.js +3 -4
  109. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  110. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  111. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  112. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  113. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  114. package/dist/rooms-tasks/layout-cli.js +107 -0
  115. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  116. package/dist/rooms-tasks/layout-config.js +89 -0
  117. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  118. package/dist/rooms-tasks/layout-control.js +52 -0
  119. package/dist/rooms-tasks/layout-service.d.ts +42 -0
  120. package/dist/rooms-tasks/layout-service.js +185 -0
  121. package/dist/rooms-tasks/layout.d.ts +114 -0
  122. package/dist/rooms-tasks/layout.js +250 -0
  123. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  124. package/dist/rooms-tasks/live-readiness.js +85 -0
  125. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  126. package/dist/rooms-tasks/member-startup.js +2 -0
  127. package/dist/rooms-tasks/provision.d.ts +14 -0
  128. package/dist/rooms-tasks/provision.js +182 -48
  129. package/dist/rooms-tasks/room-state.d.ts +4 -2
  130. package/dist/rooms-tasks/room-state.js +53 -8
  131. package/dist/rooms-tasks/task-state.d.ts +42 -7
  132. package/dist/rooms-tasks/task-state.js +153 -23
  133. package/dist/rooms-tasks/types.d.ts +28 -0
  134. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  135. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  136. package/dist/rooms-tasks/workspace.d.ts +26 -0
  137. package/dist/rooms-tasks/workspace.js +195 -0
  138. package/dist/runner.d.ts +23 -3
  139. package/dist/runner.js +581 -494
  140. package/dist/session/acp.d.ts +3 -0
  141. package/dist/session/acp.js +14 -0
  142. package/dist/session/arbiter.d.ts +3 -0
  143. package/dist/session/arbiter.js +20 -2
  144. package/dist/session/codex-app-server-transport.js +1 -1
  145. package/dist/session/codex-app-server.d.ts +3 -0
  146. package/dist/session/codex-app-server.js +25 -1
  147. package/dist/session/control.d.ts +10 -1
  148. package/dist/session/control.js +37 -2
  149. package/dist/session/conversation-normalizer.d.ts +2 -1
  150. package/dist/session/conversation-normalizer.js +1 -1
  151. package/dist/session/conversation-types.d.ts +5 -3
  152. package/dist/session/task-notice.d.ts +14 -0
  153. package/dist/session/task-notice.js +13 -0
  154. package/dist/session/types.d.ts +3 -0
  155. package/dist/socket-path.d.ts +2 -0
  156. package/dist/socket-path.js +22 -0
  157. package/dist/spawn.js +19 -16
  158. package/dist/startup-readiness.d.ts +13 -0
  159. package/dist/startup-readiness.js +38 -0
  160. package/dist/subscriptions/cli.d.ts +78 -0
  161. package/dist/subscriptions/cli.js +186 -0
  162. package/dist/subscriptions/launch.d.ts +37 -0
  163. package/dist/subscriptions/launch.js +93 -0
  164. package/dist/subscriptions/login.d.ts +75 -0
  165. package/dist/subscriptions/login.js +182 -0
  166. package/dist/subscriptions/observed.d.ts +7 -0
  167. package/dist/subscriptions/observed.js +44 -0
  168. package/dist/subscriptions/probe.d.ts +10 -0
  169. package/dist/subscriptions/probe.js +73 -0
  170. package/dist/subscriptions/service.d.ts +102 -0
  171. package/dist/subscriptions/service.js +364 -0
  172. package/dist/subscriptions/store.d.ts +71 -0
  173. package/dist/subscriptions/store.js +140 -0
  174. package/dist/supervisor/launchd.js +2 -1
  175. package/dist/supervisor/systemd.js +9 -23
  176. package/dist/temp-idle.d.ts +17 -0
  177. package/dist/temp-idle.js +42 -0
  178. package/dist/temp-lifecycle.d.ts +6 -1
  179. package/dist/temp-lifecycle.js +29 -13
  180. package/dist/temp-supervisor-recovery.d.ts +3 -0
  181. package/dist/temp-supervisor-recovery.js +46 -0
  182. package/dist/watchdog/briefing.js +3 -16
  183. package/dist/watchdog/run.js +19 -5
  184. package/dist/web/agent-attachments.d.ts +29 -0
  185. package/dist/web/agent-attachments.js +162 -0
  186. package/dist/web/auth.d.ts +28 -2
  187. package/dist/web/auth.js +81 -8
  188. package/dist/web/available-port.d.ts +2 -0
  189. package/dist/web/available-port.js +23 -0
  190. package/dist/web/control.d.ts +3 -2
  191. package/dist/web/control.js +11 -7
  192. package/dist/web/fleet-config-service.d.ts +4 -1
  193. package/dist/web/fleet-config-service.js +46 -15
  194. package/dist/web/prefix-gateway.d.ts +23 -0
  195. package/dist/web/prefix-gateway.js +156 -0
  196. package/dist/web/runtime.d.ts +4 -0
  197. package/dist/web/runtime.js +106 -20
  198. package/dist/web/server.d.ts +11 -0
  199. package/dist/web/server.js +445 -31
  200. package/dist/web/service.d.ts +11 -2
  201. package/dist/web/service.js +30 -4
  202. package/dist/web/workspace-devices.d.ts +29 -0
  203. package/dist/web/workspace-devices.js +84 -0
  204. package/dist/web/workspace-tunnel.d.ts +1 -0
  205. package/dist/web/workspace-tunnel.js +32 -0
  206. package/dist/web-app/assets/FleetApp-D58F4xje.css +1 -0
  207. package/dist/web-app/assets/FleetApp-DP5cBY0o.js +302 -0
  208. package/dist/web-app/assets/FleetApp-DP5cBY0o.js.map +1 -0
  209. package/dist/web-app/assets/abnfDiagram-VCTEODGH-BvJIz7wN.js +2 -0
  210. package/dist/web-app/assets/abnfDiagram-VCTEODGH-BvJIz7wN.js.map +1 -0
  211. package/dist/web-app/assets/arc-AT_BEfpe.js +2 -0
  212. package/dist/web-app/assets/arc-AT_BEfpe.js.map +1 -0
  213. package/dist/web-app/assets/architectureDiagram-5GKGNRK7-vSgJa9Xb.js +37 -0
  214. package/dist/web-app/assets/architectureDiagram-5GKGNRK7-vSgJa9Xb.js.map +1 -0
  215. package/dist/web-app/assets/blockDiagram-I7D4REHJ-DCFk_4Kv.js +130 -0
  216. package/dist/web-app/assets/blockDiagram-I7D4REHJ-DCFk_4Kv.js.map +1 -0
  217. package/dist/web-app/assets/browser-CMIwJ_8L.js +9 -0
  218. package/dist/web-app/assets/browser-CMIwJ_8L.js.map +1 -0
  219. package/dist/web-app/assets/c4Diagram-7LVT6UL2-8J_dVUtF.js +39 -0
  220. package/dist/web-app/assets/c4Diagram-7LVT6UL2-8J_dVUtF.js.map +1 -0
  221. package/dist/web-app/assets/channel-BHCBlfND.js +2 -0
  222. package/dist/web-app/assets/channel-BHCBlfND.js.map +1 -0
  223. package/dist/web-app/assets/chunk-2Q5K7J3B-31RX-jnL.js +2 -0
  224. package/dist/web-app/assets/chunk-2Q5K7J3B-31RX-jnL.js.map +1 -0
  225. package/dist/web-app/assets/chunk-5VM5RSS4-CFF8_ZJH.js +16 -0
  226. package/dist/web-app/assets/chunk-5VM5RSS4-CFF8_ZJH.js.map +1 -0
  227. package/dist/web-app/assets/chunk-F27PBJKO-Dx1l5wLI.js +2 -0
  228. package/dist/web-app/assets/chunk-F27PBJKO-Dx1l5wLI.js.map +1 -0
  229. package/dist/web-app/assets/chunk-IMKFNOWR-Ba2_9bl_.js +232 -0
  230. package/dist/web-app/assets/chunk-IMKFNOWR-Ba2_9bl_.js.map +1 -0
  231. package/dist/web-app/assets/chunk-JWPE2WC7-_mnRB2OH.js +2 -0
  232. package/dist/web-app/assets/chunk-JWPE2WC7-_mnRB2OH.js.map +1 -0
  233. package/dist/web-app/assets/chunk-POPQ4Y6H-B8jQTxQO.js +2 -0
  234. package/dist/web-app/assets/chunk-POPQ4Y6H-B8jQTxQO.js.map +1 -0
  235. package/dist/web-app/assets/chunk-SVP7TREG-BO_Jnb_c.js +89 -0
  236. package/dist/web-app/assets/chunk-SVP7TREG-BO_Jnb_c.js.map +1 -0
  237. package/dist/web-app/assets/chunk-TICWLB2K-ZYZ9hFu5.js +207 -0
  238. package/dist/web-app/assets/chunk-TICWLB2K-ZYZ9hFu5.js.map +1 -0
  239. package/dist/web-app/assets/chunk-XXDRQBXY-ByUAYXI3.js +2 -0
  240. package/dist/web-app/assets/chunk-XXDRQBXY-ByUAYXI3.js.map +1 -0
  241. package/dist/web-app/assets/classDiagram-ZZMXUADV-Ddx7RAuT.js +2 -0
  242. package/dist/web-app/assets/classDiagram-ZZMXUADV-Ddx7RAuT.js.map +1 -0
  243. package/dist/web-app/assets/classDiagram-v2-VYDZK3BY-Ddx7RAuT.js +2 -0
  244. package/dist/web-app/assets/classDiagram-v2-VYDZK3BY-Ddx7RAuT.js.map +1 -0
  245. package/dist/web-app/assets/cose-bilkent-JH36ORCC-BuOGJgHG.js +2 -0
  246. package/dist/web-app/assets/cose-bilkent-JH36ORCC-BuOGJgHG.js.map +1 -0
  247. package/dist/web-app/assets/cynefin-OW5HDTMX-DLksGPh8.js +179 -0
  248. package/dist/web-app/assets/cynefin-OW5HDTMX-DLksGPh8.js.map +1 -0
  249. package/dist/web-app/assets/cynefinDiagram-5FMLGOSQ-nJQb5qZR.js +63 -0
  250. package/dist/web-app/assets/cynefinDiagram-5FMLGOSQ-nJQb5qZR.js.map +1 -0
  251. package/dist/web-app/assets/cytoscape.esm-CtNdOvlO.js +332 -0
  252. package/dist/web-app/assets/cytoscape.esm-CtNdOvlO.js.map +1 -0
  253. package/dist/web-app/assets/dagre-GXQ25YYZ-CCF5O_UD.js +5 -0
  254. package/dist/web-app/assets/dagre-GXQ25YYZ-CCF5O_UD.js.map +1 -0
  255. package/dist/web-app/assets/defaultLocale-DX6XiGOO.js +2 -0
  256. package/dist/web-app/assets/defaultLocale-DX6XiGOO.js.map +1 -0
  257. package/dist/web-app/assets/diagram-S7CK7UJ4-C88au4NC.js +31 -0
  258. package/dist/web-app/assets/diagram-S7CK7UJ4-C88au4NC.js.map +1 -0
  259. package/dist/web-app/assets/diagram-UQ7AKVKN-B4QnPs7U.js +42 -0
  260. package/dist/web-app/assets/diagram-UQ7AKVKN-B4QnPs7U.js.map +1 -0
  261. package/dist/web-app/assets/diagram-VSXAHHWV-DeANdVWA.js +4 -0
  262. package/dist/web-app/assets/diagram-VSXAHHWV-DeANdVWA.js.map +1 -0
  263. package/dist/web-app/assets/diagram-VX7I27RA-YzlB_2AY.js +25 -0
  264. package/dist/web-app/assets/diagram-VX7I27RA-YzlB_2AY.js.map +1 -0
  265. package/dist/web-app/assets/diagram-Z3DM3KII-YY9NFaz_.js +25 -0
  266. package/dist/web-app/assets/diagram-Z3DM3KII-YY9NFaz_.js.map +1 -0
  267. package/dist/web-app/assets/ebnfDiagram-PWID7BFC-Cqu9kHUC.js +2 -0
  268. package/dist/web-app/assets/ebnfDiagram-PWID7BFC-Cqu9kHUC.js.map +1 -0
  269. package/dist/web-app/assets/erDiagram-RLTQ6QDP-IHi93MdG.js +100 -0
  270. package/dist/web-app/assets/erDiagram-RLTQ6QDP-IHi93MdG.js.map +1 -0
  271. package/dist/web-app/assets/flowDiagram-HODETNUW-19bm7W5P.js +169 -0
  272. package/dist/web-app/assets/flowDiagram-HODETNUW-19bm7W5P.js.map +1 -0
  273. package/dist/web-app/assets/ganttDiagram-EL5Y4UJY-BvCqvyiB.js +293 -0
  274. package/dist/web-app/assets/ganttDiagram-EL5Y4UJY-BvCqvyiB.js.map +1 -0
  275. package/dist/web-app/assets/gitGraphDiagram-WWUBYQGX-G4Ubl8Fs.js +107 -0
  276. package/dist/web-app/assets/gitGraphDiagram-WWUBYQGX-G4Ubl8Fs.js.map +1 -0
  277. package/dist/web-app/assets/index-C4BdeFB9.css +1 -0
  278. package/dist/web-app/assets/index-Dtn3ZY6J.js +430 -0
  279. package/dist/web-app/assets/index-Dtn3ZY6J.js.map +1 -0
  280. package/dist/web-app/assets/infoDiagram-27XIBGKW-B6rtfbuC.js +3 -0
  281. package/dist/web-app/assets/infoDiagram-27XIBGKW-B6rtfbuC.js.map +1 -0
  282. package/dist/web-app/assets/init-Gi6I4Gst.js +2 -0
  283. package/dist/web-app/assets/init-Gi6I4Gst.js.map +1 -0
  284. package/dist/web-app/assets/ishikawaDiagram-5VMMS53U-Lp8q1D21.js +71 -0
  285. package/dist/web-app/assets/ishikawaDiagram-5VMMS53U-Lp8q1D21.js.map +1 -0
  286. package/dist/web-app/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  287. package/dist/web-app/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  288. package/dist/web-app/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  289. package/dist/web-app/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  290. package/dist/web-app/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  291. package/dist/web-app/assets/journeyDiagram-3NMN7TZE-Av03B9l3.js +140 -0
  292. package/dist/web-app/assets/journeyDiagram-3NMN7TZE-Av03B9l3.js.map +1 -0
  293. package/dist/web-app/assets/kanban-definition-UXKFOSKX-Ddd3_ejb.js +90 -0
  294. package/dist/web-app/assets/kanban-definition-UXKFOSKX-Ddd3_ejb.js.map +1 -0
  295. package/dist/web-app/assets/katex-HP8lGamR.js +258 -0
  296. package/dist/web-app/assets/katex-HP8lGamR.js.map +1 -0
  297. package/dist/web-app/assets/layout-CNjBeOlp.js +2 -0
  298. package/dist/web-app/assets/layout-CNjBeOlp.js.map +1 -0
  299. package/dist/web-app/assets/linear-DDONU9Ir.js +2 -0
  300. package/dist/web-app/assets/linear-DDONU9Ir.js.map +1 -0
  301. package/dist/web-app/assets/mermaid.core-B9X3C61k.js +313 -0
  302. package/dist/web-app/assets/mermaid.core-B9X3C61k.js.map +1 -0
  303. package/dist/web-app/assets/mindmap-definition-YA3MSWOX-I2UME2Ce.js +97 -0
  304. package/dist/web-app/assets/mindmap-definition-YA3MSWOX-I2UME2Ce.js.map +1 -0
  305. package/dist/web-app/assets/ordinal-Cboi1Yqb.js +2 -0
  306. package/dist/web-app/assets/ordinal-Cboi1Yqb.js.map +1 -0
  307. package/dist/web-app/assets/pegDiagram-XKGWAZYB-BER8lNbi.js +2 -0
  308. package/dist/web-app/assets/pegDiagram-XKGWAZYB-BER8lNbi.js.map +1 -0
  309. package/dist/web-app/assets/pieDiagram-E7YTZNPT-FVWVg8rH.js +40 -0
  310. package/dist/web-app/assets/pieDiagram-E7YTZNPT-FVWVg8rH.js.map +1 -0
  311. package/dist/web-app/assets/quadrantDiagram-AXDQQJYC-YlvT1u77.js +8 -0
  312. package/dist/web-app/assets/quadrantDiagram-AXDQQJYC-YlvT1u77.js.map +1 -0
  313. package/dist/web-app/assets/railroadDiagram-O6MQD6OU-CvxT6dMW.js +2 -0
  314. package/dist/web-app/assets/railroadDiagram-O6MQD6OU-CvxT6dMW.js.map +1 -0
  315. package/dist/web-app/assets/requirementDiagram-BXWQKSXE-Caav4EkP.js +85 -0
  316. package/dist/web-app/assets/requirementDiagram-BXWQKSXE-Caav4EkP.js.map +1 -0
  317. package/dist/web-app/assets/sankeyDiagram-P5KCCOFB-B5Uw70Gn.js +41 -0
  318. package/dist/web-app/assets/sankeyDiagram-P5KCCOFB-B5Uw70Gn.js.map +1 -0
  319. package/dist/web-app/assets/sequenceDiagram-WJ2MYXX4-D2SLKVJl.js +163 -0
  320. package/dist/web-app/assets/sequenceDiagram-WJ2MYXX4-D2SLKVJl.js.map +1 -0
  321. package/dist/web-app/assets/sizeCapture-INFHLROL-BVaBfbwa.js +2 -0
  322. package/dist/web-app/assets/sizeCapture-INFHLROL-BVaBfbwa.js.map +1 -0
  323. package/dist/web-app/assets/stateDiagram-D77RDMKH-BlV9y2tf.js +2 -0
  324. package/dist/web-app/assets/stateDiagram-D77RDMKH-BlV9y2tf.js.map +1 -0
  325. package/dist/web-app/assets/stateDiagram-v2-MP3YSRHH-BU4Tu2pp.js +2 -0
  326. package/dist/web-app/assets/stateDiagram-v2-MP3YSRHH-BU4Tu2pp.js.map +1 -0
  327. package/dist/web-app/assets/swimlanes-42K2YHIH-Pv3Wv8c7.js +2 -0
  328. package/dist/web-app/assets/swimlanes-42K2YHIH-Pv3Wv8c7.js.map +1 -0
  329. package/dist/web-app/assets/swimlanesDiagram-VR7AAH4N-BNKjOvPH.js +9 -0
  330. package/dist/web-app/assets/swimlanesDiagram-VR7AAH4N-BNKjOvPH.js.map +1 -0
  331. package/dist/web-app/assets/timeline-definition-24CTP7MA-C9TOilaR.js +121 -0
  332. package/dist/web-app/assets/timeline-definition-24CTP7MA-C9TOilaR.js.map +1 -0
  333. package/dist/web-app/assets/vennDiagram-4TSXK5OY-DeSDprN4.js +35 -0
  334. package/dist/web-app/assets/vennDiagram-4TSXK5OY-DeSDprN4.js.map +1 -0
  335. package/dist/web-app/assets/wardleyDiagram-VM6X3IG4-SulZn-nm.js +79 -0
  336. package/dist/web-app/assets/wardleyDiagram-VM6X3IG4-SulZn-nm.js.map +1 -0
  337. package/dist/web-app/assets/xychartDiagram-S5SC5T6Z-CC6MrQ0h.js +8 -0
  338. package/dist/web-app/assets/xychartDiagram-S5SC5T6Z-CC6MrQ0h.js.map +1 -0
  339. package/dist/web-app/fleet-index.html +19 -0
  340. package/dist/web-app/fleet.webmanifest +37 -0
  341. package/dist/web-app/icons/apple-touch-icon.png +0 -0
  342. package/dist/web-app/icons/favicon-16.png +0 -0
  343. package/dist/web-app/icons/favicon-32.png +0 -0
  344. package/dist/web-app/icons/icon-192-maskable.png +0 -0
  345. package/dist/web-app/icons/icon-192.png +0 -0
  346. package/dist/web-app/icons/icon-512-maskable.png +0 -0
  347. package/dist/web-app/icons/icon-512.png +0 -0
  348. package/dist/web-app/index.html +9 -7
  349. package/dist/web-app/manifest.webmanifest +10 -8
  350. package/dist/web-app/sw.js +210 -40
  351. package/dist/web-app/version.json +1 -0
  352. package/dist/web-app/workspace-install.sh +148 -0
  353. package/dist/workspace-enrollment.d.ts +28 -0
  354. package/dist/workspace-enrollment.js +129 -0
  355. package/docs/room-layouts.md +227 -0
  356. package/docs/task-workspaces.md +130 -0
  357. package/examples/room-layouts/fresh.yaml +27 -0
  358. package/examples/room-layouts/scoped.yaml +21 -0
  359. package/examples/room-layouts/shared.yaml +19 -0
  360. package/package.json +14 -17
  361. package/presets/README.md +78 -0
  362. package/presets/brain-catalog.json +8 -2
  363. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  364. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  365. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  366. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  367. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  368. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  369. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  370. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  371. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  372. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  373. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  374. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  375. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  376. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  377. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  378. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  379. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  380. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  381. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  382. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  383. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  384. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  385. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  386. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  387. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  388. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  389. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  390. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  391. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  392. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  393. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  394. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  395. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  396. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  397. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  398. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  399. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  400. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  401. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  402. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  403. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  404. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  405. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  406. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  407. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  408. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  409. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  410. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  411. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  412. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  413. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  414. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  415. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  416. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  417. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  418. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  419. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  420. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  421. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  422. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  423. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  424. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  425. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  426. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  427. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  428. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  429. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  430. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  431. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  432. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  433. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  434. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  435. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  436. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  437. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  438. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  439. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  440. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  441. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  442. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  443. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  444. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  445. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  446. package/presets/fleet/roles/Coordinator.yaml +22 -0
  447. package/presets/fleet/roles/Engineer.yaml +40 -0
  448. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  449. package/presets/fleet/room_layouts/pair.yaml +24 -0
  450. package/presets/fleet/room_layouts/single.yaml +17 -0
  451. package/presets/fleet/room_layouts/team.yaml +30 -0
  452. package/presets/fleet/room_templates/engineering.yaml +19 -0
  453. package/presets/manifest.json +2 -2
  454. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  455. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  456. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  457. package/dist/web-app/icons/ours-fleet.svg +0 -4
  458. package/dist/web-app/offline.html +0 -18
@@ -1,3 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { packagedPresetRoot } from '../preset-bootstrap.js';
3
+ import { validateCatalog } from '../init-wizard.js';
4
+ import { getAdapter } from '../harness/registry.js';
5
+ import { storeAgentAttachment, attachmentPrompt, MAX_ATTACHMENT_BODY, readAgentAttachment, prepareAttachmentPresentation, presentAttachmentEvent } from './agent-attachments.js';
6
+ import { readChatIdle } from '../temp-idle.js';
7
+ import { agentDir } from '../paths.js';
8
+ import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
1
9
  import { existsSync } from 'node:fs';
2
10
  import { dirname, join } from 'node:path';
3
11
  import { fileURLToPath } from 'node:url';
@@ -13,6 +21,7 @@ import { FleetEventBus } from './events.js';
13
21
  import { ROLE_NAME_RE } from '../config.js';
14
22
  import { TaskListError } from '../rooms-tasks/task-lists.js';
15
23
  import { TaskStateError } from '../rooms-tasks/task-state.js';
24
+ import { isProvider } from '../subscriptions/store.js';
16
25
  const statusFor = (code) => ({
17
26
  role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
18
27
  idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
@@ -33,15 +42,24 @@ export async function buildWebServer(services, boundary, options = {}) {
33
42
  app.addHook('onRequest', async (request, reply) => {
34
43
  reply.header('X-Content-Type-Options', 'nosniff');
35
44
  reply.header('Referrer-Policy', 'no-referrer');
36
- reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
37
- reply.header('X-Frame-Options', 'DENY');
38
- reply.header('Content-Security-Policy', `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; ` +
45
+ reply.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=()');
46
+ // Only the configured account launcher may embed workspace pages.
47
+ reply.header('Content-Security-Policy', `default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; frame-src 'self' blob:; ` +
39
48
  `connect-src 'self' ${auth.secureCookies ? 'wss' : 'ws'}://${auth.host}; object-src 'none'; base-uri 'none'; ` +
40
- `frame-ancestors 'none'; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
49
+ `frame-ancestors 'self' ${auth.appOrigin}; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
41
50
  if (request.url.startsWith('/api/'))
42
51
  reply.header('Cache-Control', 'no-store');
52
+ if (request.headers.origin === auth.appOrigin && request.url.startsWith('/api/')) {
53
+ reply.header('Access-Control-Allow-Origin', auth.appOrigin).header('Vary', 'Origin');
54
+ reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
55
+ reply.header('Access-Control-Allow-Headers', 'Authorization,Content-Type,X-CSRF-Token,Idempotency-Key');
56
+ if (request.method === 'OPTIONS') {
57
+ auth.validateBoundary(request, false);
58
+ return reply.code(204).send();
59
+ }
60
+ }
43
61
  try {
44
- auth.validateBoundary(request, false);
62
+ auth.validateBoundary(request, false, !request.url.startsWith('/api/'));
45
63
  }
46
64
  catch (error) {
47
65
  if (request.url.startsWith('/api/'))
@@ -50,8 +68,24 @@ export async function buildWebServer(services, boundary, options = {}) {
50
68
  return reply.code(421).type('text/html').send(`<!doctype html><html><head><title>Fleet console address</title></head><body><main><h1>This fleet-console address is not configured</h1><p>${escapeHtml(message)}</p><p>For nginx or VPS access, configure an explicit public origin. The console does not guess proxy hosts.</p></main></body></html>`);
51
69
  }
52
70
  });
71
+ app.post('/api/v1/layout-bindings/:id/control', async (request) => {
72
+ // Initial support is direct same-host Fleet-to-Fleet transport.
73
+ const forwarded = Object.keys(request.headers).some(name => name === 'forwarded' || name.startsWith('x-forwarded-') || name === 'x-real-ip' || name === 'via');
74
+ if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(request.ip) || forwarded)
75
+ throw new FleetError('forbidden', 'layout bindings require direct loopback access');
76
+ // Machine credentials cannot authenticate browser or lifecycle routes.
77
+ if (request.headers.origin)
78
+ auth.validateBoundary(request, true);
79
+ if (!services.layoutBindings)
80
+ throw new FleetError('capability_unavailable', 'layout bindings unavailable');
81
+ return services.layoutBindings.control(request.params.id, request.headers.authorization, request.body);
82
+ });
53
83
  app.setErrorHandler(async (error, request, reply) => {
54
- const fleetError = normalizeError(error, request.id);
84
+ const privateToolParseError = ['/api/v1/roles/:id/ours/call', '/api/v1/layout-bindings/:id/control'].includes(request.routeOptions.url ?? '')
85
+ && error instanceof Error && 'code' in error
86
+ && typeof error.code === 'string' && error.code.startsWith('FST_ERR_CTP_');
87
+ const fleetError = normalizeError(privateToolParseError
88
+ ? new FleetError('invalid_request', 'expected a valid JSON tool request') : error, request.id);
55
89
  await audit.record({
56
90
  requestId: request.id, action: `${request.method} ${request.routeOptions.url ?? request.url}`,
57
91
  result: 'rejected', errorCode: fleetError.code,
@@ -96,6 +130,20 @@ export async function buildWebServer(services, boundary, options = {}) {
96
130
  const session = auth.authenticate(request);
97
131
  return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
98
132
  });
133
+ app.get('/api/v1/workspace/identity', async () => auth.workspaceIdentity());
134
+ app.post('/api/v1/devices/enroll', async (request, reply) => {
135
+ reply.header('Cache-Control', 'no-store');
136
+ const input = request.body;
137
+ if (!input)
138
+ throw new FleetError('invalid_request', 'device enrollment body required');
139
+ return auth.enrollWorkspace(request, input);
140
+ });
141
+ app.post('/api/v1/devices/link', async (request) => {
142
+ const session = auth.authenticate(request, true);
143
+ return { version: 1, origin: auth.origin, ...auth.workspaceEnrollment(session.id) };
144
+ });
145
+ app.get('/api/v1/devices', async (request) => { auth.authenticate(request); return { devices: auth.listWorkspaceDevices() }; });
146
+ app.delete('/api/v1/devices/:id', async (request) => { auth.authenticate(request, true); auth.revokeWorkspaceDevice(request.params.id); return { revoked: true }; });
99
147
  app.get('/api/v1/meta', async (request) => {
100
148
  auth.authenticate(request);
101
149
  return {
@@ -106,6 +154,19 @@ export async function buildWebServer(services, boundary, options = {}) {
106
154
  auditDegraded: audit.degraded,
107
155
  };
108
156
  });
157
+ app.get('/api/v1/onboarding', async (request) => {
158
+ auth.authenticate(request);
159
+ const harnesses = await Promise.all(['codex', 'claude-code'].map(async (harness) => {
160
+ try {
161
+ return { harness, ...await getAdapter(harness).checkPrereqs() };
162
+ }
163
+ catch {
164
+ return { harness, ok: false, checks: [{ name: 'availability', ok: false, detail: 'Harness checks unavailable' }] };
165
+ }
166
+ }));
167
+ const catalog = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8')));
168
+ return { harnesses, catalog: catalog.models, providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator' };
169
+ });
109
170
  app.get('/api/v1/creation-capabilities', async (request) => {
110
171
  auth.authenticate(request);
111
172
  return services.creation.capabilities();
@@ -128,7 +189,8 @@ export async function buildWebServer(services, boundary, options = {}) {
128
189
  });
129
190
  app.get('/api/v1/roles', async (request) => {
130
191
  auth.authenticate(request);
131
- return { roles: await services.query.list() };
192
+ const query = request.query;
193
+ return { roles: await services.query.list(query.includeTemporary === 'true') };
132
194
  });
133
195
  const taskApi = async (fn) => {
134
196
  try {
@@ -141,6 +203,8 @@ export async function buildWebServer(services, boundary, options = {}) {
141
203
  ? 'conflict' : 'invalid_request';
142
204
  throw new FleetError(code, error.message);
143
205
  }
206
+ if (error instanceof TaskStateError && 'accepted' in error && error.accepted === false)
207
+ return Promise.reject(Object.assign(new FleetError('invalid_request', error.message), { memberNotAccepted: true }));
144
208
  if (error instanceof TaskStateError) {
145
209
  throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
146
210
  }
@@ -188,9 +252,10 @@ export async function buildWebServer(services, boundary, options = {}) {
188
252
  const state = query.state && query.state !== 'all' ? query.state : undefined;
189
253
  const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
190
254
  ...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
255
+ const api = requireTaskRooms();
191
256
  return taskApi(() => query.groupByList === 'true'
192
- ? { groups: requireTaskRooms().groupedTasks(filter) }
193
- : { tasks: requireTaskRooms().listTasks(filter) });
257
+ ? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => api.withLayoutRooms(task)) })) }
258
+ : { tasks: api.listTasks(filter).map(task => api.withLayoutRooms(task)) });
194
259
  });
195
260
  app.delete('/api/v1/tasks/:id', async (request, reply) => {
196
261
  auth.authenticate(request, true);
@@ -221,12 +286,17 @@ export async function buildWebServer(services, boundary, options = {}) {
221
286
  const body = request.body;
222
287
  if (typeof body?.title !== 'string' || !body.title)
223
288
  throw new FleetError('invalid_request', 'title is required');
289
+ // Every task runs a saved Room Layout; rooms are never defined inline or from room templates here.
290
+ if (typeof body.layout !== 'string' || !body.layout)
291
+ throw new FleetError('invalid_request', 'layout is required: choose a room layout from Settings › Room layouts');
292
+ if (body.template !== undefined || body.noRoom !== undefined)
293
+ throw new FleetError('invalid_request', 'tasks use room layouts; template and noRoom are not accepted');
224
294
  const task = await taskApi(() => requireTaskRooms().createTask({
225
295
  actor: { kind: 'local_control', surface: 'web' }, title: body.title,
226
296
  brief: typeof body.brief === 'string' ? body.brief : undefined,
227
- template: typeof body.template === 'string' ? body.template : undefined,
228
- backlog: body.backlog === true, noRoom: body.noRoom === true,
297
+ backlog: body.backlog === true,
229
298
  list: typeof body.list === 'string' ? body.list : undefined,
299
+ layout: body.layout,
230
300
  idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
231
301
  ? request.headers['idempotency-key'] : undefined,
232
302
  origin: { type: 'web' },
@@ -240,11 +310,162 @@ export async function buildWebServer(services, boundary, options = {}) {
240
310
  const list = String(request.body?.list ?? '');
241
311
  return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
242
312
  });
313
+ app.patch('/api/v1/tasks/:id/description', async (request) => {
314
+ auth.authenticate(request, true);
315
+ const body = request.body;
316
+ if (typeof body?.brief !== 'string' || typeof body.expectedBrief !== 'string' || body.brief.length > 100_000)
317
+ throw new FleetError('invalid_request', 'brief and expectedBrief must be strings; brief maximum is 100000 characters');
318
+ return { task: await taskApi(() => requireTaskRooms().editTaskDescription({
319
+ actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
320
+ brief: body.brief, expectedBrief: body.expectedBrief,
321
+ })) };
322
+ });
323
+ app.get('/api/v1/tasks/:id', async (request) => {
324
+ auth.authenticate(request);
325
+ return taskApi(() => requireTaskRooms().getTask(request.params.id));
326
+ });
327
+ app.post('/api/v1/tasks/:id/members', async (request, reply) => {
328
+ auth.authenticate(request, true);
329
+ const body = request.body;
330
+ try {
331
+ const result = await taskApi(() => requireTaskRooms().addMember(request.params.id, body));
332
+ reply.code(result.state === 'running' ? 202 : 200);
333
+ return result;
334
+ }
335
+ catch (error) {
336
+ if (error instanceof Error && 'memberNotAccepted' in error)
337
+ return reply.code(409).send({ error: { code: 'member_not_accepted', message: error.message }, accepted: false });
338
+ throw error;
339
+ }
340
+ });
341
+ app.get('/api/v1/tasks/:id/member-additions/:requestId', async (request) => {
342
+ auth.authenticate(request);
343
+ const { id, requestId } = request.params;
344
+ return taskApi(() => requireTaskRooms().memberAddition(id, requestId));
345
+ });
346
+ app.patch('/api/v1/tasks/:id/layout', async (request) => {
347
+ const session = auth.authenticate(request, true);
348
+ const body = request.body;
349
+ const name = (v) => typeof v === 'string' && v.length > 0;
350
+ if (!name(body?.layout) || !(body.expectedLayout === null || name(body.expectedLayout)))
351
+ throw new FleetError('invalid_request', 'layout must be a layout name; expectedLayout a layout name or null');
352
+ const task = await taskApi(() => requireTaskRooms().setTaskLayout({ actor: { kind: 'local_control', surface: 'web' },
353
+ taskId: request.params.id, layout: body.layout, expectedLayout: body.expectedLayout }));
354
+ await audit.record({ requestId: request.id, browser: session.id, action: 'task.layout.set', result: 'succeeded' });
355
+ return { task: requireTaskRooms().withLayoutRooms(task) };
356
+ });
357
+ app.get('/api/v1/tasks/:id/layout', async (request) => {
358
+ auth.authenticate(request);
359
+ return { layout: await taskApi(async () => requireTaskRooms().taskLayout(request.params.id)) };
360
+ });
361
+ for (const operation of ['open', 'close']) {
362
+ app.post(`/api/v1/tasks/:id/layout/rooms/:room/${operation}`, async (request, reply) => {
363
+ const session = auth.authenticate(request, true);
364
+ const { id, room } = request.params;
365
+ const accepted = await taskApi(() => requireTaskRooms().launchTaskLayoutOperation({
366
+ actor: { kind: 'local_control', surface: 'web' }, taskId: id, room,
367
+ operation: operation === 'open' ? 'open' : 'close-room',
368
+ }));
369
+ await audit.record({ requestId: request.id, browser: session.id, action: `task.layout.${operation}`, result: 'succeeded' });
370
+ reply.code(202);
371
+ return { operation: accepted };
372
+ });
373
+ }
374
+ app.post('/api/v1/tasks/:id/layout/cleanup', async (request, reply) => {
375
+ auth.authenticate(request, true);
376
+ const accepted = await taskApi(() => requireTaskRooms().retryTaskLayoutCleanup({
377
+ actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
378
+ }));
379
+ reply.code(202);
380
+ return { operation: accepted };
381
+ });
382
+ app.get('/api/v1/task-templates', async (request) => {
383
+ auth.authenticate(request);
384
+ return { templates: requireTaskRooms().listTemplates() };
385
+ });
386
+ const layouts = () => {
387
+ if (!services.roomLayouts)
388
+ throw new FleetError('capability_unavailable', 'room layout editing is unavailable');
389
+ return services.roomLayouts;
390
+ };
391
+ app.get('/api/v1/room-layouts', async (request) => {
392
+ auth.authenticate(request);
393
+ const list = layouts().list(), defaultLayout = layouts().defaultLayout(list);
394
+ return { layouts: list, ...(defaultLayout ? { default_layout: defaultLayout } : {}) };
395
+ });
396
+ app.post('/api/v1/room-layouts/validate', async (request) => {
397
+ auth.authenticate(request, true);
398
+ return layouts().validate(request.body?.definition);
399
+ });
400
+ app.put('/api/v1/room-layouts/:name', async (request) => {
401
+ const session = auth.authenticate(request, true);
402
+ const body = request.body;
403
+ if (typeof body?.revision !== 'string')
404
+ throw new FleetError('invalid_request', 'revision is required');
405
+ const result = await layouts().save(request.params.name, body.revision, body.definition);
406
+ events.publish('room_layouts.changed', { name: result.name, revision: result.revision });
407
+ await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.save', result: 'succeeded' });
408
+ return result;
409
+ });
410
+ app.delete('/api/v1/room-layouts/:name', async (request) => {
411
+ const session = auth.authenticate(request, true);
412
+ const revision = request.query.revision;
413
+ if (typeof revision !== 'string' || !revision)
414
+ throw new FleetError('invalid_request', 'revision is required');
415
+ const result = await layouts().remove(request.params.name, revision);
416
+ events.publish('room_layouts.changed', { name: result.name });
417
+ await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.delete', result: 'succeeded' });
418
+ return result;
419
+ });
420
+ for (const action of ['start', 'block', 'unblock', 'review', 'finish', 'cancel']) {
421
+ app.post(`/api/v1/tasks/:id/${action}`, async (request, reply) => {
422
+ auth.authenticate(request, true);
423
+ const taskId = request.params.id;
424
+ const body = (request.body ?? {});
425
+ const input = { actor: { kind: 'local_control', surface: 'web' }, taskId };
426
+ const api = requireTaskRooms();
427
+ return taskApi(async () => {
428
+ switch (action) {
429
+ case 'start': {
430
+ // A task's layout can be chosen or changed but never removed, so this check cannot go stale.
431
+ if (!api.getTask(taskId).task.layout)
432
+ throw new FleetError('conflict', 'choose a room layout for this task before starting it');
433
+ const task = await api.startTask(input);
434
+ const provisioning = await api.launchTaskProvisioning(taskId);
435
+ if (provisioning.kind !== 'ready')
436
+ reply.code(202);
437
+ return { task, provisioning };
438
+ }
439
+ case 'block':
440
+ if (typeof body.reason !== 'string' || !body.reason.trim())
441
+ throw new FleetError('invalid_request', 'reason is required');
442
+ return { task: api.blockTask({ ...input, reason: body.reason }) };
443
+ case 'unblock': return { task: api.unblockTask(input) };
444
+ case 'review': return { task: api.reviewTask(input) };
445
+ case 'finish':
446
+ case 'cancel': {
447
+ const plan = action === 'finish' ? await api.finishTask(input) : await api.cancelTask(input);
448
+ const task = plan.settlementRequired ? await api.launchTaskSettlement(taskId) : plan.task;
449
+ const pending = task.terminal_intent?.status === 'pending';
450
+ if (pending)
451
+ reply.code(202);
452
+ return { task, pending };
453
+ }
454
+ }
455
+ });
456
+ });
457
+ }
458
+ app.get('/api/v1/configuration/provenance', async (request) => {
459
+ auth.authenticate(request);
460
+ if (!services.presetProvenance)
461
+ throw new FleetError('capability_unavailable', 'preset provenance is unavailable');
462
+ return { provenance: services.presetProvenance.read() };
463
+ });
243
464
  app.get('/api/v1/configuration', async (request) => {
244
465
  auth.authenticate(request);
245
466
  if (!services.configuration)
246
467
  throw new FleetError('capability_unavailable', 'fleet configuration editing is unavailable');
247
- return services.configuration.read();
468
+ return services.configuration.read(request.query.includeDefinitions === 'true');
248
469
  });
249
470
  app.post('/api/v1/configuration/preview', async (request) => {
250
471
  auth.authenticate(request, true);
@@ -265,6 +486,86 @@ export async function buildWebServer(services, boundary, options = {}) {
265
486
  });
266
487
  return result;
267
488
  });
489
+ // Claude Code / Codex subscription profiles. Responses carry account metadata
490
+ // only; token values never leave the CLI-owned credential files.
491
+ const subscriptions = () => {
492
+ if (!services.subscriptions)
493
+ throw new FleetError('capability_unavailable', 'subscription management is unavailable');
494
+ return services.subscriptions;
495
+ };
496
+ const provider = (raw) => {
497
+ if (!isProvider(raw))
498
+ throw new FleetError('invalid_request', 'unknown subscription provider');
499
+ return raw;
500
+ };
501
+ const subscriptionsChanged = () => events.publish('subscription.changed', {});
502
+ app.get('/api/v1/subscriptions', async (request) => {
503
+ auth.authenticate(request);
504
+ return { providers: await subscriptions().list() };
505
+ });
506
+ app.post('/api/v1/subscriptions/:provider/logins', async (request, reply) => {
507
+ const session = auth.authenticate(request, true);
508
+ const login = await subscriptions().startLogin(provider(request.params.provider), session.id);
509
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.${request.params.provider}`, result: login.state });
510
+ reply.code(201);
511
+ return login;
512
+ });
513
+ app.get('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
514
+ const session = auth.authenticate(request);
515
+ return subscriptions().login(request.params.loginId, session.id).view();
516
+ });
517
+ app.post('/api/v1/subscriptions/:provider/logins/:loginId/code', async (request) => {
518
+ const session = auth.authenticate(request, true);
519
+ const code = request.body?.code;
520
+ if (typeof code !== 'string')
521
+ throw new FleetError('invalid_request', 'code is required');
522
+ const login = subscriptions().login(request.params.loginId, session.id);
523
+ login.submitCode(code.trim());
524
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.code.${request.params.provider}`, result: 'submitted' });
525
+ return login.view();
526
+ });
527
+ app.delete('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
528
+ const session = auth.authenticate(request, true);
529
+ const login = subscriptions().login(request.params.loginId, session.id);
530
+ login.cancel();
531
+ return login.view();
532
+ });
533
+ app.post('/api/v1/subscriptions/:provider/active', async (request) => {
534
+ const session = auth.authenticate(request, true);
535
+ const profileId = request.body?.profileId;
536
+ if (typeof profileId !== 'string')
537
+ throw new FleetError('invalid_request', 'profileId is required');
538
+ const view = await subscriptions().setActive(provider(request.params.provider), profileId);
539
+ subscriptionsChanged();
540
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.activate.${request.params.provider}`, result: 'succeeded' });
541
+ return view;
542
+ });
543
+ app.post('/api/v1/subscriptions/:provider/profiles/:profileId/check', async (request) => {
544
+ auth.authenticate(request, true);
545
+ return subscriptions().check(provider(request.params.provider), request.params.profileId);
546
+ });
547
+ app.post('/api/v1/subscriptions/claude/profiles/:profileId/probe', async (request) => {
548
+ const session = auth.authenticate(request, true);
549
+ const usage = await subscriptions().probeClaudeUsage(request.params.profileId);
550
+ await audit.record({ requestId: request.id, browser: session.id, action: 'subscription.usage.probe.claude', result: usage.source });
551
+ return { usage };
552
+ });
553
+ app.patch('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
554
+ auth.authenticate(request, true);
555
+ const label = request.body?.label;
556
+ if (typeof label !== 'string')
557
+ throw new FleetError('invalid_request', 'label is required');
558
+ await subscriptions().rename(provider(request.params.provider), request.params.profileId, label);
559
+ subscriptionsChanged();
560
+ return { ok: true };
561
+ });
562
+ app.delete('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
563
+ const session = auth.authenticate(request, true);
564
+ await subscriptions().remove(provider(request.params.provider), request.params.profileId);
565
+ subscriptionsChanged();
566
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.remove.${request.params.provider}`, result: 'succeeded' });
567
+ return { ok: true };
568
+ });
268
569
  app.get('/api/v1/topology', async (request) => {
269
570
  auth.authenticate(request);
270
571
  if (!services.topology)
@@ -311,9 +612,36 @@ export async function buildWebServer(services, boundary, options = {}) {
311
612
  });
312
613
  return result;
313
614
  });
615
+ const oursTools = services.oursTools ?? new SupervisorOursTools();
616
+ app.get('/api/v1/roles/:id/contacts', async (request) => {
617
+ auth.authenticate(request);
618
+ const control = await services.session(request.params.id);
619
+ if (!control.agentContacts)
620
+ throw new FleetError('capability_unavailable', 'Supervisor contacts API is unavailable');
621
+ return control.agentContacts();
622
+ });
623
+ app.get('/api/v1/roles/:id/messages', async (request) => {
624
+ auth.authenticate(request);
625
+ const control = await services.session(request.params.id);
626
+ if (!control.agentHistory)
627
+ throw new FleetError('capability_unavailable', 'Supervisor history API is unavailable');
628
+ return control.agentHistory({ peer_cid: request.query.peer_cid, ...(request.query.limit !== undefined ? { limit: Number(request.query.limit) } : {}), ...(request.query.before_seq !== undefined ? { before_seq: Number(request.query.before_seq) } : {}) });
629
+ });
630
+ app.get('/api/v1/roles/:id/ours/tools', async (request) => {
631
+ auth.authenticate(request);
632
+ return oursTools.list(request.params.id);
633
+ });
634
+ app.post('/api/v1/roles/:id/ours/call', async (request) => {
635
+ const session = auth.authenticate(request, true);
636
+ const result = await oursTools.call(request.params.id, request.body);
637
+ await audit.record({ requestId: request.id, browser: session.id,
638
+ action: 'ours.call', result: result.result.isError === true ? 'tool_error' : 'succeeded' });
639
+ return result;
640
+ });
314
641
  app.get('/api/v1/roles/:id', async (request) => {
315
642
  auth.authenticate(request);
316
- return services.query.detail(request.params.id);
643
+ const detail = await services.query.detail(request.params.id);
644
+ return { ...detail, chatIdle: detail.role.lifetime === 'temporary' ? readChatIdle(agentDir(request.params.id, true)) ?? null : null };
317
645
  });
318
646
  app.get('/api/v1/roles/:id/removal-preview', async (request) => {
319
647
  auth.authenticate(request);
@@ -348,15 +676,79 @@ export async function buildWebServer(services, boundary, options = {}) {
348
676
  const control = await services.session(request.params.id);
349
677
  if (!control.conversationPage)
350
678
  throw new FleetError('capability_unavailable', 'this role has no conversation ledger');
351
- return control.conversationPage({
352
- after: request.query.after,
353
- limit: request.query.limit ? Number(request.query.limit) : undefined,
354
- });
679
+ const limit = request.query.limit ? Number(request.query.limit) : undefined;
680
+ // `latest` / `before` page backwards from the newest event; `after` pages forward for live updates.
681
+ const backward = request.query.latest === '1' || request.query.before !== undefined;
682
+ if (backward && request.query.after !== undefined)
683
+ throw new FleetError('invalid_request', 'after cannot be combined with latest or before');
684
+ if (backward && !control.conversationTail)
685
+ throw new FleetError('capability_unavailable', 'this role cannot page its conversation backwards');
686
+ const role = await services.repository.get(request.params.id);
687
+ const stateDir = role ? services.repository.stateDir(role) : undefined;
688
+ const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
689
+ if (backward) {
690
+ const page = await control.conversationTail({ before: request.query.before, limit });
691
+ return { ...page, events: page.events.map(present), context: page.context.map(present) };
692
+ }
693
+ const page = await control.conversationPage({ after: request.query.after, limit });
694
+ return { ...page, events: page.events.map(present) };
695
+ });
696
+ app.get('/api/v1/roles/:id/attachments/:attachmentId', async (request, reply) => {
697
+ auth.authenticate(request);
698
+ const role = await services.repository.get(request.params.id);
699
+ if (!role)
700
+ throw new FleetError('role_not_found', 'Agent not found');
701
+ const stateDir = services.repository.stateDir(role);
702
+ if (!stateDir)
703
+ throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
704
+ const { record, bytes } = readAgentAttachment(stateDir, request.params.attachmentId);
705
+ const image = /^image\/(png|jpeg|gif|webp|avif)$/.test(record.mimeType);
706
+ reply.header('Cache-Control', 'private, no-store');
707
+ reply.header('X-Content-Type-Options', 'nosniff');
708
+ reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
709
+ reply.header('Content-Disposition', `${image ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(record.name).replace(/'/g, '%27')}`);
710
+ return reply.type(image ? record.mimeType : 'application/octet-stream').send(bytes);
711
+ });
712
+ app.post('/api/v1/roles/:id/attachments', { bodyLimit: MAX_ATTACHMENT_BODY }, async (request) => {
713
+ auth.authenticate(request, true);
714
+ const role = await services.repository.get(request.params.id);
715
+ if (!role)
716
+ throw new FleetError('role_not_found', 'Agent not found');
717
+ const stateDir = services.repository.stateDir(role);
718
+ if (!stateDir)
719
+ throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
720
+ const body = request.body;
721
+ if (typeof body?.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
722
+ throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
723
+ const control = await services.session(request.params.id);
724
+ if (!control.conversationPage || !control.submitPromptV2)
725
+ throw new FleetError('capability_unavailable', 'Session-bound attachments are unavailable');
726
+ const page = await control.conversationPage({ limit: 1 });
727
+ if (page.snapshot.sessionGeneration !== body.expectedSessionGeneration)
728
+ throw new FleetError('stale_state', 'Agent session changed. Select the files again.');
729
+ return storeAgentAttachment(stateDir, body.expectedSessionGeneration, request.body);
355
730
  });
356
731
  app.post('/api/v1/roles/:id/input', async (request, reply) => {
357
732
  const session = auth.authenticate(request, true);
358
733
  const body = request.body;
359
- const text = String(body?.text ?? '');
734
+ if (body?.expectedSessionGeneration !== undefined && (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim()))
735
+ throw new FleetError('invalid_request', 'expectedSessionGeneration must be a nonempty string');
736
+ let text = String(body?.text ?? '');
737
+ if (body?.attachments !== undefined) {
738
+ if (typeof body.commandId !== 'string' || !body.commandId.trim())
739
+ throw new FleetError('invalid_request', 'commandId is required for attachments');
740
+ if (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
741
+ throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
742
+ const role = await services.repository.get(request.params.id);
743
+ if (!role)
744
+ throw new FleetError('role_not_found', 'Agent not found');
745
+ const stateDir = services.repository.stateDir(role);
746
+ if (!stateDir)
747
+ throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
748
+ const originalText = text;
749
+ text = attachmentPrompt(stateDir, body.expectedSessionGeneration, body.attachments, text);
750
+ prepareAttachmentPresentation(stateDir, body.expectedSessionGeneration, body.commandId, body.attachments, originalText, text);
751
+ }
360
752
  const commandId = typeof body?.commandId === 'string' && body.commandId.trim()
361
753
  ? body.commandId : undefined;
362
754
  const control = await services.session(request.params.id);
@@ -367,6 +759,7 @@ export async function buildWebServer(services, boundary, options = {}) {
367
759
  const admittedCommandId = commandId ?? randomBytes(16).toString('hex');
368
760
  const receipt = await control.submitPromptV2({
369
761
  commandId: admittedCommandId, text, source: 'owner_admin_console',
762
+ expectedSessionGeneration: body.expectedSessionGeneration,
370
763
  actorBrowserSession: createHmac('sha256', digestKey).update(session.id).digest('hex').slice(0, 24),
371
764
  });
372
765
  await audit.record({
@@ -378,6 +771,8 @@ export async function buildWebServer(services, boundary, options = {}) {
378
771
  reply.code(202);
379
772
  return receipt;
380
773
  }
774
+ if (body.expectedSessionGeneration !== undefined)
775
+ throw new FleetError('capability_unavailable', 'session-bound voice input is unavailable for this role');
381
776
  const receipt = await control.sendText(text);
382
777
  await audit.record({
383
778
  requestId: request.id, browser: session.id, roleId: request.params.id,
@@ -443,10 +838,22 @@ export async function buildWebServer(services, boundary, options = {}) {
443
838
  const body = request.body;
444
839
  if (!body.action || !['start', 'stop', 'restart_resume', 'restart_fresh'].includes(body.action))
445
840
  throw new FleetError('invalid_request', 'invalid lifecycle action');
446
- const receipt = await services.commands.execute({
447
- roleId: request.params.id, action: body.action,
448
- actionId: body.actionId, confirmation: body.confirmation,
449
- });
841
+ let receipt;
842
+ try {
843
+ receipt = await services.commands.execute({
844
+ roleId: request.params.id, action: body.action,
845
+ actionId: body.actionId, confirmation: body.confirmation,
846
+ });
847
+ }
848
+ catch (error) {
849
+ // An existing receipt always wins: a conflicting/repeated action is not a
850
+ // proof that the original operation was never admitted.
851
+ if (typeof body.actionId === 'string' && !services.commands.get(body.actionId)) {
852
+ const normalized = normalizeError(error);
853
+ return reply.code(409).send({ error: { code: 'action_not_accepted', message: normalized.message }, accepted: false });
854
+ }
855
+ throw error;
856
+ }
450
857
  events.publish('action.changed', receipt, request.params.id);
451
858
  reply.code(202);
452
859
  return receipt;
@@ -534,6 +941,9 @@ export async function buildWebServer(services, boundary, options = {}) {
534
941
  }
535
942
  socket.send(JSON.stringify(payload));
536
943
  };
944
+ const role = await services.repository.get(request.params.id);
945
+ const stateDir = role ? services.repository.stateDir(role) : undefined;
946
+ const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
537
947
  const follow = await control.followConversation({
538
948
  after,
539
949
  onPage: page => {
@@ -544,11 +954,11 @@ export async function buildWebServer(services, boundary, options = {}) {
544
954
  lastCursor: page.nextCursor ?? after,
545
955
  });
546
956
  if (page.events.length)
547
- guardedSend({ type: 'events', events: page.events });
957
+ guardedSend({ type: 'events', events: page.events.map(present) });
548
958
  if (page.hasMore)
549
959
  guardedSend({ type: 'resync.required' });
550
960
  },
551
- onEvent: event => guardedSend({ type: 'events', events: [event] }),
961
+ onEvent: event => guardedSend({ type: 'events', events: [present(event)] }),
552
962
  onClose: reason => {
553
963
  if (socket.readyState === socket.OPEN)
554
964
  socket.close(4409, (reason ?? 'conversation stream ended').slice(0, 120));
@@ -561,13 +971,17 @@ export async function buildWebServer(services, boundary, options = {}) {
561
971
  socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
562
972
  });
563
973
  });
564
- const staticRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
974
+ const staticRoot = options.staticRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
565
975
  if (existsSync(staticRoot)) {
566
976
  await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
567
977
  app.setNotFoundHandler((request, reply) => {
568
978
  if (request.url.startsWith('/api/'))
569
979
  return reply.code(404).send({ error: 'not found' });
570
- return reply.sendFile('index.html');
980
+ const path = request.url.split('?')[0];
981
+ if ((path === '/chats' || path.startsWith('/chats/')) && existsSync(join(staticRoot, 'fleet-index.html')))
982
+ return reply.redirect((request.url.includes('?') ? '/fleet/chats' : '/fleet') + (request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : ''));
983
+ const fleetEntry = (path === '/fleet' || path.startsWith('/fleet/')) && existsSync(join(staticRoot, 'fleet-index.html'));
984
+ return reply.sendFile(fleetEntry ? 'fleet-index.html' : 'index.html');
571
985
  });
572
986
  }
573
987
  return {
@@ -582,17 +996,17 @@ export async function buildWebServer(services, boundary, options = {}) {
582
996
  function setAuthCookies(reply, session, device, secure = false) {
583
997
  const suffix = secure ? '; Secure' : '';
584
998
  reply.header('Set-Cookie', [
585
- `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=28800${suffix}`,
586
- `ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=2592000${suffix}`,
999
+ `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${suffix}`,
1000
+ `ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000${suffix}`,
587
1001
  ]);
588
1002
  }
589
1003
  function setSessionCookie(reply, session, secure = false) {
590
- reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=28800${secure ? '; Secure' : ''}`);
1004
+ reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${secure ? '; Secure' : ''}`);
591
1005
  }
592
1006
  function clearAuthCookies(reply) {
593
1007
  reply.header('Set-Cookie', [
594
- 'ofs_session=; HttpOnly; SameSite=Strict; Path=/api; Max-Age=0',
595
- 'ofs_device=; HttpOnly; SameSite=Strict; Path=/api; Max-Age=0',
1008
+ 'ofs_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
1009
+ 'ofs_device=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
596
1010
  ]);
597
1011
  }
598
1012
  function promoteRequest(body) {