@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
@@ -1,6 +1,7 @@
1
1
  import { type Exec } from '../exec.js';
2
2
  export declare const WEB_SYSTEMD_UNIT = "ours-fleet-web.service";
3
3
  export declare const WEB_LAUNCHD_LABEL = "network.ours.fleet.web";
4
+ type ServiceEnvironment = Partial<Record<'OURS_CONFIG' | 'OURS_FLEET_HOME' | 'PATH', string>>;
4
5
  interface ServiceMetadata {
5
6
  version: 3;
6
7
  platform: 'linux' | 'darwin';
@@ -10,6 +11,7 @@ interface ServiceMetadata {
10
11
  configuration?: string;
11
12
  bind?: string;
12
13
  publicOrigin?: string;
14
+ environment?: ServiceEnvironment;
13
15
  }
14
16
  export interface WebServiceOptions {
15
17
  platform?: NodeJS.Platform;
@@ -18,6 +20,7 @@ export interface WebServiceOptions {
18
20
  stateDir?: string;
19
21
  uid?: number;
20
22
  runtimeExecutable?: string;
23
+ environment?: NodeJS.ProcessEnv;
21
24
  }
22
25
  export declare class WebServiceManager {
23
26
  private readonly platform;
@@ -26,6 +29,7 @@ export declare class WebServiceManager {
26
29
  private readonly stateDir;
27
30
  private readonly uid;
28
31
  private readonly runtimeExecutable;
32
+ private readonly environment;
29
33
  constructor(options?: WebServiceOptions);
30
34
  get metadataPath(): string;
31
35
  get definitionPath(): string;
@@ -33,6 +37,11 @@ export declare class WebServiceManager {
33
37
  bind?: string;
34
38
  publicOrigin?: string;
35
39
  }): Promise<string[]>;
40
+ /** Persist an enrollment bind fallback, reloading launchd's cached definition. */
41
+ retainBoundPort(script: string, port: number, configuration?: string, web?: {
42
+ bind?: string;
43
+ publicOrigin?: string;
44
+ }): Promise<void>;
36
45
  start(): Promise<void>;
37
46
  stop(): Promise<void>;
38
47
  restart(): Promise<void>;
@@ -45,9 +54,9 @@ export declare class WebServiceManager {
45
54
  export declare function systemdUnit(runtime: string, script: string, port: number, configuration?: string, web?: {
46
55
  bind?: string;
47
56
  publicOrigin?: string;
48
- }): string;
57
+ }, environment?: ServiceEnvironment): string;
49
58
  export declare function launchdPlist(runtime: string, script: string, port: number, configuration?: string, web?: {
50
59
  bind?: string;
51
60
  publicOrigin?: string;
52
- }): string;
61
+ }, environment?: ServiceEnvironment): string;
53
62
  export {};
@@ -15,6 +15,7 @@ export class WebServiceManager {
15
15
  stateDir;
16
16
  uid;
17
17
  runtimeExecutable;
18
+ environment;
18
19
  constructor(options = {}) {
19
20
  const platform = options.platform ?? process.platform;
20
21
  if (platform !== 'linux' && platform !== 'darwin')
@@ -25,6 +26,7 @@ export class WebServiceManager {
25
26
  this.stateDir = options.stateDir ?? join(stateRoot(), 'web');
26
27
  this.uid = options.uid ?? process.getuid?.() ?? 501;
27
28
  this.runtimeExecutable = options.runtimeExecutable ?? process.execPath;
29
+ this.environment = serviceEnvironment(options.environment ?? process.env);
28
30
  }
29
31
  get metadataPath() { return join(this.stateDir, 'service.json'); }
30
32
  get definitionPath() {
@@ -42,13 +44,14 @@ export class WebServiceManager {
42
44
  const config = configuration ? resolve(configuration) : undefined;
43
45
  const metadata = {
44
46
  version: VERSION, platform: this.platform, runtime, script: resolvedScript, port,
47
+ environment: this.environment,
45
48
  ...(config ? { configuration: config } : {}),
46
49
  ...(web.bind ? { bind: web.bind } : {}),
47
50
  ...(web.publicOrigin ? { publicOrigin: web.publicOrigin } : {}),
48
51
  };
49
52
  replaceFileAtomically(this.definitionPath, this.platform === 'linux'
50
- ? systemdUnit(runtime, resolvedScript, port, config, web)
51
- : launchdPlist(runtime, resolvedScript, port, config, web), 0o600);
53
+ ? systemdUnit(runtime, resolvedScript, port, config, web, this.environment)
54
+ : launchdPlist(runtime, resolvedScript, port, config, web, this.environment), 0o600);
52
55
  replaceFileAtomically(this.metadataPath, JSON.stringify(metadata, null, 2) + '\n', 0o600);
53
56
  if (this.platform === 'linux') {
54
57
  await this.must('systemctl', ['--user', 'daemon-reload']);
@@ -63,6 +66,14 @@ export class WebServiceManager {
63
66
  }
64
67
  return [`installed ${this.definitionPath}`, 'launchd service starts at login'];
65
68
  }
69
+ /** Persist an enrollment bind fallback, reloading launchd's cached definition. */
70
+ async retainBoundPort(script, port, configuration, web = {}) {
71
+ await this.install(script, port, configuration, web);
72
+ if (this.platform === 'darwin') {
73
+ await this.stop();
74
+ await this.start();
75
+ }
76
+ }
66
77
  async start() {
67
78
  this.requireInstalled();
68
79
  if (this.platform === 'linux') {
@@ -159,19 +170,20 @@ function validatePort(port) {
159
170
  function systemdQuote(value) {
160
171
  return `"${value.replace(/[%\\"]/g, char => char === '%' ? '%%' : `\\${char}`)}"`;
161
172
  }
162
- export function systemdUnit(runtime, script, port, configuration, web = {}) {
173
+ export function systemdUnit(runtime, script, port, configuration, web = {}, environment = {}) {
163
174
  const config = configuration ? ` --configuration ${systemdQuote(configuration)}` : '';
164
175
  const access = `${web.bind ? ` --bind ${systemdQuote(web.bind)}` : ''}`
165
176
  + `${web.publicOrigin ? ` --public-origin ${systemdQuote(web.publicOrigin)}` : ''}`;
166
177
  return `[Unit]\nDescription=ours-fleet localhost web console\nAfter=default.target\n\n`
167
178
  + `[Service]\nType=simple\nExecStart=${systemdQuote(runtime)} ${systemdQuote(script)} web serve --port ${port} --no-open${config}${access}\n`
179
+ + Object.entries(serviceEnvironment(environment)).map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}\n`).join('')
168
180
  + `Restart=on-failure\nRestartSec=5\nTimeoutStopSec=15\n\n`
169
181
  + `[Install]\nWantedBy=default.target\n`;
170
182
  }
171
183
  const xml = (value) => value.replace(/[&<>"']/g, char => ({
172
184
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;',
173
185
  }[char]));
174
- export function launchdPlist(runtime, script, port, configuration, web = {}) {
186
+ export function launchdPlist(runtime, script, port, configuration, web = {}, environment = {}) {
175
187
  const config = configuration
176
188
  ? `<string>--configuration</string><string>${xml(configuration)}</string>` : '';
177
189
  const access = `${web.bind ? `<string>--bind</string><string>${xml(web.bind)}</string>` : ''}`
@@ -181,6 +193,20 @@ export function launchdPlist(runtime, script, port, configuration, web = {}) {
181
193
  + `<plist version="1.0"><dict>\n<key>Label</key><string>${WEB_LAUNCHD_LABEL}</string>\n`
182
194
  + `<key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(script)}</string><string>web</string>`
183
195
  + `<string>serve</string><string>--port</string><string>${port}</string><string>--no-open</string>${config}${access}</array>\n`
196
+ + `<key>EnvironmentVariables</key><dict>${Object.entries(serviceEnvironment(environment)).map(([key, value]) => `<key>${key}</key><string>${xml(value)}</string>`).join('')}</dict>\n`
184
197
  + `<key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n`
185
198
  + `<key>ProcessType</key><string>Background</string>\n</dict></plist>\n`;
186
199
  }
200
+ // Retain only profile/tool paths, never inherited credentials or arbitrary environment.
201
+ function serviceEnvironment(source) {
202
+ const result = {};
203
+ for (const key of ['OURS_CONFIG', 'OURS_FLEET_HOME', 'PATH']) {
204
+ const value = source[key];
205
+ if (!value)
206
+ continue;
207
+ if (/[\r\n\0]/.test(value))
208
+ throw new FleetError('invalid_request', `invalid service path setting: ${key}`);
209
+ result[key] = key === 'PATH' ? value : resolve(value);
210
+ }
211
+ return result;
212
+ }
@@ -0,0 +1,107 @@
1
+ import { FleetError } from '../application/errors.js';
2
+ /** Only failure of the enrolled device check may invalidate the App's saved credential. */
3
+ export declare class WorkspaceDeviceAuthError extends FleetError {
4
+ constructor(message?: string);
5
+ }
6
+ /** A spent refresh credential came back after its replacement was in use: the link is revoked, whoever presented it. */
7
+ export declare class WorkspaceRefreshReuseError extends WorkspaceDeviceAuthError {
8
+ readonly revoked: string[];
9
+ constructor(revoked: string[]);
10
+ }
11
+ /**
12
+ * Credential lifetimes of an App browser link. Rotation is not expiry: every refresh replaces the
13
+ * refresh credential, each one lapses when unused for REFRESH_IDLE_TTL, and no rotation moves a
14
+ * link past the FAMILY_TTL fixed when it was created.
15
+ */
16
+ export declare const ACCESS_TTL: number;
17
+ export declare const REFRESH_IDLE_TTL: number;
18
+ export declare const FAMILY_TTL: number;
19
+ /** How long after this store first runs rotating credentials the earlier fixed bearer is still accepted. */
20
+ export declare const LEGACY_BEARER_WINDOW: number;
21
+ export interface WorkspaceDevice {
22
+ id: string;
23
+ label: string;
24
+ createdAt: number;
25
+ lastUsedAt: number;
26
+ expiresAt: number;
27
+ revokedAt: number | null;
28
+ }
29
+ export interface WorkspaceCredentials {
30
+ device: WorkspaceDevice;
31
+ workspaceId: string;
32
+ accessToken: string;
33
+ accessExpiresAt: number;
34
+ refreshToken: string;
35
+ refreshExpiresAt: number;
36
+ familyExpiresAt: number;
37
+ }
38
+ /** An authenticated device and the moment the presented credential stops authorizing anything. */
39
+ export type AuthenticatedWorkspaceDevice = WorkspaceDevice & {
40
+ authorizedUntil: number;
41
+ rotating: boolean;
42
+ };
43
+ export declare class WorkspaceDeviceStore {
44
+ private readonly now;
45
+ private readonly db;
46
+ readonly workspaceId: string;
47
+ /** After this moment a fixed bearer no longer authenticates and no new one is issued. */
48
+ readonly legacySunsetAt: number;
49
+ private readonly rotationKey;
50
+ constructor(dir?: string, now?: () => number);
51
+ private transaction;
52
+ mint(issuer?: string | null): {
53
+ enrollment: string;
54
+ workspaceId: string;
55
+ expiresAt: number;
56
+ };
57
+ /** Link a browser with rotating credentials. */
58
+ enroll(enrollment: string, workspaceId: string, label: string, rotating: true): WorkspaceCredentials;
59
+ /** The fixed bearer of earlier App versions; refused once the legacy window has closed. */
60
+ enroll(enrollment: string, workspaceId: string, label: string, rotating?: false): {
61
+ token: string;
62
+ device: WorkspaceDevice;
63
+ workspaceId: string;
64
+ };
65
+ /** Inside a transaction: an access credential beside the given current refresh credential. */
66
+ private grant;
67
+ private live;
68
+ private device;
69
+ /**
70
+ * Replace a refresh credential. It has exactly one successor, derived on this host. Until that
71
+ * successor is seen in use (presented itself, or an access credential issued with it
72
+ * authenticates), the spent credential is answered again with the same successor: a lost
73
+ * answer, a closed tab or a restart converge on one value and extend nothing. Once the successor
74
+ * is in use, the spent credential is a replay and ends the link and the links it issued.
75
+ */
76
+ refresh(refreshToken: string): WorkspaceCredentials;
77
+ /**
78
+ * A link code for another browser, issued by this link. It is a lasting grant, so it takes the
79
+ * link's current, unspent refresh credential and rotates it in the same step, with the successor
80
+ * counted as in use at once: whoever else holds the spent credential is a replay on their next
81
+ * refresh, which also ends the link made from this code. An answer lost here is not retried.
82
+ */
83
+ link(refreshToken: string): WorkspaceCredentials & {
84
+ enrollment: string;
85
+ enrollmentExpiresAt: number;
86
+ };
87
+ /** Inside a transaction. Returns new credentials, or the devices ended by a replay. `strict` allows no repeat and confirms the successor. */
88
+ private rotate;
89
+ /**
90
+ * One-time move of a fixed bearer to rotating credentials. The device keeps its identity, the
91
+ * bearer stops working in the same transaction, and the link ends no later than it would have.
92
+ */
93
+ exchange(token: string): WorkspaceCredentials;
94
+ private legacy;
95
+ /** The device behind an access credential, or behind a fixed bearer while those are still accepted. */
96
+ authenticate(token: string): AuthenticatedWorkspaceDevice;
97
+ /** End the link a refresh credential belongs to, named by its SHA-256: a hash can end a link, never use it. */
98
+ revokeByRefreshHash(refreshHash: string): string | undefined;
99
+ /** Inside a transaction. With cascade, links issued from this one end too. Returns every device ended. */
100
+ private revokeRows;
101
+ valid(id: string): boolean;
102
+ list(): WorkspaceDevice[];
103
+ revoke(id: string): void;
104
+ /** Retire account links without changing the installation's stable host ID or other data. */
105
+ revokeAll(): void;
106
+ close(): void;
107
+ }
@@ -0,0 +1,277 @@
1
+ import { createRequire } from 'node:module';
2
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { mkdirSync, chmodSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { stateRoot } from '../paths.js';
6
+ import { FleetError } from '../application/errors.js';
7
+ const opaque = () => randomBytes(32).toString('base64url');
8
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
9
+ /** Only failure of the enrolled device check may invalidate the App's saved credential. */
10
+ export class WorkspaceDeviceAuthError extends FleetError {
11
+ constructor(message = 'workspace device is missing, expired, or revoked') { super('unauthorized', message); }
12
+ }
13
+ /** A spent refresh credential came back after its replacement was in use: the link is revoked, whoever presented it. */
14
+ export class WorkspaceRefreshReuseError extends WorkspaceDeviceAuthError {
15
+ revoked;
16
+ constructor(revoked) {
17
+ super('workspace refresh credential was already replaced; this device link is revoked');
18
+ this.revoked = revoked;
19
+ }
20
+ }
21
+ /**
22
+ * Credential lifetimes of an App browser link. Rotation is not expiry: every refresh replaces the
23
+ * refresh credential, each one lapses when unused for REFRESH_IDLE_TTL, and no rotation moves a
24
+ * link past the FAMILY_TTL fixed when it was created.
25
+ */
26
+ export const ACCESS_TTL = 5 * 60_000;
27
+ export const REFRESH_IDLE_TTL = 7 * 86_400_000;
28
+ export const FAMILY_TTL = 30 * 86_400_000;
29
+ /** How long after this store first runs rotating credentials the earlier fixed bearer is still accepted. */
30
+ export const LEGACY_BEARER_WINDOW = 30 * 86_400_000;
31
+ const ACCESS_PREFIX = 'owa.', REFRESH_PREFIX = 'owr.';
32
+ const refreshShaped = (value) => typeof value === 'string' && value.startsWith(REFRESH_PREFIX) && value.length <= 128;
33
+ export class WorkspaceDeviceStore {
34
+ now;
35
+ db;
36
+ workspaceId;
37
+ /** After this moment a fixed bearer no longer authenticates and no new one is issued. */
38
+ legacySunsetAt;
39
+ rotationKey;
40
+ constructor(dir = join(stateRoot(), 'web'), now = Date.now) {
41
+ this.now = now;
42
+ const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
43
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
44
+ chmodSync(dir, 0o700);
45
+ const path = join(dir, 'workspace-devices.db');
46
+ this.db = new DatabaseSync(path);
47
+ chmodSync(path, 0o600);
48
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;
49
+ CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY,value TEXT NOT NULL);
50
+ CREATE TABLE IF NOT EXISTS devices(id TEXT PRIMARY KEY,hash TEXT NOT NULL,label TEXT NOT NULL,createdAt INTEGER NOT NULL,lastUsedAt INTEGER NOT NULL,expiresAt INTEGER NOT NULL,revokedAt INTEGER);
51
+ CREATE TABLE IF NOT EXISTS enrollments(hash TEXT PRIMARY KEY,expiresAt INTEGER NOT NULL,issuer TEXT);`);
52
+ // A device row is one browser link. A rotating link keeps no bearer hash: its credentials are the rows below.
53
+ const columns = this.db.prepare('PRAGMA table_info(devices)').all().map(column => column.name);
54
+ if (!columns.includes('familyExpiresAt'))
55
+ this.db.exec('ALTER TABLE devices ADD COLUMN familyExpiresAt INTEGER');
56
+ if (!columns.includes('issuer'))
57
+ this.db.exec('ALTER TABLE devices ADD COLUMN issuer TEXT');
58
+ this.db.exec(`CREATE TABLE IF NOT EXISTS refresh_tokens(hash TEXT PRIMARY KEY,deviceId TEXT NOT NULL,issuedAt INTEGER NOT NULL,expiresAt INTEGER NOT NULL,consumedAt INTEGER,confirmedAt INTEGER,parent TEXT);
59
+ CREATE INDEX IF NOT EXISTS refresh_tokens_device ON refresh_tokens(deviceId);
60
+ CREATE UNIQUE INDEX IF NOT EXISTS refresh_tokens_parent ON refresh_tokens(parent) WHERE parent IS NOT NULL;
61
+ CREATE TABLE IF NOT EXISTS access_tokens(hash TEXT PRIMARY KEY,deviceId TEXT NOT NULL,expiresAt INTEGER NOT NULL,refresh TEXT NOT NULL);
62
+ CREATE INDEX IF NOT EXISTS access_tokens_device ON access_tokens(deviceId);`);
63
+ const metadata = (key, initial) => { this.db.prepare('INSERT OR IGNORE INTO metadata VALUES(?,?)').run(key, initial); return String(this.db.prepare('SELECT value FROM metadata WHERE key=?').get(key).value); };
64
+ this.legacySunsetAt = Number(metadata('legacySunsetAt', String(this.now() + LEGACY_BEARER_WINDOW)));
65
+ // Derives each refresh credential's one successor from the credential itself; it never leaves
66
+ // this host and is useless without a current credential. It lives and dies with this
67
+ // database: a lost or replaced file ends every link, which then needs a new link code.
68
+ this.rotationKey = Buffer.from(metadata('rotationKey', opaque()), 'base64url');
69
+ this.workspaceId = metadata('workspaceId', opaque());
70
+ }
71
+ transaction(run) {
72
+ this.db.exec('BEGIN IMMEDIATE');
73
+ try {
74
+ const result = run();
75
+ this.db.exec('COMMIT');
76
+ return result;
77
+ }
78
+ catch (error) {
79
+ this.db.exec('ROLLBACK');
80
+ throw error;
81
+ }
82
+ }
83
+ mint(issuer = null) {
84
+ const now = this.now(), enrollment = opaque(), expiresAt = now + 5 * 60000;
85
+ return this.transaction(() => {
86
+ this.db.prepare('DELETE FROM enrollments WHERE expiresAt<=?').run(now);
87
+ const count = Number(this.db.prepare('SELECT count(*) AS n FROM enrollments').get().n);
88
+ if (count >= 32)
89
+ throw new FleetError('rate_limited', 'too many pending device links');
90
+ this.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(hash(enrollment), expiresAt, issuer);
91
+ return { enrollment, workspaceId: this.workspaceId, expiresAt };
92
+ });
93
+ }
94
+ enroll(enrollment, workspaceId, label, rotating = false) {
95
+ if (workspaceId !== this.workspaceId || typeof enrollment !== 'string' || !/^[\w-]{43}$/.test(enrollment))
96
+ throw new FleetError('unauthorized', 'invalid device enrollment');
97
+ if (typeof label !== 'string' || !label.trim() || label.length > 100 || /[\x00-\x1f\x7f]/.test(label))
98
+ throw new FleetError('invalid_request', 'device name must contain 1–100 printable characters');
99
+ const now = this.now(), id = opaque(), token = `${id}.${opaque()}`;
100
+ if (!rotating && now >= this.legacySunsetAt)
101
+ throw new FleetError('invalid_request', 'this workspace no longer issues fixed device credentials; update the App');
102
+ return this.transaction(() => {
103
+ const code = this.db.prepare('DELETE FROM enrollments WHERE hash=? AND expiresAt>? RETURNING issuer').get(hash(enrollment), now);
104
+ if (!code)
105
+ throw new FleetError('unauthorized', 'device enrollment is expired or already used');
106
+ if (code.issuer && !this.live(String(code.issuer), now))
107
+ throw new FleetError('unauthorized', 'issuing device has been revoked');
108
+ const count = Number(this.db.prepare('SELECT count(*) AS n FROM devices WHERE revokedAt IS NULL AND expiresAt>?').get(now).n);
109
+ if (count >= 128)
110
+ throw new FleetError('rate_limited', 'authorized device limit reached');
111
+ const issuer = code.issuer === null ? null : String(code.issuer);
112
+ if (!rotating) {
113
+ const device = { id, label: label.trim(), createdAt: now, lastUsedAt: now, expiresAt: now + 90 * 86400000, revokedAt: null };
114
+ this.db.prepare('INSERT INTO devices(id,hash,label,createdAt,lastUsedAt,expiresAt,revokedAt,issuer) VALUES(?,?,?,?,?,?,NULL,?)').run(id, hash(token), device.label, now, now, device.expiresAt, issuer);
115
+ return { token, device, workspaceId };
116
+ }
117
+ const familyExpiresAt = now + FAMILY_TTL, refreshToken = REFRESH_PREFIX + opaque(), refreshExpiresAt = Math.min(now + REFRESH_IDLE_TTL, familyExpiresAt);
118
+ const device = { id, label: label.trim(), createdAt: now, lastUsedAt: now, expiresAt: refreshExpiresAt, revokedAt: null };
119
+ this.db.prepare('INSERT INTO devices(id,hash,label,createdAt,lastUsedAt,expiresAt,revokedAt,familyExpiresAt,issuer) VALUES(?,?,?,?,?,?,NULL,?,?)').run(id, '', device.label, now, now, refreshExpiresAt, familyExpiresAt, issuer);
120
+ this.db.prepare('INSERT INTO refresh_tokens VALUES(?,?,?,?,NULL,NULL,NULL)').run(hash(refreshToken), id, now, refreshExpiresAt);
121
+ return this.grant(device, familyExpiresAt, refreshToken, refreshExpiresAt, now);
122
+ });
123
+ }
124
+ /** Inside a transaction: an access credential beside the given current refresh credential. */
125
+ grant(device, familyExpiresAt, refreshToken, refreshExpiresAt, now) {
126
+ const accessToken = ACCESS_PREFIX + opaque(), accessExpiresAt = Math.min(now + ACCESS_TTL, familyExpiresAt);
127
+ this.db.prepare('DELETE FROM access_tokens WHERE expiresAt<=?').run(now);
128
+ this.db.prepare('DELETE FROM refresh_tokens WHERE deviceId IN (SELECT id FROM devices WHERE revokedAt IS NOT NULL OR expiresAt<=?)').run(now);
129
+ this.db.prepare('INSERT INTO access_tokens VALUES(?,?,?,?)').run(hash(accessToken), device.id, accessExpiresAt, hash(refreshToken));
130
+ return { device, workspaceId: this.workspaceId, accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, familyExpiresAt };
131
+ }
132
+ live(id, now) {
133
+ const row = this.db.prepare('SELECT * FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(id, now);
134
+ if (!row || (row.familyExpiresAt === null && now >= this.legacySunsetAt))
135
+ return undefined;
136
+ return { id: String(row.id), hash: String(row.hash), label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: Number(row.lastUsedAt), expiresAt: Number(row.expiresAt), revokedAt: null, familyExpiresAt: row.familyExpiresAt === null ? null : Number(row.familyExpiresAt) };
137
+ }
138
+ device(row, changes = {}) {
139
+ return { id: row.id, label: row.label, createdAt: row.createdAt, lastUsedAt: row.lastUsedAt, expiresAt: row.expiresAt, revokedAt: null, ...changes };
140
+ }
141
+ /**
142
+ * Replace a refresh credential. It has exactly one successor, derived on this host. Until that
143
+ * successor is seen in use (presented itself, or an access credential issued with it
144
+ * authenticates), the spent credential is answered again with the same successor: a lost
145
+ * answer, a closed tab or a restart converge on one value and extend nothing. Once the successor
146
+ * is in use, the spent credential is a replay and ends the link and the links it issued.
147
+ */
148
+ refresh(refreshToken) {
149
+ const outcome = this.transaction(() => this.rotate(refreshToken, false));
150
+ if (Array.isArray(outcome))
151
+ throw new WorkspaceRefreshReuseError(outcome);
152
+ return outcome;
153
+ }
154
+ /**
155
+ * A link code for another browser, issued by this link. It is a lasting grant, so it takes the
156
+ * link's current, unspent refresh credential and rotates it in the same step, with the successor
157
+ * counted as in use at once: whoever else holds the spent credential is a replay on their next
158
+ * refresh, which also ends the link made from this code. An answer lost here is not retried.
159
+ */
160
+ link(refreshToken) {
161
+ const now = this.now(), enrollment = opaque(), enrollmentExpiresAt = now + 5 * 60000;
162
+ const outcome = this.transaction(() => {
163
+ const rotated = this.rotate(refreshToken, true);
164
+ if (Array.isArray(rotated))
165
+ return rotated;
166
+ this.db.prepare('DELETE FROM enrollments WHERE expiresAt<=?').run(now);
167
+ if (Number(this.db.prepare('SELECT count(*) AS n FROM enrollments').get().n) >= 32)
168
+ throw new FleetError('rate_limited', 'too many pending device links');
169
+ this.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(hash(enrollment), enrollmentExpiresAt, rotated.device.id);
170
+ return { ...rotated, enrollment, enrollmentExpiresAt };
171
+ });
172
+ if (Array.isArray(outcome))
173
+ throw new WorkspaceRefreshReuseError(outcome);
174
+ return outcome;
175
+ }
176
+ /** Inside a transaction. Returns new credentials, or the devices ended by a replay. `strict` allows no repeat and confirms the successor. */
177
+ rotate(refreshToken, strict) {
178
+ if (!refreshShaped(refreshToken))
179
+ throw new WorkspaceDeviceAuthError();
180
+ const now = this.now(), key = hash(refreshToken);
181
+ const row = this.db.prepare('SELECT * FROM refresh_tokens WHERE hash=?').get(key);
182
+ const device = row && this.live(String(row.deviceId), now);
183
+ if (!row || !device || device.familyExpiresAt === null)
184
+ throw new WorkspaceDeviceAuthError();
185
+ const next = REFRESH_PREFIX + createHmac('sha256', this.rotationKey).update(`ours.fleet.workspace-refresh.v1\0${device.id}\0${refreshToken}`).digest('base64url');
186
+ if (row.consumedAt === null) {
187
+ if (Number(row.expiresAt) <= now)
188
+ throw new WorkspaceDeviceAuthError();
189
+ const expiresAt = Math.min(now + REFRESH_IDLE_TTL, device.familyExpiresAt);
190
+ this.db.prepare('UPDATE refresh_tokens SET consumedAt=?,confirmedAt=coalesce(confirmedAt,?) WHERE hash=?').run(now, now, key);
191
+ this.db.prepare('INSERT INTO refresh_tokens VALUES(?,?,?,?,NULL,?,?)').run(hash(next), device.id, now, expiresAt, strict ? now : null, key);
192
+ // The link itself lapses with its newest refresh credential.
193
+ this.db.prepare('UPDATE devices SET expiresAt=?,lastUsedAt=? WHERE id=?').run(expiresAt, now, device.id);
194
+ return this.grant(this.device(device, { expiresAt, lastUsedAt: now }), device.familyExpiresAt, next, expiresAt, now);
195
+ }
196
+ const successor = this.db.prepare('SELECT * FROM refresh_tokens WHERE parent=?').get(key);
197
+ if (strict || !successor || successor.confirmedAt !== null || successor.consumedAt !== null)
198
+ return this.revokeRows(device.id, now, true);
199
+ if (Number(successor.expiresAt) <= now)
200
+ throw new WorkspaceDeviceAuthError();
201
+ return this.grant(this.device(device), device.familyExpiresAt, next, Number(successor.expiresAt), now);
202
+ }
203
+ /**
204
+ * One-time move of a fixed bearer to rotating credentials. The device keeps its identity, the
205
+ * bearer stops working in the same transaction, and the link ends no later than it would have.
206
+ */
207
+ exchange(token) {
208
+ const now = this.now();
209
+ return this.transaction(() => {
210
+ const legacy = this.legacy(token, now);
211
+ const familyExpiresAt = Math.min(legacy.expiresAt, now + FAMILY_TTL), refreshToken = REFRESH_PREFIX + opaque(), refreshExpiresAt = Math.min(now + REFRESH_IDLE_TTL, familyExpiresAt);
212
+ this.db.prepare('UPDATE devices SET hash=?,familyExpiresAt=?,expiresAt=?,lastUsedAt=? WHERE id=?').run('', familyExpiresAt, refreshExpiresAt, now, legacy.id);
213
+ this.db.prepare('INSERT INTO refresh_tokens VALUES(?,?,?,?,NULL,NULL,NULL)').run(hash(refreshToken), legacy.id, now, refreshExpiresAt);
214
+ return this.grant({ ...legacy, expiresAt: refreshExpiresAt, lastUsedAt: now }, familyExpiresAt, refreshToken, refreshExpiresAt, now);
215
+ });
216
+ }
217
+ legacy(token, now) {
218
+ const row = this.live(typeof token === 'string' ? token.split('.')[0] : '', now);
219
+ const supplied = Buffer.from(hash(typeof token === 'string' ? token : '')), expected = Buffer.from(row?.hash || '0'.repeat(64));
220
+ if (!row || row.familyExpiresAt !== null || !timingSafeEqual(supplied, expected))
221
+ throw new WorkspaceDeviceAuthError();
222
+ return this.device(row);
223
+ }
224
+ /** The device behind an access credential, or behind a fixed bearer while those are still accepted. */
225
+ authenticate(token) {
226
+ const now = this.now();
227
+ if (typeof token === 'string' && token.startsWith(ACCESS_PREFIX)) {
228
+ const access = token.length <= 128 ? this.db.prepare('SELECT * FROM access_tokens WHERE hash=? AND expiresAt>?').get(hash(token), now) : undefined;
229
+ const device = access && this.live(String(access.deviceId), now);
230
+ if (!access || !device || device.familyExpiresAt === null)
231
+ throw new WorkspaceDeviceAuthError();
232
+ this.db.prepare('UPDATE devices SET lastUsedAt=? WHERE id=?').run(now, device.id);
233
+ // Use of this credential shows its refresh credential reached the browser.
234
+ this.db.prepare('UPDATE refresh_tokens SET confirmedAt=? WHERE hash=? AND confirmedAt IS NULL').run(now, String(access.refresh));
235
+ return { ...this.device(device, { lastUsedAt: now }), authorizedUntil: Number(access.expiresAt), rotating: true };
236
+ }
237
+ const device = this.legacy(token, now);
238
+ this.db.prepare('UPDATE devices SET lastUsedAt=? WHERE id=?').run(now, device.id);
239
+ return { ...device, lastUsedAt: now, authorizedUntil: Math.min(device.expiresAt, this.legacySunsetAt), rotating: false };
240
+ }
241
+ /** End the link a refresh credential belongs to, named by its SHA-256: a hash can end a link, never use it. */
242
+ revokeByRefreshHash(refreshHash) {
243
+ if (typeof refreshHash !== 'string' || !/^[a-f0-9]{64}$/.test(refreshHash))
244
+ return undefined;
245
+ const row = this.db.prepare('SELECT deviceId FROM refresh_tokens WHERE hash=?').get(refreshHash);
246
+ if (!row)
247
+ return undefined;
248
+ this.revoke(String(row.deviceId));
249
+ return String(row.deviceId);
250
+ }
251
+ /** Inside a transaction. With cascade, links issued from this one end too. Returns every device ended. */
252
+ revokeRows(id, now, cascade = false) {
253
+ const ended = [], pending = [id];
254
+ while (pending.length) {
255
+ const current = pending.pop();
256
+ if (ended.includes(current))
257
+ continue;
258
+ ended.push(current);
259
+ this.db.prepare('UPDATE devices SET revokedAt=? WHERE id=? AND revokedAt IS NULL').run(now, current);
260
+ this.db.prepare('DELETE FROM enrollments WHERE issuer=?').run(current);
261
+ this.db.prepare('DELETE FROM access_tokens WHERE deviceId=?').run(current);
262
+ this.db.prepare('DELETE FROM refresh_tokens WHERE deviceId=?').run(current);
263
+ if (cascade)
264
+ for (const child of this.db.prepare('SELECT id FROM devices WHERE issuer=? AND revokedAt IS NULL').all(current))
265
+ pending.push(String(child.id));
266
+ }
267
+ return ended;
268
+ }
269
+ valid(id) { return Boolean(this.live(id, this.now())); }
270
+ list() { return this.db.prepare('SELECT id,label,createdAt,lastUsedAt,expiresAt,revokedAt FROM devices ORDER BY createdAt DESC LIMIT 256').all().map(row => ({ id: String(row.id), label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: Number(row.lastUsedAt), expiresAt: Number(row.expiresAt), revokedAt: row.revokedAt === null ? null : Number(row.revokedAt) })); }
271
+ revoke(id) { this.transaction(() => this.revokeRows(id, this.now())); }
272
+ /** Retire account links without changing the installation's stable host ID or other data. */
273
+ revokeAll() {
274
+ this.transaction(() => { this.db.prepare('UPDATE devices SET revokedAt=? WHERE revokedAt IS NULL').run(this.now()); this.db.exec('DELETE FROM enrollments; DELETE FROM access_tokens; DELETE FROM refresh_tokens'); });
275
+ }
276
+ close() { this.db.close(); }
277
+ }
@@ -0,0 +1 @@
1
+ export declare function startWorkspaceTunnel(origin: string): () => Promise<void>;
@@ -0,0 +1,34 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { stateRoot } from '../paths.js';
5
+ export function startWorkspaceTunnel(origin) {
6
+ const dir = join(stateRoot(), 'workspace'), file = join(dir, 'tunnel.json');
7
+ if (!existsSync(file))
8
+ return async () => { };
9
+ const config = JSON.parse(readFileSync(file, 'utf8'));
10
+ if (config.origin !== origin)
11
+ throw Error('Workspace tunnel requires its configured public origin');
12
+ const tokenFile = join(dir, 'connector');
13
+ if (config.tokenFile !== tokenFile)
14
+ throw Error('Unexpected connector credential path');
15
+ const stat = lstatSync(tokenFile);
16
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())
17
+ throw Error('Connector credential must be an owned private file');
18
+ let stopped = false, child, timer, backoff = 1000;
19
+ const launch = () => {
20
+ if (stopped)
21
+ return;
22
+ child = spawn('cloudflared', ['tunnel', '--no-autoupdate', 'run', '--token-file', tokenFile], { stdio: 'ignore' });
23
+ let scheduled = false;
24
+ // Keep an error listener for the child's whole lifetime, including shutdown.
25
+ const retry = () => { if (scheduled || stopped)
26
+ return; scheduled = true; timer = setTimeout(launch, backoff); backoff = Math.min(30000, backoff * 2); };
27
+ child.on('error', retry);
28
+ child.once('exit', retry);
29
+ };
30
+ launch();
31
+ // A failed spawn has no pid and reports error/close rather than exit.
32
+ return async () => { stopped = true; clearTimeout(timer); const current = child; if (!current?.pid || current.exitCode !== null || current.signalCode !== null)
33
+ return; await new Promise(resolve => { const timeout = setTimeout(() => { current.kill('SIGKILL'); resolve(); }, 5000); current.once('close', () => { clearTimeout(timeout); resolve(); }); current.kill('SIGTERM'); }); };
34
+ }
@@ -0,0 +1,82 @@
1
+ import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
2
+ export interface WorkspacePayload {
3
+ version: 1;
4
+ appOrigin: string;
5
+ hostname: string;
6
+ rootName: string;
7
+ name: string;
8
+ surname: string;
9
+ connectorToken?: string;
10
+ invitation: string;
11
+ serverCid: string;
12
+ challenge: {
13
+ nonce: string;
14
+ workspaceId: string;
15
+ accountId: string;
16
+ expiresAt: number;
17
+ };
18
+ }
19
+ export declare function readWorkspacePayload(file: string): WorkspacePayload;
20
+ export declare function decodeWorkspacePayload(input: string, options?: {
21
+ allowExpired?: boolean;
22
+ }): WorkspacePayload;
23
+ /** Credential-free renewal is allowed only on the host already holding this private connector. */
24
+ export declare function checkWorkspaceConnector(payload: WorkspacePayload): void;
25
+ type DaemonClient = Pick<OursClient, 'listIdentities' | 'chooseIdentity' | 'addContact' | 'listContacts' | 'sendCommand' | 'removeContact' | 'releaseLease' | 'close'>;
26
+ /** How tunnel setup reaches this host's daemon; replaced only by tests. */
27
+ export type AttachDaemonClient = (options: AttachOursClientOptions) => Promise<DaemonClient>;
28
+ /**
29
+ * Check the Fleet configuration tunnel setup will use, before the one-time command is used.
30
+ * Reads only; an existing Owner is compared with this host's own Messenger identity.
31
+ */
32
+ export declare function checkWorkspaceConfiguration(configuration?: string, attach?: AttachDaemonClient): Promise<void>;
33
+ export declare function enrollWorkspace(payload: WorkspacePayload, configuration?: string, options?: {
34
+ migrateAppOrigin?: boolean;
35
+ preserveProfile?: boolean;
36
+ replacingWorkspace?: string;
37
+ attach?: AttachDaemonClient;
38
+ contactWaitMs?: number;
39
+ afterInvitation?: () => void;
40
+ }): Promise<{
41
+ origin: string;
42
+ hostWorkspaceId: string;
43
+ rootCid: string;
44
+ }>;
45
+ /** Only the existing signed-root receipt authorizes the scoped tunnel target. */
46
+ /**
47
+ * The enrollment server contact exists only to carry the signed binding. Once the account has confirmed
48
+ * that binding, remove exactly that contact from this host's Messenger, so it does not stay in the
49
+ * person's chats. The server identity is the one recorded with the binding; nothing else is touched.
50
+ */
51
+ export declare function removeEnrollmentContact(attach?: AttachDaemonClient): Promise<boolean>;
52
+ export declare function configureWorkspacePort(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number): Promise<void>;
53
+ /**
54
+ * Point the tunnel at this host's port. The account answers only for a confirmed signed binding, and
55
+ * only after that answer is the enrollment contact removed; a refused or unconfirmed target leaves it,
56
+ * so the setup can be finished later. A removal that fails is reported and does not undo the setup.
57
+ */
58
+ export declare function confirmWorkspaceTarget(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number, cleanupFailed?: (error: unknown) => void): Promise<void>;
59
+ /** Retire exactly the original registration using the same signed Human root that bound it. */
60
+ export declare function unregisterWorkspace(previous: {
61
+ workspaceId: string;
62
+ hostWorkspaceId: string;
63
+ appOrigin: string;
64
+ serverCid: string;
65
+ proofRootCid?: string;
66
+ }, payload: WorkspacePayload, operationNonce: string, attach?: AttachDaemonClient, options?: {
67
+ request?: typeof fetch;
68
+ waitMs?: number;
69
+ checkOnly?: boolean;
70
+ beforeSend?: () => void;
71
+ operationExpiresAt?: number;
72
+ onReceipt?: (status: {
73
+ deleted?: boolean;
74
+ retired?: boolean;
75
+ absent?: boolean;
76
+ rejected?: boolean;
77
+ expired?: boolean;
78
+ deadlineEnforced?: boolean;
79
+ oldBindingPresent?: boolean;
80
+ }) => void;
81
+ }): Promise<string>;
82
+ export {};