@ours.network/fleet 1.2.0-nightly.6 → 1.2.0-nightly.61

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 (389) hide show
  1. package/README.md +287 -65
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.js +35 -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 +4 -0
  11. package/dist/agent-ours/mcp-endpoint.js +5 -0
  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 +21 -1
  15. package/dist/agent-ours/service.js +94 -14
  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 +27 -0
  19. package/dist/application/conversation-history.js +212 -0
  20. package/dist/application/fleet-query-service.d.ts +1 -1
  21. package/dist/application/fleet-query-service.js +2 -2
  22. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  23. package/dist/application/legacy-supervisor-identity.js +43 -0
  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/role-creation-service.js +3 -2
  30. package/dist/application/role-removal-service.js +9 -1
  31. package/dist/application/role-repository.js +19 -2
  32. package/dist/application/room-layout-definitions.d.ts +45 -0
  33. package/dist/application/room-layout-definitions.js +130 -0
  34. package/dist/application/session-control.d.ts +30 -0
  35. package/dist/application/session-control.js +72 -3
  36. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  37. package/dist/application/supervisor-ours-tools.js +115 -0
  38. package/dist/application/task-layouts.d.ts +91 -0
  39. package/dist/application/task-layouts.js +259 -0
  40. package/dist/application/task-room-service.d.ts +99 -3
  41. package/dist/application/task-room-service.js +285 -32
  42. package/dist/application/types.d.ts +2 -0
  43. package/dist/atomic-file.d.ts +5 -0
  44. package/dist/atomic-file.js +62 -1
  45. package/dist/briefing.d.ts +3 -1
  46. package/dist/briefing.js +70 -30
  47. package/dist/build-info.json +7 -4
  48. package/dist/capabilities.d.ts +6 -1
  49. package/dist/capabilities.js +8 -0
  50. package/dist/cli.js +407 -37
  51. package/dist/client-profile.d.ts +5 -17
  52. package/dist/client-profile.js +4 -127
  53. package/dist/config.d.ts +45 -0
  54. package/dist/config.js +57 -6
  55. package/dist/creation.d.ts +1 -1
  56. package/dist/creation.js +4 -4
  57. package/dist/daemon-recovery.d.ts +1 -8
  58. package/dist/daemon-recovery.js +8 -113
  59. package/dist/docs.d.ts +1 -1
  60. package/dist/docs.js +123 -19
  61. package/dist/doctor.d.ts +1 -1
  62. package/dist/doctor.js +55 -60
  63. package/dist/erased-resources.d.ts +9 -0
  64. package/dist/erased-resources.js +87 -0
  65. package/dist/file-delivery/reader.d.ts +15 -0
  66. package/dist/file-delivery/reader.js +63 -0
  67. package/dist/file-delivery/store.d.ts +15 -0
  68. package/dist/file-delivery/store.js +124 -0
  69. package/dist/file-delivery/types.d.ts +19 -0
  70. package/dist/file-delivery/types.js +1 -0
  71. package/dist/fleet-command-audit.d.ts +7 -0
  72. package/dist/fleet-command-audit.js +26 -9
  73. package/dist/harness/agent-session.d.ts +2 -0
  74. package/dist/harness/claude-code-session.js +1 -1
  75. package/dist/harness/claude-code.js +20 -11
  76. package/dist/harness/claude-runtime.d.ts +12 -0
  77. package/dist/harness/claude-runtime.js +43 -0
  78. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  79. package/dist/harness/codex-app-server-proxy.js +41 -5
  80. package/dist/harness/codex-reasoning.d.ts +5 -0
  81. package/dist/harness/codex-reasoning.js +12 -0
  82. package/dist/harness/codex-runtime.d.ts +11 -1
  83. package/dist/harness/codex-runtime.js +31 -4
  84. package/dist/harness/codex-session.js +2 -2
  85. package/dist/harness/codex.js +23 -12
  86. package/dist/init-wizard.d.ts +2 -0
  87. package/dist/init-wizard.js +38 -19
  88. package/dist/isolation/policy.js +6 -1
  89. package/dist/isolation/types.d.ts +5 -0
  90. package/dist/managed-cli.d.ts +253 -0
  91. package/dist/managed-cli.js +923 -0
  92. package/dist/managed-recovery.d.ts +13 -0
  93. package/dist/managed-recovery.js +31 -0
  94. package/dist/minimal-setup.d.ts +2 -0
  95. package/dist/minimal-setup.js +44 -0
  96. package/dist/monitor.d.ts +8 -34
  97. package/dist/monitor.js +89 -152
  98. package/dist/notifications/fleet-producer.d.ts +19 -0
  99. package/dist/notifications/fleet-producer.js +208 -0
  100. package/dist/notifications/outbox.d.ts +46 -0
  101. package/dist/notifications/outbox.js +158 -0
  102. package/dist/notifications/target-cleanup.d.ts +8 -0
  103. package/dist/notifications/target-cleanup.js +41 -0
  104. package/dist/notifications/task-producer.d.ts +18 -0
  105. package/dist/notifications/task-producer.js +67 -0
  106. package/dist/ops.d.ts +2 -0
  107. package/dist/ops.js +20 -3
  108. package/dist/owner-channel/channel.js +17 -6
  109. package/dist/owner-channel/commands.d.ts +4 -0
  110. package/dist/owner-channel/commands.js +8 -0
  111. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  112. package/dist/owner-channel/ours-client.js +2 -17
  113. package/dist/paths.d.ts +2 -0
  114. package/dist/paths.js +2 -0
  115. package/dist/preset-migration.js +25 -0
  116. package/dist/resolved-plan.js +1 -0
  117. package/dist/rooms-tasks/add-member.d.ts +48 -0
  118. package/dist/rooms-tasks/add-member.js +83 -0
  119. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  120. package/dist/rooms-tasks/archived-absence.js +48 -0
  121. package/dist/rooms-tasks/cli.js +65 -5
  122. package/dist/rooms-tasks/close.d.ts +6 -2
  123. package/dist/rooms-tasks/close.js +246 -37
  124. package/dist/rooms-tasks/config.d.ts +1 -1
  125. package/dist/rooms-tasks/config.js +11 -4
  126. package/dist/rooms-tasks/cowork-adapter.d.ts +2 -4
  127. package/dist/rooms-tasks/cowork-adapter.js +7 -121
  128. package/dist/rooms-tasks/cowork-http.js +29 -1
  129. package/dist/rooms-tasks/deletion.d.ts +1 -1
  130. package/dist/rooms-tasks/deletion.js +84 -5
  131. package/dist/rooms-tasks/erasure.d.ts +6 -0
  132. package/dist/rooms-tasks/erasure.js +173 -0
  133. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  134. package/dist/rooms-tasks/external-worker.js +3 -4
  135. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  136. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  137. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  138. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  139. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  140. package/dist/rooms-tasks/layout-cli.js +107 -0
  141. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  142. package/dist/rooms-tasks/layout-config.js +89 -0
  143. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  144. package/dist/rooms-tasks/layout-control.js +52 -0
  145. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  146. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  147. package/dist/rooms-tasks/layout-service.d.ts +43 -0
  148. package/dist/rooms-tasks/layout-service.js +203 -0
  149. package/dist/rooms-tasks/layout.d.ts +121 -0
  150. package/dist/rooms-tasks/layout.js +285 -0
  151. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  152. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  153. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  154. package/dist/rooms-tasks/live-readiness.js +85 -0
  155. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  156. package/dist/rooms-tasks/member-ownership.js +40 -0
  157. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  158. package/dist/rooms-tasks/member-startup.js +2 -0
  159. package/dist/rooms-tasks/provision.d.ts +3 -0
  160. package/dist/rooms-tasks/provision.js +101 -49
  161. package/dist/rooms-tasks/recovery.d.ts +19 -0
  162. package/dist/rooms-tasks/recovery.js +110 -0
  163. package/dist/rooms-tasks/room-state.d.ts +4 -2
  164. package/dist/rooms-tasks/room-state.js +47 -8
  165. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  166. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  167. package/dist/rooms-tasks/task-state.d.ts +51 -8
  168. package/dist/rooms-tasks/task-state.js +245 -25
  169. package/dist/rooms-tasks/types.d.ts +40 -0
  170. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  171. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  172. package/dist/rooms-tasks/workspace.d.ts +35 -0
  173. package/dist/rooms-tasks/workspace.js +325 -0
  174. package/dist/runner.d.ts +25 -3
  175. package/dist/runner.js +291 -49
  176. package/dist/session/acp.d.ts +53 -1
  177. package/dist/session/acp.js +216 -40
  178. package/dist/session/arbiter.d.ts +11 -0
  179. package/dist/session/arbiter.js +43 -2
  180. package/dist/session/codex-app-server.d.ts +32 -0
  181. package/dist/session/codex-app-server.js +137 -23
  182. package/dist/session/control.d.ts +12 -1
  183. package/dist/session/control.js +68 -3
  184. package/dist/session/conversation-normalizer.d.ts +2 -1
  185. package/dist/session/conversation-normalizer.js +1 -1
  186. package/dist/session/conversation-store.d.ts +1 -1
  187. package/dist/session/conversation-store.js +7 -11
  188. package/dist/session/conversation-types.d.ts +28 -3
  189. package/dist/session/prompt-batch.d.ts +63 -0
  190. package/dist/session/prompt-batch.js +86 -0
  191. package/dist/session/task-notice.d.ts +14 -0
  192. package/dist/session/task-notice.js +13 -0
  193. package/dist/session/types.d.ts +14 -0
  194. package/dist/setup-tunnel-preflight.d.ts +34 -0
  195. package/dist/setup-tunnel-preflight.js +62 -0
  196. package/dist/socket-path.d.ts +2 -0
  197. package/dist/socket-path.js +22 -0
  198. package/dist/spawn.d.ts +5 -1
  199. package/dist/spawn.js +40 -4
  200. package/dist/startup-prompt.d.ts +4 -0
  201. package/dist/startup-prompt.js +36 -0
  202. package/dist/startup-readiness.d.ts +13 -0
  203. package/dist/startup-readiness.js +38 -0
  204. package/dist/startup-telemetry.d.ts +16 -0
  205. package/dist/startup-telemetry.js +46 -0
  206. package/dist/subscriptions/cli.d.ts +91 -0
  207. package/dist/subscriptions/cli.js +223 -0
  208. package/dist/subscriptions/launch.d.ts +37 -0
  209. package/dist/subscriptions/launch.js +93 -0
  210. package/dist/subscriptions/login.d.ts +75 -0
  211. package/dist/subscriptions/login.js +182 -0
  212. package/dist/subscriptions/observed.d.ts +7 -0
  213. package/dist/subscriptions/observed.js +44 -0
  214. package/dist/subscriptions/probe.d.ts +10 -0
  215. package/dist/subscriptions/probe.js +73 -0
  216. package/dist/subscriptions/service.d.ts +102 -0
  217. package/dist/subscriptions/service.js +364 -0
  218. package/dist/subscriptions/store.d.ts +71 -0
  219. package/dist/subscriptions/store.js +140 -0
  220. package/dist/supervisor/adoption.d.ts +8 -0
  221. package/dist/supervisor/adoption.js +78 -0
  222. package/dist/supervisor/catalog.d.ts +62 -0
  223. package/dist/supervisor/catalog.js +254 -0
  224. package/dist/supervisor/fleet.d.ts +8 -0
  225. package/dist/supervisor/fleet.js +89 -0
  226. package/dist/supervisor/index.js +3 -6
  227. package/dist/supervisor/launchd.d.ts +1 -1
  228. package/dist/supervisor/launchd.js +26 -13
  229. package/dist/supervisor/legacy.d.ts +35 -0
  230. package/dist/supervisor/legacy.js +524 -0
  231. package/dist/supervisor/manager.d.ts +15 -0
  232. package/dist/supervisor/manager.js +270 -0
  233. package/dist/supervisor/scope.d.ts +4 -0
  234. package/dist/supervisor/scope.js +10 -0
  235. package/dist/supervisor/systemd.d.ts +2 -2
  236. package/dist/supervisor/systemd.js +19 -33
  237. package/dist/task-supervision.d.ts +9 -0
  238. package/dist/task-supervision.js +145 -0
  239. package/dist/temp-idle.d.ts +17 -0
  240. package/dist/temp-idle.js +42 -0
  241. package/dist/temp-lifecycle.d.ts +23 -8
  242. package/dist/temp-lifecycle.js +113 -65
  243. package/dist/temp-supervisor-recovery.d.ts +1 -1
  244. package/dist/temp-supervisor-recovery.js +2 -2
  245. package/dist/tunnel-setup-completion.d.ts +7 -0
  246. package/dist/tunnel-setup-completion.js +13 -0
  247. package/dist/tunnel-setup-input.d.ts +19 -0
  248. package/dist/tunnel-setup-input.js +124 -0
  249. package/dist/web/agent-attachments.d.ts +29 -0
  250. package/dist/web/agent-attachments.js +162 -0
  251. package/dist/web/auth.d.ts +63 -2
  252. package/dist/web/auth.js +191 -10
  253. package/dist/web/available-port.d.ts +2 -0
  254. package/dist/web/available-port.js +23 -0
  255. package/dist/web/control.d.ts +3 -3
  256. package/dist/web/control.js +10 -8
  257. package/dist/web/fleet-config-service.d.ts +4 -1
  258. package/dist/web/fleet-config-service.js +46 -15
  259. package/dist/web/prefix-gateway.d.ts +32 -0
  260. package/dist/web/prefix-gateway.js +238 -0
  261. package/dist/web/runtime.d.ts +3 -1
  262. package/dist/web/runtime.js +116 -31
  263. package/dist/web/server.d.ts +16 -0
  264. package/dist/web/server.js +654 -55
  265. package/dist/web/service.d.ts +11 -2
  266. package/dist/web/service.js +30 -4
  267. package/dist/web/workspace-devices.d.ts +107 -0
  268. package/dist/web/workspace-devices.js +277 -0
  269. package/dist/web/workspace-tunnel.d.ts +1 -0
  270. package/dist/web/workspace-tunnel.js +34 -0
  271. package/dist/workspace-enrollment.d.ts +82 -0
  272. package/dist/workspace-enrollment.js +566 -0
  273. package/dist/workspace-replacement.d.ts +66 -0
  274. package/dist/workspace-replacement.js +220 -0
  275. package/docs/acp-files.md +146 -0
  276. package/docs/fleet-migration.md +54 -0
  277. package/docs/live-voice.md +13 -0
  278. package/docs/room-layouts.md +227 -0
  279. package/docs/task-workspaces.md +225 -0
  280. package/docs/validation/managed-cli-permissions.md +421 -0
  281. package/examples/room-layouts/fresh.yaml +27 -0
  282. package/examples/room-layouts/scoped.yaml +21 -0
  283. package/examples/room-layouts/shared.yaml +19 -0
  284. package/package.json +20 -19
  285. package/presets/README.md +78 -0
  286. package/presets/brain-catalog.json +8 -2
  287. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  288. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  289. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  290. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
  291. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  292. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  293. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  294. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  295. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  296. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  297. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  298. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  299. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  300. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  301. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  302. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  303. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  304. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  305. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  306. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  307. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  308. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  309. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  310. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  311. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  312. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  313. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  314. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  315. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  316. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  317. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  318. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  319. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  320. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  321. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  322. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  323. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  324. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  325. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  326. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  327. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  328. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  329. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  330. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  331. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  332. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  333. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  334. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  335. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  336. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  337. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  338. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  339. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  340. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  341. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  342. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  343. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  344. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  345. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  346. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  347. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  348. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  349. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  350. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  351. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  352. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  353. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  354. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  355. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  356. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  357. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  358. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  359. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  360. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  361. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  362. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  363. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  364. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  365. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  366. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  367. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  368. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  369. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  370. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  371. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  372. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  373. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  374. package/presets/fleet/roles/Coordinator.yaml +34 -11
  375. package/presets/fleet/roles/Engineer.yaml +40 -0
  376. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  377. package/presets/fleet/room_layouts/pair.yaml +24 -0
  378. package/presets/fleet/room_layouts/single.yaml +17 -0
  379. package/presets/fleet/room_layouts/team.yaml +30 -0
  380. package/presets/fleet/room_templates/engineering.yaml +19 -0
  381. package/presets/manifest.json +2 -2
  382. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  383. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  384. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  385. package/dist/web-app/icons/ours-fleet.svg +0 -4
  386. package/dist/web-app/index.html +0 -17
  387. package/dist/web-app/manifest.webmanifest +0 -15
  388. package/dist/web-app/offline.html +0 -18
  389. 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,6 +57,8 @@ 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;
@@ -60,9 +71,59 @@ export declare class WebAuth {
60
71
  expiresAt: string;
61
72
  };
62
73
  consumeTicket(request: FastifyRequest, value: string, purpose: Ticket['purpose'], roleId?: string): BrowserSession;
63
- bindSocket(sessionId: string, socket: WebSocket): void;
74
+ /**
75
+ * `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
76
+ * device shares one session id across credentials, so the socket ends at that credential's own
77
+ * deadline; revocation still ends everything the device has open.
78
+ */
79
+ bindSocket(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, socket: WebSocket): void;
80
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
81
+ bindTransport(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, close: () => void, renewal?: string): () => void;
82
+ /**
83
+ * Keep one open transport past the credential that opened it. Only its own reader knows the
84
+ * name it was given, and it must authenticate with a newer credential of the same device while
85
+ * the transport is still open: a copied credential cannot renew after it has expired.
86
+ */
87
+ renewTransport(request: FastifyRequest, renewal: string): {
88
+ authorizedUntil: number;
89
+ };
64
90
  clearSessions(): void;
65
91
  revokeAllTrustedDevices(): number;
92
+ workspaceIdentity(): {
93
+ workspaceId: string;
94
+ };
95
+ workspaceEnrollment(issuer?: string | null): {
96
+ enrollment: string;
97
+ workspaceId: string;
98
+ expiresAt: number;
99
+ };
100
+ enrollWorkspace(request: FastifyRequest, input: {
101
+ enrollment: string;
102
+ workspaceId: string;
103
+ label: string;
104
+ credential?: unknown;
105
+ }): import("./workspace-devices.js").WorkspaceCredentials | {
106
+ token: string;
107
+ device: import("./workspace-devices.js").WorkspaceDevice;
108
+ workspaceId: string;
109
+ };
110
+ /**
111
+ * Refresh credentials are exchanged between the account server and this host only. A browser
112
+ * always states where a request comes from, so a request that does is refused: no page can be
113
+ * handed refresh material, whichever origin it runs on.
114
+ */
115
+ private machineOnly;
116
+ refreshWorkspace(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials;
117
+ exchangeWorkspace(request: FastifyRequest): import("./workspace-devices.js").WorkspaceCredentials;
118
+ revokeWorkspaceRefresh(request: FastifyRequest, refreshHash: string): boolean;
119
+ /** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
120
+ linkWorkspaceDevice(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials & {
121
+ enrollment: string;
122
+ enrollmentExpiresAt: number;
123
+ };
124
+ listWorkspaceDevices(): import("./workspace-devices.js").WorkspaceDevice[];
125
+ revokeWorkspaceDevice(id: string): void;
126
+ private requireWorkspaceDevices;
66
127
  shutdown(): void;
67
128
  private createSession;
68
129
  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,7 +115,24 @@ 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();
@@ -121,7 +153,9 @@ export class WebAuth {
121
153
  logout(request) {
122
154
  const session = this.authenticate(request, true);
123
155
  const deviceId = this.sessionDevices.get(session.id);
124
- if (deviceId && deviceId !== 'unprotected')
156
+ if (session.id.startsWith('workspace:'))
157
+ this.workspaceDevices?.revoke(session.id.slice(10));
158
+ else if (deviceId && deviceId !== 'unprotected')
125
159
  this.devices.revokeId(deviceId);
126
160
  this.removeSession(session.id);
127
161
  }
@@ -130,7 +164,8 @@ export class WebAuth {
130
164
  const value = token();
131
165
  const ticket = {
132
166
  value, sessionId: session.id, purpose, roleId,
133
- expiresAt: this.now() + 30_000,
167
+ expiresAt: Math.min(this.now() + 30_000, session.absoluteExpiresAt),
168
+ authorizedUntil: session.absoluteExpiresAt,
134
169
  };
135
170
  this.tickets.set(value, ticket);
136
171
  return { ticket: value, expiresAt: new Date(ticket.expiresAt).toISOString() };
@@ -143,20 +178,82 @@ export class WebAuth {
143
178
  || ticket.roleId !== roleId)
144
179
  throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
145
180
  const session = this.sessions.get(ticket.sessionId);
146
- if (!session)
181
+ if (!session || (session.id.startsWith('workspace:') && !this.workspaceDevices?.valid(session.id.slice(10))))
147
182
  throw new FleetError('unauthorized', 'browser session expired');
148
- return session;
183
+ if (!session.id.startsWith('workspace:'))
184
+ return session;
185
+ // A workspace device's session entry is replaced by each newer credential; the ticket carries
186
+ // the deadline of the credential that minted it, and nothing newer extends it.
187
+ if (ticket.authorizedUntil <= this.now())
188
+ throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
189
+ return { ...session, absoluteExpiresAt: ticket.authorizedUntil };
149
190
  }
150
- bindSocket(sessionId, socket) {
191
+ /**
192
+ * `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
193
+ * device shares one session id across credentials, so the socket ends at that credential's own
194
+ * deadline; revocation still ends everything the device has open.
195
+ */
196
+ bindSocket(session, socket) {
197
+ const sessionId = session.id, deadline = sessionId.startsWith('workspace:') ? session.absoluteExpiresAt : undefined;
151
198
  let sockets = this.sockets.get(sessionId);
152
199
  if (!sockets) {
153
200
  sockets = new Set();
154
201
  this.sockets.set(sessionId, sockets);
155
202
  }
156
203
  sockets.add(socket);
157
- socket.once('close', () => sockets?.delete(socket));
204
+ const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (deadline !== undefined ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
205
+ this.removeSession(sessionId);
206
+ else if (deadline !== undefined && deadline <= this.now()) {
207
+ clearInterval(expiry);
208
+ socket.close(4401, 'authentication expired');
209
+ } }, 1000);
210
+ expiry.unref();
211
+ socket.once('close', () => { clearInterval(expiry); sockets?.delete(socket); });
212
+ }
213
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
214
+ bindTransport(session, close, renewal) {
215
+ const sessionId = session.id, workspace = sessionId.startsWith('workspace:');
216
+ let transports = this.transports.get(sessionId);
217
+ if (!transports) {
218
+ transports = new Set();
219
+ this.transports.set(sessionId, transports);
220
+ }
221
+ transports.add(close);
222
+ const lease = { sessionId, deadline: session.absoluteExpiresAt };
223
+ if (workspace && renewal)
224
+ this.renewals.set(renewal, lease);
225
+ const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (workspace ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
226
+ this.removeSession(sessionId);
227
+ else if (workspace && lease.deadline <= this.now()) {
228
+ clearInterval(expiry);
229
+ transports.delete(close);
230
+ if (renewal)
231
+ this.renewals.delete(renewal);
232
+ close();
233
+ } }, 1000);
234
+ expiry.unref();
235
+ return () => { clearInterval(expiry); transports.delete(close); if (renewal)
236
+ this.renewals.delete(renewal); if (!transports.size)
237
+ this.transports.delete(sessionId); };
238
+ }
239
+ /**
240
+ * Keep one open transport past the credential that opened it. Only its own reader knows the
241
+ * name it was given, and it must authenticate with a newer credential of the same device while
242
+ * the transport is still open: a copied credential cannot renew after it has expired.
243
+ */
244
+ renewTransport(request, renewal) {
245
+ const session = this.authenticate(request, true), lease = this.renewals.get(renewal);
246
+ if (!lease || lease.sessionId !== session.id || lease.deadline <= this.now())
247
+ throw new FleetError('resource_not_found', 'transport is not open');
248
+ lease.deadline = Math.max(lease.deadline, session.absoluteExpiresAt);
249
+ return { authorizedUntil: lease.deadline };
158
250
  }
159
251
  clearSessions() {
252
+ for (const transports of this.transports.values())
253
+ for (const close of transports)
254
+ close();
255
+ this.transports.clear();
256
+ this.renewals.clear();
160
257
  for (const sockets of this.sockets.values())
161
258
  for (const socket of sockets)
162
259
  socket.close(4401, 'authentication revoked');
@@ -167,10 +264,88 @@ export class WebAuth {
167
264
  }
168
265
  revokeAllTrustedDevices() {
169
266
  const count = this.devices.revokeAll();
267
+ for (const device of this.workspaceDevices?.list() ?? [])
268
+ this.workspaceDevices?.revoke(device.id);
170
269
  this.clearSessions();
171
270
  return count;
172
271
  }
173
- shutdown() { this.clearSessions(); }
272
+ workspaceIdentity() { return { workspaceId: this.requireWorkspaceDevices().workspaceId }; }
273
+ workspaceEnrollment(issuer = null) { return this.requireWorkspaceDevices().mint(issuer?.startsWith('workspace:') ? issuer.slice(10) : null); }
274
+ enrollWorkspace(request, input) {
275
+ if (input.credential === 2) {
276
+ this.machineOnly(request);
277
+ this.consumeRate('workspace-enrollment', 30, 60000);
278
+ return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label, true);
279
+ }
280
+ this.validateBoundary(request, true);
281
+ this.consumeRate('workspace-enrollment', 30, 60000);
282
+ return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label);
283
+ }
284
+ /**
285
+ * Refresh credentials are exchanged between the account server and this host only. A browser
286
+ * always states where a request comes from, so a request that does is refused: no page can be
287
+ * handed refresh material, whichever origin it runs on.
288
+ */
289
+ machineOnly(request) {
290
+ this.validateBoundary(request, false);
291
+ if (request.headers.origin !== undefined || Object.keys(request.headers).some(name => name.startsWith('sec-fetch-')) || request.headers.cookie !== undefined)
292
+ throw new FleetError('forbidden', 'this endpoint does not accept browser requests');
293
+ }
294
+ refreshWorkspace(request, refreshToken) {
295
+ this.machineOnly(request);
296
+ this.consumeRate('workspace-refresh', 240, 60000);
297
+ try {
298
+ return this.requireWorkspaceDevices().refresh(refreshToken);
299
+ }
300
+ catch (error) {
301
+ if (error instanceof WorkspaceRefreshReuseError)
302
+ for (const id of error.revoked)
303
+ this.removeSession(`workspace:${id}`);
304
+ throw error;
305
+ }
306
+ }
307
+ exchangeWorkspace(request) {
308
+ this.machineOnly(request);
309
+ this.consumeRate('workspace-enrollment', 30, 60000);
310
+ const authorization = String(request.headers.authorization ?? '');
311
+ const issued = this.requireWorkspaceDevices().exchange(authorization.startsWith('Bearer ') ? authorization.slice(7) : '');
312
+ // Whatever the fixed bearer had open ends with it.
313
+ this.removeSession(`workspace:${issued.device.id}`);
314
+ return issued;
315
+ }
316
+ revokeWorkspaceRefresh(request, refreshHash) {
317
+ this.machineOnly(request);
318
+ this.consumeRate('workspace-revoke', 120, 60000);
319
+ const id = this.requireWorkspaceDevices().revokeByRefreshHash(refreshHash);
320
+ if (id)
321
+ this.removeSession(`workspace:${id}`);
322
+ return Boolean(id);
323
+ }
324
+ /** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
325
+ linkWorkspaceDevice(request, refreshToken) {
326
+ this.machineOnly(request);
327
+ this.consumeRate('workspace-link', 10, 60000);
328
+ try {
329
+ return this.requireWorkspaceDevices().link(refreshToken);
330
+ }
331
+ catch (error) {
332
+ if (error instanceof WorkspaceRefreshReuseError)
333
+ for (const id of error.revoked)
334
+ this.removeSession(`workspace:${id}`);
335
+ throw error;
336
+ }
337
+ }
338
+ listWorkspaceDevices() { return this.requireWorkspaceDevices().list(); }
339
+ revokeWorkspaceDevice(id) {
340
+ this.requireWorkspaceDevices().revoke(id);
341
+ this.removeSession(`workspace:${id}`);
342
+ }
343
+ requireWorkspaceDevices() {
344
+ if (!this.workspaceDevices)
345
+ throw new FleetError('capability_unavailable', 'workspace device access is not configured');
346
+ return this.workspaceDevices;
347
+ }
348
+ shutdown() { this.clearSessions(); this.workspaceDevices?.close(); }
174
349
  createSession(deviceId) {
175
350
  const now = this.now();
176
351
  const session = {
@@ -182,6 +357,12 @@ export class WebAuth {
182
357
  return session;
183
358
  }
184
359
  removeSession(id) {
360
+ for (const close of this.transports.get(id) ?? [])
361
+ close();
362
+ this.transports.delete(id);
363
+ for (const [renewal, lease] of this.renewals)
364
+ if (lease.sessionId === id)
365
+ this.renewals.delete(renewal);
185
366
  this.sessions.delete(id);
186
367
  this.sessionDevices.delete(id);
187
368
  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>;