@ours.network/fleet 1.2.0-nightly.7 → 1.2.0-nightly.71

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 (423) hide show
  1. package/README.md +279 -68
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.js +26 -9
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/file-rpc.d.ts +12 -0
  8. package/dist/agent-ours/file-rpc.js +18 -4
  9. package/dist/agent-ours/harness.js +9 -1
  10. package/dist/agent-ours/mcp-endpoint.d.ts +19 -0
  11. package/dist/agent-ours/mcp-endpoint.js +98 -25
  12. package/dist/agent-ours/runtime.d.ts +9 -1
  13. package/dist/agent-ours/runtime.js +50 -2
  14. package/dist/agent-ours/service.d.ts +24 -1
  15. package/dist/agent-ours/service.js +97 -13
  16. package/dist/application/codex-models.d.ts +15 -0
  17. package/dist/application/codex-models.js +56 -0
  18. package/dist/application/conversation-history.d.ts +34 -0
  19. package/dist/application/conversation-history.js +280 -0
  20. package/dist/application/conversation-role-observer.d.ts +62 -0
  21. package/dist/application/conversation-role-observer.js +206 -0
  22. package/dist/application/fleet-query-service.d.ts +1 -1
  23. package/dist/application/fleet-query-service.js +3 -3
  24. package/dist/application/log-service.js +8 -8
  25. package/dist/application/model-catalog.d.ts +1 -1
  26. package/dist/application/model-catalog.js +5 -2
  27. package/dist/application/preset-provenance.d.ts +19 -0
  28. package/dist/application/preset-provenance.js +59 -0
  29. package/dist/application/resource-observer.d.ts +40 -0
  30. package/dist/application/resource-observer.js +278 -0
  31. package/dist/application/role-creation-service.js +3 -2
  32. package/dist/application/role-removal-service.js +17 -3
  33. package/dist/application/role-repository.d.ts +1 -1
  34. package/dist/application/role-repository.js +30 -7
  35. package/dist/application/room-layout-definitions.d.ts +45 -0
  36. package/dist/application/room-layout-definitions.js +130 -0
  37. package/dist/application/session-control.d.ts +61 -17
  38. package/dist/application/session-control.js +207 -23
  39. package/dist/application/supervisor-ours-tools.js +5 -11
  40. package/dist/application/task-layouts.d.ts +91 -0
  41. package/dist/application/task-layouts.js +261 -0
  42. package/dist/application/task-room-service.d.ts +101 -3
  43. package/dist/application/task-room-service.js +286 -33
  44. package/dist/application/types.d.ts +2 -0
  45. package/dist/atomic-file.d.ts +34 -1
  46. package/dist/atomic-file.js +161 -18
  47. package/dist/briefing.d.ts +3 -1
  48. package/dist/briefing.js +97 -31
  49. package/dist/build-info.json +9 -4
  50. package/dist/capabilities.d.ts +9 -1
  51. package/dist/capabilities.js +13 -0
  52. package/dist/cli.js +400 -37
  53. package/dist/client-profile.d.ts +5 -17
  54. package/dist/client-profile.js +4 -127
  55. package/dist/config.d.ts +45 -0
  56. package/dist/config.js +57 -6
  57. package/dist/creation.d.ts +1 -1
  58. package/dist/creation.js +4 -4
  59. package/dist/daemon-recovery.d.ts +14 -9
  60. package/dist/daemon-recovery.js +124 -115
  61. package/dist/docs.d.ts +1 -1
  62. package/dist/docs.js +207 -19
  63. package/dist/doctor.d.ts +4 -1
  64. package/dist/doctor.js +75 -60
  65. package/dist/erased-resources.d.ts +9 -0
  66. package/dist/erased-resources.js +87 -0
  67. package/dist/file-delivery/reader.d.ts +15 -0
  68. package/dist/file-delivery/reader.js +63 -0
  69. package/dist/file-delivery/store.d.ts +15 -0
  70. package/dist/file-delivery/store.js +124 -0
  71. package/dist/file-delivery/types.d.ts +19 -0
  72. package/dist/file-delivery/types.js +1 -0
  73. package/dist/fleet-command-audit.d.ts +7 -0
  74. package/dist/fleet-command-audit.js +24 -8
  75. package/dist/harness/agent-session.d.ts +4 -0
  76. package/dist/harness/claude-code-session.js +1 -1
  77. package/dist/harness/claude-code.js +21 -11
  78. package/dist/harness/claude-runtime.d.ts +12 -0
  79. package/dist/harness/claude-runtime.js +43 -0
  80. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  81. package/dist/harness/codex-app-server-proxy.js +41 -5
  82. package/dist/harness/codex-reasoning.d.ts +5 -0
  83. package/dist/harness/codex-reasoning.js +12 -0
  84. package/dist/harness/codex-runtime.d.ts +11 -1
  85. package/dist/harness/codex-runtime.js +31 -4
  86. package/dist/harness/codex-session.js +2 -2
  87. package/dist/harness/codex.js +24 -12
  88. package/dist/init-wizard.d.ts +2 -0
  89. package/dist/init-wizard.js +38 -19
  90. package/dist/isolation/policy.js +6 -1
  91. package/dist/isolation/types.d.ts +5 -0
  92. package/dist/managed-cli.d.ts +253 -0
  93. package/dist/managed-cli.js +923 -0
  94. package/dist/managed-recovery.d.ts +13 -0
  95. package/dist/managed-recovery.js +31 -0
  96. package/dist/minimal-setup.d.ts +2 -0
  97. package/dist/minimal-setup.js +44 -0
  98. package/dist/monitor.d.ts +9 -34
  99. package/dist/monitor.js +90 -153
  100. package/dist/notifications/fleet-producer.d.ts +22 -0
  101. package/dist/notifications/fleet-producer.js +237 -0
  102. package/dist/notifications/outbox.d.ts +46 -0
  103. package/dist/notifications/outbox.js +158 -0
  104. package/dist/notifications/target-cleanup.d.ts +11 -0
  105. package/dist/notifications/target-cleanup.js +63 -0
  106. package/dist/notifications/task-producer.d.ts +20 -0
  107. package/dist/notifications/task-producer.js +100 -0
  108. package/dist/ops.d.ts +2 -0
  109. package/dist/ops.js +20 -3
  110. package/dist/owner-channel/channel.js +17 -6
  111. package/dist/owner-channel/commands.d.ts +4 -0
  112. package/dist/owner-channel/commands.js +8 -0
  113. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  114. package/dist/owner-channel/ours-client.js +2 -17
  115. package/dist/paths.d.ts +2 -0
  116. package/dist/paths.js +2 -0
  117. package/dist/preset-migration.js +43 -0
  118. package/dist/process-incarnation.d.ts +23 -0
  119. package/dist/process-incarnation.js +42 -0
  120. package/dist/reminders/manager.d.ts +75 -0
  121. package/dist/reminders/manager.js +278 -0
  122. package/dist/reminders/runtime.d.ts +27 -0
  123. package/dist/reminders/runtime.js +32 -0
  124. package/dist/reminders/schedule.d.ts +37 -0
  125. package/dist/reminders/schedule.js +104 -0
  126. package/dist/reminders/store.d.ts +341 -0
  127. package/dist/reminders/store.js +762 -0
  128. package/dist/reminders/tools.d.ts +20 -0
  129. package/dist/reminders/tools.js +114 -0
  130. package/dist/resolved-plan.js +1 -0
  131. package/dist/rooms-tasks/add-member.d.ts +48 -0
  132. package/dist/rooms-tasks/add-member.js +84 -0
  133. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  134. package/dist/rooms-tasks/archived-absence.js +48 -0
  135. package/dist/rooms-tasks/cli.js +65 -5
  136. package/dist/rooms-tasks/close.d.ts +6 -2
  137. package/dist/rooms-tasks/close.js +246 -37
  138. package/dist/rooms-tasks/config.d.ts +1 -1
  139. package/dist/rooms-tasks/config.js +11 -4
  140. package/dist/rooms-tasks/cowork-adapter.d.ts +12 -5
  141. package/dist/rooms-tasks/cowork-adapter.js +15 -121
  142. package/dist/rooms-tasks/cowork-http.js +56 -6
  143. package/dist/rooms-tasks/deletion.d.ts +1 -1
  144. package/dist/rooms-tasks/deletion.js +84 -5
  145. package/dist/rooms-tasks/erasure.d.ts +6 -0
  146. package/dist/rooms-tasks/erasure.js +173 -0
  147. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  148. package/dist/rooms-tasks/external-worker.js +3 -4
  149. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  150. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  151. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  152. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  153. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  154. package/dist/rooms-tasks/layout-cli.js +107 -0
  155. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  156. package/dist/rooms-tasks/layout-config.js +89 -0
  157. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  158. package/dist/rooms-tasks/layout-control.js +52 -0
  159. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  160. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  161. package/dist/rooms-tasks/layout-service.d.ts +43 -0
  162. package/dist/rooms-tasks/layout-service.js +203 -0
  163. package/dist/rooms-tasks/layout.d.ts +121 -0
  164. package/dist/rooms-tasks/layout.js +286 -0
  165. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  166. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  167. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  168. package/dist/rooms-tasks/live-readiness.js +85 -0
  169. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  170. package/dist/rooms-tasks/member-ownership.js +40 -0
  171. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  172. package/dist/rooms-tasks/member-startup.js +2 -0
  173. package/dist/rooms-tasks/provision.d.ts +3 -0
  174. package/dist/rooms-tasks/provision.js +101 -49
  175. package/dist/rooms-tasks/recovery.d.ts +19 -0
  176. package/dist/rooms-tasks/recovery.js +110 -0
  177. package/dist/rooms-tasks/room-state.d.ts +4 -2
  178. package/dist/rooms-tasks/room-state.js +47 -8
  179. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  180. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  181. package/dist/rooms-tasks/task-state.d.ts +52 -8
  182. package/dist/rooms-tasks/task-state.js +251 -26
  183. package/dist/rooms-tasks/types.d.ts +40 -0
  184. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  185. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  186. package/dist/rooms-tasks/workspace.d.ts +35 -0
  187. package/dist/rooms-tasks/workspace.js +325 -0
  188. package/dist/runner.d.ts +33 -4
  189. package/dist/runner.js +410 -62
  190. package/dist/session/acp.d.ts +75 -2
  191. package/dist/session/acp.js +345 -82
  192. package/dist/session/arbiter.d.ts +98 -0
  193. package/dist/session/arbiter.js +272 -3
  194. package/dist/session/codex-app-server.d.ts +45 -1
  195. package/dist/session/codex-app-server.js +225 -32
  196. package/dist/session/control.d.ts +17 -3
  197. package/dist/session/control.js +213 -23
  198. package/dist/session/conversation-normalizer.d.ts +2 -1
  199. package/dist/session/conversation-normalizer.js +1 -1
  200. package/dist/session/conversation-observation.d.ts +25 -0
  201. package/dist/session/conversation-observation.js +100 -0
  202. package/dist/session/conversation-replay.d.ts +50 -0
  203. package/dist/session/conversation-replay.js +203 -0
  204. package/dist/session/conversation-store.d.ts +15 -1
  205. package/dist/session/conversation-store.js +206 -26
  206. package/dist/session/conversation-types.d.ts +57 -4
  207. package/dist/session/prompt-batch.d.ts +79 -0
  208. package/dist/session/prompt-batch.js +120 -0
  209. package/dist/session/task-notice.d.ts +14 -0
  210. package/dist/session/task-notice.js +13 -0
  211. package/dist/session/types.d.ts +42 -1
  212. package/dist/setup-tunnel-preflight.d.ts +34 -0
  213. package/dist/setup-tunnel-preflight.js +62 -0
  214. package/dist/socket-path.d.ts +2 -0
  215. package/dist/socket-path.js +22 -0
  216. package/dist/spawn.d.ts +5 -1
  217. package/dist/spawn.js +40 -4
  218. package/dist/startup-prompt.d.ts +15 -0
  219. package/dist/startup-prompt.js +71 -0
  220. package/dist/startup-readiness.d.ts +32 -0
  221. package/dist/startup-readiness.js +101 -0
  222. package/dist/startup-telemetry.d.ts +16 -0
  223. package/dist/startup-telemetry.js +46 -0
  224. package/dist/subscriptions/cli.d.ts +91 -0
  225. package/dist/subscriptions/cli.js +223 -0
  226. package/dist/subscriptions/launch.d.ts +37 -0
  227. package/dist/subscriptions/launch.js +93 -0
  228. package/dist/subscriptions/login.d.ts +77 -0
  229. package/dist/subscriptions/login.js +215 -0
  230. package/dist/subscriptions/observed.d.ts +7 -0
  231. package/dist/subscriptions/observed.js +44 -0
  232. package/dist/subscriptions/probe.d.ts +10 -0
  233. package/dist/subscriptions/probe.js +73 -0
  234. package/dist/subscriptions/service.d.ts +102 -0
  235. package/dist/subscriptions/service.js +364 -0
  236. package/dist/subscriptions/store.d.ts +71 -0
  237. package/dist/subscriptions/store.js +143 -0
  238. package/dist/supervisor/adoption.d.ts +8 -0
  239. package/dist/supervisor/adoption.js +78 -0
  240. package/dist/supervisor/catalog.d.ts +62 -0
  241. package/dist/supervisor/catalog.js +254 -0
  242. package/dist/supervisor/fleet.d.ts +8 -0
  243. package/dist/supervisor/fleet.js +89 -0
  244. package/dist/supervisor/index.js +3 -6
  245. package/dist/supervisor/launchd.d.ts +1 -1
  246. package/dist/supervisor/launchd.js +26 -13
  247. package/dist/supervisor/legacy.d.ts +35 -0
  248. package/dist/supervisor/legacy.js +524 -0
  249. package/dist/supervisor/manager.d.ts +15 -0
  250. package/dist/supervisor/manager.js +270 -0
  251. package/dist/supervisor/registration-refresh.d.ts +45 -0
  252. package/dist/supervisor/registration-refresh.js +266 -0
  253. package/dist/supervisor/scope.d.ts +4 -0
  254. package/dist/supervisor/scope.js +10 -0
  255. package/dist/supervisor/systemd.d.ts +2 -2
  256. package/dist/supervisor/systemd.js +19 -33
  257. package/dist/task-supervision.d.ts +9 -0
  258. package/dist/task-supervision.js +145 -0
  259. package/dist/temp-idle.d.ts +17 -0
  260. package/dist/temp-idle.js +42 -0
  261. package/dist/temp-lifecycle.d.ts +23 -8
  262. package/dist/temp-lifecycle.js +123 -66
  263. package/dist/temp-supervisor-recovery.d.ts +1 -1
  264. package/dist/temp-supervisor-recovery.js +2 -2
  265. package/dist/tunnel-setup-completion.d.ts +7 -0
  266. package/dist/tunnel-setup-completion.js +13 -0
  267. package/dist/tunnel-setup-input.d.ts +19 -0
  268. package/dist/tunnel-setup-input.js +124 -0
  269. package/dist/watchdog/query.d.ts +3 -1
  270. package/dist/watchdog/query.js +2 -1
  271. package/dist/web/agent-attachments.d.ts +29 -0
  272. package/dist/web/agent-attachments.js +162 -0
  273. package/dist/web/auth.d.ts +65 -2
  274. package/dist/web/auth.js +201 -11
  275. package/dist/web/available-port.d.ts +2 -0
  276. package/dist/web/available-port.js +23 -0
  277. package/dist/web/control.d.ts +3 -3
  278. package/dist/web/control.js +10 -8
  279. package/dist/web/conversation-stream-codec.d.ts +23 -0
  280. package/dist/web/conversation-stream-codec.js +110 -0
  281. package/dist/web/conversation-stream.d.ts +25 -0
  282. package/dist/web/conversation-stream.js +231 -0
  283. package/dist/web/events.d.ts +2 -0
  284. package/dist/web/events.js +9 -0
  285. package/dist/web/fleet-config-service.d.ts +4 -1
  286. package/dist/web/fleet-config-service.js +46 -15
  287. package/dist/web/prefix-gateway.d.ts +32 -0
  288. package/dist/web/prefix-gateway.js +248 -0
  289. package/dist/web/resource-views.d.ts +124 -0
  290. package/dist/web/resource-views.js +424 -0
  291. package/dist/web/runtime.d.ts +3 -1
  292. package/dist/web/runtime.js +132 -34
  293. package/dist/web/server.d.ts +19 -0
  294. package/dist/web/server.js +802 -67
  295. package/dist/web/service.d.ts +11 -2
  296. package/dist/web/service.js +30 -4
  297. package/dist/web/workspace-devices.d.ts +107 -0
  298. package/dist/web/workspace-devices.js +277 -0
  299. package/dist/web/workspace-tunnel.d.ts +1 -0
  300. package/dist/web/workspace-tunnel.js +34 -0
  301. package/dist/workspace-enrollment.d.ts +83 -0
  302. package/dist/workspace-enrollment.js +580 -0
  303. package/dist/workspace-replacement.d.ts +66 -0
  304. package/dist/workspace-replacement.js +220 -0
  305. package/docs/acp-files.md +146 -0
  306. package/docs/conversation-stream.md +196 -0
  307. package/docs/fleet-migration.md +54 -0
  308. package/docs/live-voice.md +13 -0
  309. package/docs/room-layouts.md +227 -0
  310. package/docs/self-reminders.md +374 -0
  311. package/docs/task-workspaces.md +225 -0
  312. package/docs/validation/managed-cli-permissions.md +421 -0
  313. package/examples/room-layouts/fresh.yaml +27 -0
  314. package/examples/room-layouts/scoped.yaml +21 -0
  315. package/examples/room-layouts/shared.yaml +19 -0
  316. package/package.json +24 -20
  317. package/presets/README.md +78 -0
  318. package/presets/brain-catalog.json +8 -2
  319. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  320. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  321. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  322. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -2
  323. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  324. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  325. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  326. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  327. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  328. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  329. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  330. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  331. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  332. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  333. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  334. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  335. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  336. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  337. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  338. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  339. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  340. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  341. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  342. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  343. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  344. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  345. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  346. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  347. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  348. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  349. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  350. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  351. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  352. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  353. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  354. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  355. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  356. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  357. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  358. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  359. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  360. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  361. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  362. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  363. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  364. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  365. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  366. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  367. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  368. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  369. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  370. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  371. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  372. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  373. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  374. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  375. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  376. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  377. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  378. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  379. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  380. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  381. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  382. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  383. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  384. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  385. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  386. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  387. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  388. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  389. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  390. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  391. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  392. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  393. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  394. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  395. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  396. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  397. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  398. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  399. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  400. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  401. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  402. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  403. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  404. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  405. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  406. package/presets/fleet/roles/Coordinator.yaml +12 -11
  407. package/presets/fleet/roles/Critic.yaml +13 -0
  408. package/presets/fleet/roles/Developer.yaml +13 -0
  409. package/presets/fleet/roles/Engineer.yaml +53 -0
  410. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  411. package/presets/fleet/room_layouts/pair.yaml +24 -0
  412. package/presets/fleet/room_layouts/single.yaml +17 -0
  413. package/presets/fleet/room_layouts/team.yaml +30 -0
  414. package/presets/fleet/room_templates/engineering.yaml +19 -0
  415. package/presets/manifest.json +2 -2
  416. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  417. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  418. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  419. package/dist/web-app/icons/ours-fleet.svg +0 -4
  420. package/dist/web-app/index.html +0 -17
  421. package/dist/web-app/manifest.webmanifest +0 -15
  422. package/dist/web-app/offline.html +0 -18
  423. package/dist/web-app/sw.js +0 -51
@@ -0,0 +1,162 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { FleetError } from '../application/errors.js';
5
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
6
+ export const MAX_ATTACHMENT_BODY = Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 + 4096;
7
+ function fail(message) { throw new FleetError('invalid_request', message); }
8
+ const digest = (data) => createHash('sha256').update(data).digest('hex');
9
+ function directory(path, create = false, privateStorage = true) {
10
+ if (create && !existsSync(path))
11
+ mkdirSync(path, { mode: 0o700 });
12
+ const stat = lstatSync(path);
13
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & (privateStorage ? 0o022 : 0o002)))
14
+ fail('Attachment directory is not a private owned directory');
15
+ }
16
+ function read(path, max) {
17
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
18
+ try {
19
+ const stat = fstatSync(fd);
20
+ if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.size > max)
21
+ fail('Attachment is not a bounded owned regular file');
22
+ return readFileSync(fd);
23
+ }
24
+ finally {
25
+ closeSync(fd);
26
+ }
27
+ }
28
+ function immutable(path, bytes) {
29
+ try {
30
+ writeFileSync(path, bytes, { flag: 'wx', mode: 0o400 });
31
+ }
32
+ catch (error) {
33
+ if (error.code !== 'EEXIST')
34
+ throw error;
35
+ if (!read(path, bytes.length).equals(bytes))
36
+ fail('Attachment content changed');
37
+ }
38
+ }
39
+ function filename(name) {
40
+ const chars = Array.from(name.replace(/[^\p{L}\p{N}._-]/gu, '_').replace(/^\.+/, '_'));
41
+ while (Buffer.byteLength(chars.join('')) > 120)
42
+ chars.pop();
43
+ return chars.join('') || 'file';
44
+ }
45
+ export function storeAgentAttachment(stateDir, generation, input) {
46
+ if (!input || typeof input !== 'object')
47
+ fail('File data is required');
48
+ const body = input;
49
+ if (typeof body.name !== 'string' || !body.name.trim() || body.name.length > 240 || /[\x00-\x1f]/u.test(body.name))
50
+ fail('A file name of 1–240 characters is required');
51
+ if (typeof body.mimeType !== 'string' || !/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(body.mimeType))
52
+ fail('A valid media type is required');
53
+ if (typeof body.data !== 'string' || body.data.length > Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 || /[^A-Za-z0-9+/=]/.test(body.data))
54
+ fail('Invalid base64 file data or file exceeds 20 MiB');
55
+ const bytes = Buffer.from(body.data, 'base64');
56
+ if (bytes.length > MAX_ATTACHMENT_BYTES || bytes.toString('base64') !== body.data)
57
+ fail('Invalid file data or file exceeds 20 MiB');
58
+ const sha256 = digest(bytes), name = body.name, mimeType = body.mimeType;
59
+ const id = digest(JSON.stringify({ generation, name, mimeType, sha256 }));
60
+ const record = { id, name, mimeType, size: bytes.length, sha256, generation };
61
+ // Fleet runtime state directories may use the account group (0775). Upload storage remains private.
62
+ directory(stateDir, false, false);
63
+ const root = join(stateDir, 'web-attachments');
64
+ directory(root, true);
65
+ const dir = join(root, id);
66
+ directory(dir, true);
67
+ immutable(join(dir, 'content-' + filename(name)), bytes);
68
+ immutable(join(dir, 'metadata.json'), Buffer.from(JSON.stringify(record)));
69
+ return record;
70
+ }
71
+ export function attachmentPrompt(stateDir, generation, ids, text) {
72
+ if (!Array.isArray(ids) || ids.length < 1 || ids.length > 5 || new Set(ids).size !== ids.length)
73
+ fail('Select between 1 and 5 distinct attachments');
74
+ // Fleet runtime state directories may use the account group (0775). Upload storage remains private.
75
+ directory(stateDir, false, false);
76
+ const root = join(stateDir, 'web-attachments');
77
+ directory(root);
78
+ const files = ids.map(id => {
79
+ if (typeof id !== 'string' || !/^[a-f0-9]{64}$/.test(id))
80
+ fail('Invalid attachment ID');
81
+ const dir = join(root, id);
82
+ directory(dir);
83
+ const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
84
+ if (record.id !== id || record.generation !== generation)
85
+ throw new FleetError('stale_state', 'Attachment belongs to a different agent session');
86
+ const file = join(dir, 'content-' + filename(record.name)), bytes = read(file, MAX_ATTACHMENT_BYTES);
87
+ if (bytes.length !== record.size || digest(bytes) !== record.sha256 || digest(JSON.stringify({ generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
88
+ fail('Attachment content changed');
89
+ return { name: record.name, mimeType: record.mimeType, size: record.size, sha256: record.sha256, path: file };
90
+ });
91
+ return `${text}${text ? '\n\n' : ''}Attached files (available on this host; open the files to inspect their contents, and use the image viewer for images):\n${JSON.stringify(files, null, 2)}`;
92
+ }
93
+ /** Authenticated content access reuses the same ownership and integrity checks as admission. */
94
+ function attachmentMetadata(stateDir, id) {
95
+ if (!/^[a-f0-9]{64}$/.test(id))
96
+ fail('Invalid attachment ID');
97
+ directory(stateDir, false, false);
98
+ const root = join(stateDir, 'web-attachments');
99
+ directory(root);
100
+ const dir = join(root, id);
101
+ directory(dir);
102
+ const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
103
+ if (record.id !== id || typeof record.name !== 'string' || typeof record.mimeType !== 'string')
104
+ fail('Invalid attachment metadata');
105
+ if (digest(JSON.stringify({ generation: record.generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
106
+ fail('Attachment metadata changed');
107
+ return record;
108
+ }
109
+ export function readAgentAttachment(stateDir, id) {
110
+ let record;
111
+ try {
112
+ record = attachmentMetadata(stateDir, id);
113
+ }
114
+ catch (error) {
115
+ if (error.code === 'ENOENT')
116
+ throw new FleetError('resource_not_found', 'Attachment not found');
117
+ throw error;
118
+ }
119
+ const bytes = read(join(stateDir, 'web-attachments', id, 'content-' + filename(record.name)), MAX_ATTACHMENT_BYTES);
120
+ if (bytes.length !== record.size || digest(bytes) !== record.sha256)
121
+ fail('Attachment content changed');
122
+ return { record, bytes };
123
+ }
124
+ /** Written before admission so a lost HTTP response cannot lose the visible message. */
125
+ export function storeAttachmentPresentation(stateDir, value) {
126
+ directory(stateDir, false, false);
127
+ const root = join(stateDir, 'web-attachment-messages');
128
+ directory(root, true);
129
+ immutable(join(root, digest(JSON.stringify([value.generation, value.commandId])) + '.json'), Buffer.from(JSON.stringify(value)));
130
+ }
131
+ export function prepareAttachmentPresentation(stateDir, generation, commandId, ids, text, transport) {
132
+ storeAttachmentPresentation(stateDir, { generation, commandId, ids, text, transportDigest: digest(transport) });
133
+ }
134
+ /** Never infer attachments by stripping user text or interpreting a marker. */
135
+ export function presentAttachmentEvent(stateDir, roleId, event) {
136
+ const e = event;
137
+ if (!stateDir || e.kind !== 'prompt.admitted' || e.source !== 'owner_admin_console' || !e.commandId || e.payload?.text?.redacted)
138
+ return event;
139
+ const root = join(stateDir, 'web-attachment-messages');
140
+ if (!existsSync(root))
141
+ return event;
142
+ const path = join(root, digest(JSON.stringify([e.sessionGeneration, e.commandId])) + '.json');
143
+ if (!existsSync(path))
144
+ return event;
145
+ try {
146
+ directory(stateDir, false, false);
147
+ directory(root);
148
+ const value = JSON.parse(read(path, 1024 * 1024).toString());
149
+ if (value.generation !== e.sessionGeneration || value.commandId !== e.commandId || value.transportDigest !== digest(e.payload.text.text))
150
+ return event;
151
+ const attachments = value.ids.map(id => {
152
+ const record = attachmentMetadata(stateDir, id);
153
+ if (record.generation !== value.generation)
154
+ fail('Attachment session changed');
155
+ return { id, name: record.name, mimeType: record.mimeType, size: record.size, url: `/api/v1/roles/${encodeURIComponent(roleId)}/attachments/${id}` };
156
+ });
157
+ return { ...e, payload: { ...e.payload, text: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, displayText: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, attachments } };
158
+ }
159
+ catch {
160
+ return event;
161
+ }
162
+ }
@@ -1,3 +1,4 @@
1
+ import { WorkspaceDeviceStore } from './workspace-devices.js';
1
2
  import type { FastifyRequest } from 'fastify';
2
3
  import type { WebSocket } from 'ws';
3
4
  import { TrustedDeviceStore, type TrustedDeviceIssue } from './device-store.js';
@@ -8,6 +9,8 @@ export interface BrowserSession {
8
9
  createdAt: number;
9
10
  lastSeenAt: number;
10
11
  absoluteExpiresAt: number;
12
+ /** A workspace access credential, which cannot create further device links. */
13
+ rotating?: boolean;
11
14
  }
12
15
  export interface AuthResult {
13
16
  session: BrowserSession;
@@ -19,6 +22,8 @@ interface Ticket {
19
22
  purpose: 'events' | 'conversation';
20
23
  roleId?: string;
21
24
  expiresAt: number;
25
+ /** When the credential that minted this ticket stops authorizing; a socket opened with it ends then. */
26
+ authorizedUntil: number;
22
27
  }
23
28
  export declare class WebAuth {
24
29
  private _origin;
@@ -26,6 +31,8 @@ export declare class WebAuth {
26
31
  private readonly now;
27
32
  private readonly devices;
28
33
  private readonly access;
34
+ private readonly workspaceDevices?;
35
+ readonly appOrigin: string;
29
36
  private _bootstrapSecret;
30
37
  private bootstrapExpiresAt;
31
38
  private bootstrapUsed;
@@ -33,8 +40,10 @@ export declare class WebAuth {
33
40
  private readonly sessionDevices;
34
41
  private readonly tickets;
35
42
  private readonly rates;
43
+ private readonly transports;
44
+ private readonly renewals;
36
45
  private readonly sockets;
37
- constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig);
46
+ constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig, workspaceDevices?: WorkspaceDeviceStore | undefined, appOrigin?: string);
38
47
  get bootstrapSecret(): string;
39
48
  get origin(): string;
40
49
  get host(): string;
@@ -48,21 +57,75 @@ export declare class WebAuth {
48
57
  }): void;
49
58
  /** Mint a replacement for an operator-triggered reauthentication ceremony. */
50
59
  mintBootstrap(): string;
60
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
61
+ accountOrigin(request: Pick<FastifyRequest, 'headers'>): string | undefined;
51
62
  validateBoundary(request: FastifyRequest, requireOrigin: boolean): void;
52
63
  exchange(request: FastifyRequest): AuthResult;
53
64
  login(request: FastifyRequest, password: string): AuthResult;
54
65
  anonymous(request: FastifyRequest): BrowserSession;
55
66
  resume(request: FastifyRequest): AuthResult;
56
67
  authenticate(request: FastifyRequest, mutation?: boolean): BrowserSession;
68
+ /** Check a transport's opening lease without refreshing cookie idle activity. */
69
+ sessionValid(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>): boolean;
57
70
  logout(request: FastifyRequest): void;
58
71
  mintTicket(request: FastifyRequest, purpose: Ticket['purpose'], roleId?: string): {
59
72
  ticket: string;
60
73
  expiresAt: string;
61
74
  };
62
75
  consumeTicket(request: FastifyRequest, value: string, purpose: Ticket['purpose'], roleId?: string): BrowserSession;
63
- bindSocket(sessionId: string, socket: WebSocket): void;
76
+ /**
77
+ * `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
78
+ * device shares one session id across credentials, so the socket ends at that credential's own
79
+ * deadline; revocation still ends everything the device has open.
80
+ */
81
+ bindSocket(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, socket: WebSocket): void;
82
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
83
+ bindTransport(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, close: () => void, renewal?: string): () => void;
84
+ /**
85
+ * Keep one open transport past the credential that opened it. Only its own reader knows the
86
+ * name it was given, and it must authenticate with a newer credential of the same device while
87
+ * the transport is still open: a copied credential cannot renew after it has expired.
88
+ */
89
+ renewTransport(request: FastifyRequest, renewal: string): {
90
+ authorizedUntil: number;
91
+ };
64
92
  clearSessions(): void;
65
93
  revokeAllTrustedDevices(): number;
94
+ workspaceIdentity(): {
95
+ workspaceId: string;
96
+ };
97
+ workspaceEnrollment(issuer?: string | null): {
98
+ enrollment: string;
99
+ workspaceId: string;
100
+ expiresAt: number;
101
+ };
102
+ enrollWorkspace(request: FastifyRequest, input: {
103
+ enrollment: string;
104
+ workspaceId: string;
105
+ label: string;
106
+ credential?: unknown;
107
+ }): import("./workspace-devices.js").WorkspaceCredentials | {
108
+ token: string;
109
+ device: import("./workspace-devices.js").WorkspaceDevice;
110
+ workspaceId: string;
111
+ };
112
+ /**
113
+ * Refresh credentials are exchanged between the account server and this host only. A browser
114
+ * always states where a request comes from, so a request that does is refused: no page can be
115
+ * handed refresh material, whichever origin it runs on.
116
+ */
117
+ private machineOnly;
118
+ refreshWorkspace(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials;
119
+ exchangeWorkspace(request: FastifyRequest): import("./workspace-devices.js").WorkspaceCredentials;
120
+ revokeWorkspaceRefresh(request: FastifyRequest, refreshHash: string): boolean;
121
+ /** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
122
+ linkWorkspaceDevice(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials & {
123
+ enrollment: string;
124
+ enrollmentExpiresAt: number;
125
+ };
126
+ listWorkspaceDevices(): import("./workspace-devices.js").WorkspaceDevice[];
127
+ revokeWorkspaceDevice(id: string): void;
128
+ private requireWorkspaceDevices;
66
129
  shutdown(): void;
67
130
  private createSession;
68
131
  private removeSession;
package/dist/web/auth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { WorkspaceDeviceAuthError, WorkspaceRefreshReuseError } from './workspace-devices.js';
1
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
2
3
  import { FleetError } from '../application/errors.js';
3
4
  import { TrustedDeviceStore } from './device-store.js';
@@ -14,6 +15,8 @@ export class WebAuth {
14
15
  now;
15
16
  devices;
16
17
  access;
18
+ workspaceDevices;
19
+ appOrigin;
17
20
  _bootstrapSecret = token(32);
18
21
  bootstrapExpiresAt = Date.now() + 5 * 60_000;
19
22
  bootstrapUsed = false;
@@ -21,13 +24,17 @@ export class WebAuth {
21
24
  sessionDevices = new Map();
22
25
  tickets = new Map();
23
26
  rates = new Map();
27
+ transports = new Map();
28
+ renewals = new Map();
24
29
  sockets = new Map();
25
- constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }) {
30
+ constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }, workspaceDevices, appOrigin = 'https://app.ours.network') {
26
31
  this._origin = _origin;
27
32
  this._host = _host;
28
33
  this.now = now;
29
34
  this.devices = devices;
30
35
  this.access = access;
36
+ this.workspaceDevices = workspaceDevices;
37
+ this.appOrigin = appOrigin;
31
38
  }
32
39
  get bootstrapSecret() { return this._bootstrapSecret; }
33
40
  get origin() { return this._origin; }
@@ -49,16 +56,24 @@ export class WebAuth {
49
56
  this.bootstrapUsed = false;
50
57
  return this._bootstrapSecret;
51
58
  }
59
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
60
+ accountOrigin(request) {
61
+ return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
62
+ }
52
63
  validateBoundary(request, requireOrigin) {
53
64
  const host = request.headers.host;
54
65
  const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
55
66
  const origins = this.allowedOrigins.size ? this.allowedOrigins : new Set([this.origin]);
56
67
  if (!host || !hosts.has(host))
57
68
  throw new FleetError('forbidden', `This address is not configured for the fleet console. Open ${this.origin} or set --public-origin.`);
58
- if (requireOrigin && (!request.headers.origin || !origins.has(request.headers.origin)))
69
+ if (this.workspaceDevices && String(request.headers.authorization ?? '').startsWith('Bearer ') && request.headers.origin && request.headers.origin !== this.appOrigin && !origins.has(request.headers.origin))
70
+ throw new FleetError('forbidden', 'workspace request Origin does not match configured account origin');
71
+ const workspaceRequest = Boolean(this.workspaceDevices) && request.headers.origin === this.appOrigin
72
+ && (String(request.headers.authorization ?? '').startsWith('Bearer ') || (request.url ?? '').split('?')[0] === '/api/v1/devices/enroll' || request.method === 'OPTIONS');
73
+ if (requireOrigin && !workspaceRequest && (!request.headers.origin || !origins.has(request.headers.origin)))
59
74
  throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
60
75
  const fetchSite = request.headers['sec-fetch-site'];
61
- if (fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
76
+ if (!workspaceRequest && fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
62
77
  throw new FleetError('forbidden', 'cross-site request rejected');
63
78
  }
64
79
  exchange(request) {
@@ -100,11 +115,28 @@ export class WebAuth {
100
115
  return { session: this.createSession(device.id), device };
101
116
  }
102
117
  authenticate(request, mutation = false) {
118
+ const authorization = String(request.headers.authorization ?? '');
119
+ if (authorization.startsWith('Bearer ')) {
120
+ this.validateBoundary(request, mutation);
121
+ if (!this.workspaceDevices)
122
+ throw new WorkspaceDeviceAuthError('workspace bearer credential is invalid');
123
+ const device = this.requireWorkspaceDevices().authenticate(authorization.slice(7));
124
+ const id = `workspace:${device.id}`;
125
+ // A transport opened with this credential ends when this credential does, whatever replaces it.
126
+ const session = { id, csrf: '', createdAt: device.createdAt, lastSeenAt: this.now(), absoluteExpiresAt: device.authorizedUntil, rotating: device.rotating };
127
+ this.sessions.set(id, session);
128
+ this.sessionDevices.set(id, id);
129
+ if (mutation)
130
+ this.consumeRate(`mutation:${id}`, 120, 60000);
131
+ return session;
132
+ }
103
133
  this.validateBoundary(request, mutation);
134
+ if (request.headers.origin === this.appOrigin && !this.allowedOrigins.has(this.appOrigin))
135
+ throw new FleetError('unauthorized', 'workspace bearer credential required');
104
136
  const id = parseCookies(request.headers.cookie ?? '').ofs_session;
105
137
  const session = id ? this.sessions.get(id) : undefined;
106
138
  const now = this.now();
107
- if (!session || now > session.absoluteExpiresAt || now - session.lastSeenAt > 30 * 60_000) {
139
+ if (!session || !this.sessionValid(session)) {
108
140
  if (id)
109
141
  this.removeSession(id);
110
142
  throw new FleetError('unauthorized', 'browser session is missing or expired');
@@ -118,10 +150,21 @@ export class WebAuth {
118
150
  session.lastSeenAt = now;
119
151
  return session;
120
152
  }
153
+ /** Check a transport's opening lease without refreshing cookie idle activity. */
154
+ sessionValid(session) {
155
+ const current = this.sessions.get(session.id), now = this.now();
156
+ if (!current || now >= session.absoluteExpiresAt)
157
+ return false;
158
+ if (session.id.startsWith('workspace:'))
159
+ return Boolean(this.workspaceDevices?.valid(session.id.slice(10)));
160
+ return now < current.absoluteExpiresAt && now - current.lastSeenAt < 30 * 60_000;
161
+ }
121
162
  logout(request) {
122
163
  const session = this.authenticate(request, true);
123
164
  const deviceId = this.sessionDevices.get(session.id);
124
- if (deviceId && deviceId !== 'unprotected')
165
+ if (session.id.startsWith('workspace:'))
166
+ this.workspaceDevices?.revoke(session.id.slice(10));
167
+ else if (deviceId && deviceId !== 'unprotected')
125
168
  this.devices.revokeId(deviceId);
126
169
  this.removeSession(session.id);
127
170
  }
@@ -130,7 +173,8 @@ export class WebAuth {
130
173
  const value = token();
131
174
  const ticket = {
132
175
  value, sessionId: session.id, purpose, roleId,
133
- expiresAt: this.now() + 30_000,
176
+ expiresAt: Math.min(this.now() + 30_000, session.absoluteExpiresAt),
177
+ authorizedUntil: session.absoluteExpiresAt,
134
178
  };
135
179
  this.tickets.set(value, ticket);
136
180
  return { ticket: value, expiresAt: new Date(ticket.expiresAt).toISOString() };
@@ -143,20 +187,82 @@ export class WebAuth {
143
187
  || ticket.roleId !== roleId)
144
188
  throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
145
189
  const session = this.sessions.get(ticket.sessionId);
146
- if (!session)
190
+ if (!session || (session.id.startsWith('workspace:') && !this.workspaceDevices?.valid(session.id.slice(10))))
147
191
  throw new FleetError('unauthorized', 'browser session expired');
148
- return session;
192
+ if (!session.id.startsWith('workspace:'))
193
+ return session;
194
+ // A workspace device's session entry is replaced by each newer credential; the ticket carries
195
+ // the deadline of the credential that minted it, and nothing newer extends it.
196
+ if (ticket.authorizedUntil <= this.now())
197
+ throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
198
+ return { ...session, absoluteExpiresAt: ticket.authorizedUntil };
149
199
  }
150
- bindSocket(sessionId, socket) {
200
+ /**
201
+ * `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
202
+ * device shares one session id across credentials, so the socket ends at that credential's own
203
+ * deadline; revocation still ends everything the device has open.
204
+ */
205
+ bindSocket(session, socket) {
206
+ const sessionId = session.id, deadline = sessionId.startsWith('workspace:') ? session.absoluteExpiresAt : undefined;
151
207
  let sockets = this.sockets.get(sessionId);
152
208
  if (!sockets) {
153
209
  sockets = new Set();
154
210
  this.sockets.set(sessionId, sockets);
155
211
  }
156
212
  sockets.add(socket);
157
- socket.once('close', () => sockets?.delete(socket));
213
+ const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (deadline !== undefined ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
214
+ this.removeSession(sessionId);
215
+ else if (deadline !== undefined && deadline <= this.now()) {
216
+ clearInterval(expiry);
217
+ socket.close(4401, 'authentication expired');
218
+ } }, 1000);
219
+ expiry.unref();
220
+ socket.once('close', () => { clearInterval(expiry); sockets?.delete(socket); });
221
+ }
222
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
223
+ bindTransport(session, close, renewal) {
224
+ const sessionId = session.id, workspace = sessionId.startsWith('workspace:');
225
+ let transports = this.transports.get(sessionId);
226
+ if (!transports) {
227
+ transports = new Set();
228
+ this.transports.set(sessionId, transports);
229
+ }
230
+ transports.add(close);
231
+ const lease = { sessionId, deadline: session.absoluteExpiresAt };
232
+ if (workspace && renewal)
233
+ this.renewals.set(renewal, lease);
234
+ const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (workspace ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
235
+ this.removeSession(sessionId);
236
+ else if (workspace && lease.deadline <= this.now()) {
237
+ clearInterval(expiry);
238
+ transports.delete(close);
239
+ if (renewal)
240
+ this.renewals.delete(renewal);
241
+ close();
242
+ } }, 1000);
243
+ expiry.unref();
244
+ return () => { clearInterval(expiry); transports.delete(close); if (renewal)
245
+ this.renewals.delete(renewal); if (!transports.size)
246
+ this.transports.delete(sessionId); };
247
+ }
248
+ /**
249
+ * Keep one open transport past the credential that opened it. Only its own reader knows the
250
+ * name it was given, and it must authenticate with a newer credential of the same device while
251
+ * the transport is still open: a copied credential cannot renew after it has expired.
252
+ */
253
+ renewTransport(request, renewal) {
254
+ const session = this.authenticate(request, true), lease = this.renewals.get(renewal);
255
+ if (!lease || lease.sessionId !== session.id || lease.deadline <= this.now())
256
+ throw new FleetError('resource_not_found', 'transport is not open');
257
+ lease.deadline = Math.max(lease.deadline, session.absoluteExpiresAt);
258
+ return { authorizedUntil: lease.deadline };
158
259
  }
159
260
  clearSessions() {
261
+ for (const transports of this.transports.values())
262
+ for (const close of transports)
263
+ close();
264
+ this.transports.clear();
265
+ this.renewals.clear();
160
266
  for (const sockets of this.sockets.values())
161
267
  for (const socket of sockets)
162
268
  socket.close(4401, 'authentication revoked');
@@ -167,10 +273,88 @@ export class WebAuth {
167
273
  }
168
274
  revokeAllTrustedDevices() {
169
275
  const count = this.devices.revokeAll();
276
+ for (const device of this.workspaceDevices?.list() ?? [])
277
+ this.workspaceDevices?.revoke(device.id);
170
278
  this.clearSessions();
171
279
  return count;
172
280
  }
173
- shutdown() { this.clearSessions(); }
281
+ workspaceIdentity() { return { workspaceId: this.requireWorkspaceDevices().workspaceId }; }
282
+ workspaceEnrollment(issuer = null) { return this.requireWorkspaceDevices().mint(issuer?.startsWith('workspace:') ? issuer.slice(10) : null); }
283
+ enrollWorkspace(request, input) {
284
+ if (input.credential === 2) {
285
+ this.machineOnly(request);
286
+ this.consumeRate('workspace-enrollment', 30, 60000);
287
+ return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label, true);
288
+ }
289
+ this.validateBoundary(request, true);
290
+ this.consumeRate('workspace-enrollment', 30, 60000);
291
+ return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label);
292
+ }
293
+ /**
294
+ * Refresh credentials are exchanged between the account server and this host only. A browser
295
+ * always states where a request comes from, so a request that does is refused: no page can be
296
+ * handed refresh material, whichever origin it runs on.
297
+ */
298
+ machineOnly(request) {
299
+ this.validateBoundary(request, false);
300
+ if (request.headers.origin !== undefined || Object.keys(request.headers).some(name => name.startsWith('sec-fetch-')) || request.headers.cookie !== undefined)
301
+ throw new FleetError('forbidden', 'this endpoint does not accept browser requests');
302
+ }
303
+ refreshWorkspace(request, refreshToken) {
304
+ this.machineOnly(request);
305
+ this.consumeRate('workspace-refresh', 240, 60000);
306
+ try {
307
+ return this.requireWorkspaceDevices().refresh(refreshToken);
308
+ }
309
+ catch (error) {
310
+ if (error instanceof WorkspaceRefreshReuseError)
311
+ for (const id of error.revoked)
312
+ this.removeSession(`workspace:${id}`);
313
+ throw error;
314
+ }
315
+ }
316
+ exchangeWorkspace(request) {
317
+ this.machineOnly(request);
318
+ this.consumeRate('workspace-enrollment', 30, 60000);
319
+ const authorization = String(request.headers.authorization ?? '');
320
+ const issued = this.requireWorkspaceDevices().exchange(authorization.startsWith('Bearer ') ? authorization.slice(7) : '');
321
+ // Whatever the fixed bearer had open ends with it.
322
+ this.removeSession(`workspace:${issued.device.id}`);
323
+ return issued;
324
+ }
325
+ revokeWorkspaceRefresh(request, refreshHash) {
326
+ this.machineOnly(request);
327
+ this.consumeRate('workspace-revoke', 120, 60000);
328
+ const id = this.requireWorkspaceDevices().revokeByRefreshHash(refreshHash);
329
+ if (id)
330
+ this.removeSession(`workspace:${id}`);
331
+ return Boolean(id);
332
+ }
333
+ /** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
334
+ linkWorkspaceDevice(request, refreshToken) {
335
+ this.machineOnly(request);
336
+ this.consumeRate('workspace-link', 10, 60000);
337
+ try {
338
+ return this.requireWorkspaceDevices().link(refreshToken);
339
+ }
340
+ catch (error) {
341
+ if (error instanceof WorkspaceRefreshReuseError)
342
+ for (const id of error.revoked)
343
+ this.removeSession(`workspace:${id}`);
344
+ throw error;
345
+ }
346
+ }
347
+ listWorkspaceDevices() { return this.requireWorkspaceDevices().list(); }
348
+ revokeWorkspaceDevice(id) {
349
+ this.requireWorkspaceDevices().revoke(id);
350
+ this.removeSession(`workspace:${id}`);
351
+ }
352
+ requireWorkspaceDevices() {
353
+ if (!this.workspaceDevices)
354
+ throw new FleetError('capability_unavailable', 'workspace device access is not configured');
355
+ return this.workspaceDevices;
356
+ }
357
+ shutdown() { this.clearSessions(); this.workspaceDevices?.close(); }
174
358
  createSession(deviceId) {
175
359
  const now = this.now();
176
360
  const session = {
@@ -182,6 +366,12 @@ export class WebAuth {
182
366
  return session;
183
367
  }
184
368
  removeSession(id) {
369
+ for (const close of this.transports.get(id) ?? [])
370
+ close();
371
+ this.transports.delete(id);
372
+ for (const [renewal, lease] of this.renewals)
373
+ if (lease.sessionId === id)
374
+ this.renewals.delete(renewal);
185
375
  this.sessions.delete(id);
186
376
  this.sessionDevices.delete(id);
187
377
  for (const [ticket, value] of this.tickets)
@@ -0,0 +1,2 @@
1
+ /** Bind the preferred loopback port; only EADDRINUSE permits an OS-selected replacement. */
2
+ export declare function availableWebPort(preferred: number): Promise<number>;
@@ -0,0 +1,23 @@
1
+ import { createServer } from 'node:net';
2
+ /** Bind the preferred loopback port; only EADDRINUSE permits an OS-selected replacement. */
3
+ export async function availableWebPort(preferred) {
4
+ const server = createServer();
5
+ const listen = (port) => new Promise((resolve, reject) => {
6
+ server.once('error', reject);
7
+ server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); });
8
+ });
9
+ try {
10
+ await listen(preferred);
11
+ }
12
+ catch (error) {
13
+ if (error.code !== 'EADDRINUSE')
14
+ throw error;
15
+ await listen(0);
16
+ }
17
+ const address = server.address();
18
+ if (!address || typeof address === 'string')
19
+ throw Error('No loopback port selected');
20
+ const port = address.port;
21
+ await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
22
+ return port;
23
+ }
@@ -1,4 +1,4 @@
1
- export type WebControlCommand = 'open' | 'revoke-all';
1
+ export type WebControlCommand = 'revoke-all' | 'link-device';
2
2
  export declare const webControlPath: (dir?: string) => string;
3
3
  export interface WebControlServer {
4
4
  path: string;
@@ -6,9 +6,9 @@ export interface WebControlServer {
6
6
  }
7
7
  export declare function startWebControlServer(options: {
8
8
  dir?: string;
9
- onOpen(): void | Promise<void>;
9
+ onLinkDevice?(): Record<string, unknown> | Promise<Record<string, unknown>>;
10
10
  onRevokeAll(): void | Promise<void>;
11
11
  now?: () => number;
12
12
  rateLimit?: number;
13
13
  }): Promise<WebControlServer>;
14
- export declare function requestWebControl(command: WebControlCommand, path?: string, timeoutMs?: number): Promise<void>;
14
+ export declare function requestWebControl(command: WebControlCommand, path?: string, timeoutMs?: number): Promise<Record<string, unknown> | void>;