@ours.network/fleet 1.2.0-nightly.4 → 1.2.0-nightly.41

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 (311) hide show
  1. package/README.md +165 -53
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.js +9 -0
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/harness.js +8 -0
  8. package/dist/agent-ours/runtime.d.ts +6 -0
  9. package/dist/agent-ours/runtime.js +36 -0
  10. package/dist/agent-ours/service.js +33 -13
  11. package/dist/application/codex-models.d.ts +15 -0
  12. package/dist/application/codex-models.js +56 -0
  13. package/dist/application/conversation-history.d.ts +25 -0
  14. package/dist/application/conversation-history.js +189 -0
  15. package/dist/application/fleet-query-service.d.ts +1 -1
  16. package/dist/application/fleet-query-service.js +2 -2
  17. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  18. package/dist/application/legacy-supervisor-identity.js +43 -0
  19. package/dist/application/model-catalog.d.ts +1 -1
  20. package/dist/application/model-catalog.js +5 -2
  21. package/dist/application/preset-provenance.d.ts +19 -0
  22. package/dist/application/preset-provenance.js +59 -0
  23. package/dist/application/role-creation-service.js +1 -1
  24. package/dist/application/role-repository.js +2 -2
  25. package/dist/application/room-layout-definitions.d.ts +45 -0
  26. package/dist/application/room-layout-definitions.js +130 -0
  27. package/dist/application/session-control.d.ts +18 -0
  28. package/dist/application/session-control.js +54 -3
  29. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  30. package/dist/application/supervisor-ours-tools.js +113 -0
  31. package/dist/application/task-layouts.d.ts +91 -0
  32. package/dist/application/task-layouts.js +249 -0
  33. package/dist/application/task-room-service.d.ts +99 -3
  34. package/dist/application/task-room-service.js +296 -31
  35. package/dist/atomic-file.d.ts +5 -0
  36. package/dist/atomic-file.js +62 -1
  37. package/dist/build-info.json +6 -4
  38. package/dist/capabilities.d.ts +3 -1
  39. package/dist/capabilities.js +4 -0
  40. package/dist/cli.js +186 -34
  41. package/dist/client-profile.d.ts +5 -16
  42. package/dist/client-profile.js +4 -112
  43. package/dist/config.d.ts +18 -0
  44. package/dist/config.js +28 -4
  45. package/dist/creation.js +2 -2
  46. package/dist/daemon-recovery.d.ts +1 -8
  47. package/dist/daemon-recovery.js +8 -113
  48. package/dist/docs.d.ts +3 -3
  49. package/dist/docs.js +44 -22
  50. package/dist/doctor.d.ts +1 -1
  51. package/dist/doctor.js +46 -59
  52. package/dist/erased-resources.d.ts +9 -0
  53. package/dist/erased-resources.js +87 -0
  54. package/dist/fleet-command-audit.d.ts +1 -0
  55. package/dist/fleet-command-audit.js +11 -6
  56. package/dist/harness/claude-code.js +9 -1
  57. package/dist/harness/claude-runtime.d.ts +12 -0
  58. package/dist/harness/claude-runtime.js +43 -0
  59. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  60. package/dist/harness/codex-app-server-proxy.js +41 -5
  61. package/dist/harness/codex-runtime.d.ts +11 -1
  62. package/dist/harness/codex-runtime.js +31 -4
  63. package/dist/harness/codex.d.ts +3 -2
  64. package/dist/harness/codex.js +8 -5
  65. package/dist/init-wizard.d.ts +2 -0
  66. package/dist/init-wizard.js +38 -19
  67. package/dist/isolation/policy.js +2 -0
  68. package/dist/isolation/types.d.ts +5 -0
  69. package/dist/minimal-setup.d.ts +2 -0
  70. package/dist/minimal-setup.js +44 -0
  71. package/dist/monitor.d.ts +1 -34
  72. package/dist/monitor.js +24 -151
  73. package/dist/notifications/fleet-producer.d.ts +18 -0
  74. package/dist/notifications/fleet-producer.js +129 -0
  75. package/dist/notifications/outbox.d.ts +42 -0
  76. package/dist/notifications/outbox.js +149 -0
  77. package/dist/owner-channel/channel.d.ts +3 -0
  78. package/dist/owner-channel/channel.js +34 -6
  79. package/dist/owner-channel/commands.d.ts +4 -0
  80. package/dist/owner-channel/commands.js +8 -0
  81. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  82. package/dist/owner-channel/ours-client.js +2 -17
  83. package/dist/paths.d.ts +2 -0
  84. package/dist/paths.js +2 -0
  85. package/dist/preset-migration.js +25 -0
  86. package/dist/rooms-tasks/add-member.d.ts +48 -0
  87. package/dist/rooms-tasks/add-member.js +83 -0
  88. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  89. package/dist/rooms-tasks/archived-absence.js +48 -0
  90. package/dist/rooms-tasks/cli.js +34 -5
  91. package/dist/rooms-tasks/close.d.ts +5 -2
  92. package/dist/rooms-tasks/close.js +208 -28
  93. package/dist/rooms-tasks/config.d.ts +1 -1
  94. package/dist/rooms-tasks/config.js +11 -4
  95. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  96. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  97. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  98. package/dist/rooms-tasks/cowork-http.js +70 -0
  99. package/dist/rooms-tasks/deletion.d.ts +1 -1
  100. package/dist/rooms-tasks/deletion.js +74 -5
  101. package/dist/rooms-tasks/erasure.d.ts +6 -0
  102. package/dist/rooms-tasks/erasure.js +169 -0
  103. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  104. package/dist/rooms-tasks/external-worker.js +3 -4
  105. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  106. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  107. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  108. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  109. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  110. package/dist/rooms-tasks/layout-cli.js +107 -0
  111. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  112. package/dist/rooms-tasks/layout-config.js +89 -0
  113. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  114. package/dist/rooms-tasks/layout-control.js +52 -0
  115. package/dist/rooms-tasks/layout-service.d.ts +42 -0
  116. package/dist/rooms-tasks/layout-service.js +185 -0
  117. package/dist/rooms-tasks/layout.d.ts +114 -0
  118. package/dist/rooms-tasks/layout.js +250 -0
  119. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  120. package/dist/rooms-tasks/live-readiness.js +85 -0
  121. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  122. package/dist/rooms-tasks/member-startup.js +2 -0
  123. package/dist/rooms-tasks/provision.d.ts +14 -0
  124. package/dist/rooms-tasks/provision.js +173 -46
  125. package/dist/rooms-tasks/room-state.d.ts +4 -2
  126. package/dist/rooms-tasks/room-state.js +53 -8
  127. package/dist/rooms-tasks/task-state.d.ts +42 -7
  128. package/dist/rooms-tasks/task-state.js +153 -23
  129. package/dist/rooms-tasks/types.d.ts +28 -0
  130. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  131. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  132. package/dist/rooms-tasks/workspace.d.ts +26 -0
  133. package/dist/rooms-tasks/workspace.js +195 -0
  134. package/dist/runner.d.ts +19 -3
  135. package/dist/runner.js +86 -29
  136. package/dist/session/acp.d.ts +7 -1
  137. package/dist/session/acp.js +21 -6
  138. package/dist/session/arbiter.d.ts +3 -0
  139. package/dist/session/arbiter.js +20 -2
  140. package/dist/session/codex-app-server.d.ts +3 -0
  141. package/dist/session/codex-app-server.js +26 -3
  142. package/dist/session/control.d.ts +10 -1
  143. package/dist/session/control.js +37 -2
  144. package/dist/session/conversation-normalizer.d.ts +2 -1
  145. package/dist/session/conversation-normalizer.js +1 -1
  146. package/dist/session/conversation-types.d.ts +5 -3
  147. package/dist/session/task-notice.d.ts +14 -0
  148. package/dist/session/task-notice.js +13 -0
  149. package/dist/session/types.d.ts +3 -0
  150. package/dist/setup-tunnel-preflight.d.ts +32 -0
  151. package/dist/setup-tunnel-preflight.js +58 -0
  152. package/dist/socket-path.d.ts +2 -0
  153. package/dist/socket-path.js +22 -0
  154. package/dist/spawn.js +7 -0
  155. package/dist/startup-readiness.d.ts +13 -0
  156. package/dist/startup-readiness.js +38 -0
  157. package/dist/subscriptions/cli.d.ts +91 -0
  158. package/dist/subscriptions/cli.js +223 -0
  159. package/dist/subscriptions/launch.d.ts +37 -0
  160. package/dist/subscriptions/launch.js +93 -0
  161. package/dist/subscriptions/login.d.ts +75 -0
  162. package/dist/subscriptions/login.js +182 -0
  163. package/dist/subscriptions/observed.d.ts +7 -0
  164. package/dist/subscriptions/observed.js +44 -0
  165. package/dist/subscriptions/probe.d.ts +10 -0
  166. package/dist/subscriptions/probe.js +73 -0
  167. package/dist/subscriptions/service.d.ts +102 -0
  168. package/dist/subscriptions/service.js +364 -0
  169. package/dist/subscriptions/store.d.ts +71 -0
  170. package/dist/subscriptions/store.js +140 -0
  171. package/dist/supervisor/launchd.js +2 -1
  172. package/dist/supervisor/systemd.js +9 -23
  173. package/dist/temp-idle.d.ts +17 -0
  174. package/dist/temp-idle.js +42 -0
  175. package/dist/temp-lifecycle.d.ts +6 -1
  176. package/dist/temp-lifecycle.js +29 -13
  177. package/dist/web/agent-attachments.d.ts +29 -0
  178. package/dist/web/agent-attachments.js +162 -0
  179. package/dist/web/auth.d.ts +29 -1
  180. package/dist/web/auth.js +81 -7
  181. package/dist/web/available-port.d.ts +2 -0
  182. package/dist/web/available-port.js +23 -0
  183. package/dist/web/control.d.ts +3 -3
  184. package/dist/web/control.js +10 -8
  185. package/dist/web/fleet-config-service.d.ts +4 -1
  186. package/dist/web/fleet-config-service.js +46 -15
  187. package/dist/web/prefix-gateway.d.ts +29 -0
  188. package/dist/web/prefix-gateway.js +206 -0
  189. package/dist/web/runtime.d.ts +3 -1
  190. package/dist/web/runtime.js +116 -31
  191. package/dist/web/server.d.ts +16 -0
  192. package/dist/web/server.js +501 -53
  193. package/dist/web/service.d.ts +11 -2
  194. package/dist/web/service.js +30 -4
  195. package/dist/web/workspace-devices.d.ts +29 -0
  196. package/dist/web/workspace-devices.js +84 -0
  197. package/dist/web/workspace-tunnel.d.ts +1 -0
  198. package/dist/web/workspace-tunnel.js +32 -0
  199. package/dist/workspace-enrollment.d.ts +53 -0
  200. package/dist/workspace-enrollment.js +370 -0
  201. package/docs/room-layouts.md +227 -0
  202. package/docs/task-workspaces.md +130 -0
  203. package/examples/room-layouts/fresh.yaml +27 -0
  204. package/examples/room-layouts/scoped.yaml +21 -0
  205. package/examples/room-layouts/shared.yaml +19 -0
  206. package/package.json +13 -18
  207. package/presets/README.md +78 -0
  208. package/presets/brain-catalog.json +8 -2
  209. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  210. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  211. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  212. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
  213. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  214. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  215. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  216. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  217. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  218. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  219. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  220. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  221. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  222. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  223. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  224. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  225. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  226. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  227. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  228. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  229. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  230. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  231. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  232. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  233. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  234. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  235. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  236. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  237. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  238. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  239. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  240. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  241. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  242. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  243. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  244. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  245. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  246. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  247. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  248. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  249. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  250. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  251. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  252. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  253. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  254. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  255. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  256. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  257. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  258. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  259. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  260. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  261. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  262. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  263. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  264. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  265. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  266. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  267. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  268. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  269. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  270. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  271. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  272. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  273. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  274. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  275. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  276. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  277. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  278. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  279. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  280. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  281. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  282. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  283. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  284. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  285. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  286. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  287. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  288. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  289. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  290. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  291. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  292. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  293. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  294. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  295. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  296. package/presets/fleet/roles/Coordinator.yaml +34 -11
  297. package/presets/fleet/roles/Engineer.yaml +40 -0
  298. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  299. package/presets/fleet/room_layouts/pair.yaml +24 -0
  300. package/presets/fleet/room_layouts/single.yaml +17 -0
  301. package/presets/fleet/room_layouts/team.yaml +30 -0
  302. package/presets/fleet/room_templates/engineering.yaml +19 -0
  303. package/presets/manifest.json +2 -2
  304. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  305. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  306. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  307. package/dist/web-app/icons/ours-fleet.svg +0 -4
  308. package/dist/web-app/index.html +0 -17
  309. package/dist/web-app/manifest.webmanifest +0 -15
  310. package/dist/web-app/offline.html +0 -18
  311. package/dist/web-app/sw.js +0 -51
package/dist/web/auth.js CHANGED
@@ -14,6 +14,8 @@ export class WebAuth {
14
14
  now;
15
15
  devices;
16
16
  access;
17
+ workspaceDevices;
18
+ appOrigin;
17
19
  _bootstrapSecret = token(32);
18
20
  bootstrapExpiresAt = Date.now() + 5 * 60_000;
19
21
  bootstrapUsed = false;
@@ -21,13 +23,16 @@ export class WebAuth {
21
23
  sessionDevices = new Map();
22
24
  tickets = new Map();
23
25
  rates = new Map();
26
+ transports = new Map();
24
27
  sockets = new Map();
25
- constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }) {
28
+ constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }, workspaceDevices, appOrigin = 'https://app.ours.network') {
26
29
  this._origin = _origin;
27
30
  this._host = _host;
28
31
  this.now = now;
29
32
  this.devices = devices;
30
33
  this.access = access;
34
+ this.workspaceDevices = workspaceDevices;
35
+ this.appOrigin = appOrigin;
31
36
  }
32
37
  get bootstrapSecret() { return this._bootstrapSecret; }
33
38
  get origin() { return this._origin; }
@@ -49,16 +54,24 @@ export class WebAuth {
49
54
  this.bootstrapUsed = false;
50
55
  return this._bootstrapSecret;
51
56
  }
57
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
58
+ accountOrigin(request) {
59
+ return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
60
+ }
52
61
  validateBoundary(request, requireOrigin) {
53
62
  const host = request.headers.host;
54
63
  const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
55
64
  const origins = this.allowedOrigins.size ? this.allowedOrigins : new Set([this.origin]);
56
65
  if (!host || !hosts.has(host))
57
66
  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)))
67
+ if (this.workspaceDevices && String(request.headers.authorization ?? '').startsWith('Bearer ') && request.headers.origin && request.headers.origin !== this.appOrigin && !origins.has(request.headers.origin))
68
+ throw new FleetError('forbidden', 'workspace request Origin does not match configured account origin');
69
+ const workspaceRequest = Boolean(this.workspaceDevices) && request.headers.origin === this.appOrigin
70
+ && (String(request.headers.authorization ?? '').startsWith('Bearer ') || (request.url ?? '').split('?')[0] === '/api/v1/devices/enroll' || request.method === 'OPTIONS');
71
+ if (requireOrigin && !workspaceRequest && (!request.headers.origin || !origins.has(request.headers.origin)))
59
72
  throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
60
73
  const fetchSite = request.headers['sec-fetch-site'];
61
- if (fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
74
+ if (!workspaceRequest && fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
62
75
  throw new FleetError('forbidden', 'cross-site request rejected');
63
76
  }
64
77
  exchange(request) {
@@ -100,7 +113,23 @@ export class WebAuth {
100
113
  return { session: this.createSession(device.id), device };
101
114
  }
102
115
  authenticate(request, mutation = false) {
116
+ const authorization = String(request.headers.authorization ?? '');
117
+ if (authorization.startsWith('Bearer ')) {
118
+ this.validateBoundary(request, mutation);
119
+ if (!this.workspaceDevices)
120
+ throw new FleetError('unauthorized', 'workspace bearer credential is invalid');
121
+ const device = this.requireWorkspaceDevices().authenticate(authorization.slice(7));
122
+ const id = `workspace:${device.id}`;
123
+ const session = { id, csrf: '', createdAt: device.createdAt, lastSeenAt: this.now(), absoluteExpiresAt: device.expiresAt };
124
+ this.sessions.set(id, session);
125
+ this.sessionDevices.set(id, id);
126
+ if (mutation)
127
+ this.consumeRate(`mutation:${id}`, 120, 60000);
128
+ return session;
129
+ }
103
130
  this.validateBoundary(request, mutation);
131
+ if (request.headers.origin === this.appOrigin && !this.allowedOrigins.has(this.appOrigin))
132
+ throw new FleetError('unauthorized', 'workspace bearer credential required');
104
133
  const id = parseCookies(request.headers.cookie ?? '').ofs_session;
105
134
  const session = id ? this.sessions.get(id) : undefined;
106
135
  const now = this.now();
@@ -121,7 +150,9 @@ export class WebAuth {
121
150
  logout(request) {
122
151
  const session = this.authenticate(request, true);
123
152
  const deviceId = this.sessionDevices.get(session.id);
124
- if (deviceId && deviceId !== 'unprotected')
153
+ if (session.id.startsWith('workspace:'))
154
+ this.workspaceDevices?.revoke(session.id.slice(10));
155
+ else if (deviceId && deviceId !== 'unprotected')
125
156
  this.devices.revokeId(deviceId);
126
157
  this.removeSession(session.id);
127
158
  }
@@ -143,7 +174,7 @@ export class WebAuth {
143
174
  || ticket.roleId !== roleId)
144
175
  throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
145
176
  const session = this.sessions.get(ticket.sessionId);
146
- if (!session)
177
+ if (!session || (session.id.startsWith('workspace:') && !this.workspaceDevices?.valid(session.id.slice(10))))
147
178
  throw new FleetError('unauthorized', 'browser session expired');
148
179
  return session;
149
180
  }
@@ -154,9 +185,30 @@ export class WebAuth {
154
185
  this.sockets.set(sessionId, sockets);
155
186
  }
156
187
  sockets.add(socket);
157
- socket.once('close', () => sockets?.delete(socket));
188
+ const expiry = setInterval(() => { const session = this.sessions.get(sessionId); if (!session || session.absoluteExpiresAt <= this.now() || (sessionId.startsWith('workspace:') && !this.workspaceDevices?.valid(sessionId.slice(10))))
189
+ this.removeSession(sessionId); }, 1000);
190
+ expiry.unref();
191
+ socket.once('close', () => { clearInterval(expiry); sockets?.delete(socket); });
192
+ }
193
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
194
+ bindTransport(sessionId, close) {
195
+ let transports = this.transports.get(sessionId);
196
+ if (!transports) {
197
+ transports = new Set();
198
+ this.transports.set(sessionId, transports);
199
+ }
200
+ transports.add(close);
201
+ const expiry = setInterval(() => { const session = this.sessions.get(sessionId); if (!session || session.absoluteExpiresAt <= this.now() || (sessionId.startsWith('workspace:') && !this.workspaceDevices?.valid(sessionId.slice(10))))
202
+ this.removeSession(sessionId); }, 1000);
203
+ expiry.unref();
204
+ return () => { clearInterval(expiry); transports.delete(close); if (!transports.size)
205
+ this.transports.delete(sessionId); };
158
206
  }
159
207
  clearSessions() {
208
+ for (const transports of this.transports.values())
209
+ for (const close of transports)
210
+ close();
211
+ this.transports.clear();
160
212
  for (const sockets of this.sockets.values())
161
213
  for (const socket of sockets)
162
214
  socket.close(4401, 'authentication revoked');
@@ -167,10 +219,29 @@ export class WebAuth {
167
219
  }
168
220
  revokeAllTrustedDevices() {
169
221
  const count = this.devices.revokeAll();
222
+ for (const device of this.workspaceDevices?.list() ?? [])
223
+ this.workspaceDevices?.revoke(device.id);
170
224
  this.clearSessions();
171
225
  return count;
172
226
  }
173
- shutdown() { this.clearSessions(); }
227
+ workspaceIdentity() { return { workspaceId: this.requireWorkspaceDevices().workspaceId }; }
228
+ workspaceEnrollment(issuer = null) { return this.requireWorkspaceDevices().mint(issuer?.startsWith('workspace:') ? issuer.slice(10) : null); }
229
+ enrollWorkspace(request, input) {
230
+ this.validateBoundary(request, true);
231
+ this.consumeRate('workspace-enrollment', 30, 60000);
232
+ return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label);
233
+ }
234
+ listWorkspaceDevices() { return this.requireWorkspaceDevices().list(); }
235
+ revokeWorkspaceDevice(id) {
236
+ this.requireWorkspaceDevices().revoke(id);
237
+ this.removeSession(`workspace:${id}`);
238
+ }
239
+ requireWorkspaceDevices() {
240
+ if (!this.workspaceDevices)
241
+ throw new FleetError('capability_unavailable', 'workspace device access is not configured');
242
+ return this.workspaceDevices;
243
+ }
244
+ shutdown() { this.clearSessions(); this.workspaceDevices?.close(); }
174
245
  createSession(deviceId) {
175
246
  const now = this.now();
176
247
  const session = {
@@ -182,6 +253,9 @@ export class WebAuth {
182
253
  return session;
183
254
  }
184
255
  removeSession(id) {
256
+ for (const close of this.transports.get(id) ?? [])
257
+ close();
258
+ this.transports.delete(id);
185
259
  this.sessions.delete(id);
186
260
  this.sessionDevices.delete(id);
187
261
  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>;
@@ -1,9 +1,10 @@
1
+ import { socketPath as privateSocketPath } from '../socket-path.js';
1
2
  import { chmodSync, mkdirSync, rmSync } from 'node:fs';
2
3
  import { createConnection, createServer } from 'node:net';
3
4
  import { join } from 'node:path';
4
5
  import { FleetError, safeLine } from '../application/errors.js';
5
6
  import { stateRoot } from '../paths.js';
6
- export const webControlPath = (dir = join(stateRoot(), 'web')) => join(dir, 'control.sock');
7
+ export const webControlPath = (dir = join(stateRoot(), 'web')) => privateSocketPath(join(dir, 'control.sock'));
7
8
  export async function startWebControlServer(options) {
8
9
  const dir = options.dir ?? join(stateRoot(), 'web');
9
10
  const path = webControlPath(dir);
@@ -34,13 +35,14 @@ export async function startWebControlServer(options) {
34
35
  if (used > rateLimit)
35
36
  throw new FleetError('rate_limited', 'local web control rate limit exceeded');
36
37
  const parsed = JSON.parse(body.slice(0, body.indexOf('\n')));
37
- if (parsed.command === 'open')
38
- await options.onOpen();
38
+ let result;
39
+ if (parsed.command === 'link-device' && options.onLinkDevice)
40
+ result = await options.onLinkDevice();
39
41
  else if (parsed.command === 'revoke-all')
40
42
  await options.onRevokeAll();
41
43
  else
42
44
  throw new FleetError('invalid_request', 'unknown local web control command');
43
- socket.end(JSON.stringify({ ok: true }) + '\n');
45
+ socket.end(JSON.stringify({ ok: true, result }) + '\n');
44
46
  }
45
47
  catch (error) {
46
48
  const code = error instanceof FleetError ? error.code : 'internal';
@@ -65,7 +67,7 @@ export async function startWebControlServer(options) {
65
67
  };
66
68
  }
67
69
  export async function requestWebControl(command, path = webControlPath(), timeoutMs = 5_000) {
68
- await new Promise((resolve, reject) => {
70
+ return await new Promise((resolve, reject) => {
69
71
  const socket = createConnection(path);
70
72
  let response = '';
71
73
  let settled = false;
@@ -73,7 +75,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
73
75
  socket.destroy();
74
76
  reject(new FleetError('timeout', 'local web control request timed out'));
75
77
  }, timeoutMs);
76
- const finish = (error) => {
78
+ const finish = (error, result) => {
77
79
  if (settled)
78
80
  return;
79
81
  settled = true;
@@ -81,7 +83,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
81
83
  if (error)
82
84
  reject(error);
83
85
  else
84
- resolve();
86
+ resolve(result);
85
87
  };
86
88
  socket.once('connect', () => socket.write(JSON.stringify({ command }) + '\n'));
87
89
  socket.on('data', chunk => { response += chunk.toString('utf8'); });
@@ -91,7 +93,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
91
93
  const parsed = JSON.parse(response);
92
94
  if (!parsed.ok)
93
95
  throw new FleetError(parsed.error?.code === 'rate_limited' ? 'rate_limited' : 'rejected', parsed.error?.message ?? 'local web control request was rejected');
94
- finish();
96
+ finish(undefined, parsed.result);
95
97
  }
96
98
  catch (error) {
97
99
  finish(error);
@@ -4,6 +4,9 @@ export interface EditableFleetModel {
4
4
  manifest: Record<string, unknown>;
5
5
  agents: Record<string, Record<string, unknown>>;
6
6
  agent_templates: Record<string, Record<string, unknown>>;
7
+ roles?: Record<string, Record<string, unknown>>;
8
+ brains?: Record<string, Record<string, unknown>>;
9
+ room_templates?: Record<string, Record<string, unknown>>;
7
10
  }
8
11
  export interface ConfigReadResult {
9
12
  path: string;
@@ -45,7 +48,7 @@ export declare class FleetConfigService {
45
48
  private readonly preflight;
46
49
  private readonly beforeMutation?;
47
50
  constructor(options?: FleetConfigServiceOptions);
48
- read(): ConfigReadResult;
51
+ read(includeDefinitions?: boolean): ConfigReadResult;
49
52
  preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
50
53
  write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
51
54
  }
@@ -23,7 +23,7 @@ export class FleetConfigService {
23
23
  this.preflight = options.preflight ?? (async () => emptyReport());
24
24
  this.beforeMutation = options.beforeMutation;
25
25
  }
26
- read() {
26
+ read(includeDefinitions = false) {
27
27
  const sources = readSources(this.path);
28
28
  try {
29
29
  loadConfig(this.path, { yamlMode: 'strict' });
@@ -31,7 +31,7 @@ export class FleetConfigService {
31
31
  catch (error) {
32
32
  throw safeError(error, sourceSensitiveValues(sources));
33
33
  }
34
- const raw = modelFrom(sources);
34
+ const raw = modelFrom(sources, includeDefinitions);
35
35
  const redacted = redactModel(raw);
36
36
  return {
37
37
  path: basename(this.path), exists: true, firstRun: false,
@@ -41,7 +41,7 @@ export class FleetConfigService {
41
41
  async preview(baseRevision, input) {
42
42
  const current = readSources(this.path);
43
43
  assertRevision(baseRevision, current);
44
- const currentModel = modelFrom(current);
44
+ const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
45
45
  const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
46
46
  const proposal = renderProposal(current, next);
47
47
  const staged = stageProposal(this.path, current, proposal);
@@ -63,7 +63,7 @@ export class FleetConfigService {
63
63
  return withFileLock(`${splitRootFor(this.path)}.web-edit.lock`, async () => {
64
64
  const current = readSources(this.path);
65
65
  assertRevision(baseRevision, current);
66
- const currentModel = modelFrom(current);
66
+ const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
67
67
  const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
68
68
  const proposal = renderProposal(current, next);
69
69
  const changed = changedDocuments(current, proposal);
@@ -105,6 +105,8 @@ export class FleetConfigService {
105
105
  rmSync(target, { force: true });
106
106
  else {
107
107
  assertWritableTarget(target, current.documents.has(rel));
108
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
109
+ trustedDirectory(dirname(target));
108
110
  replaceFileAtomically(target, contents, 0o600);
109
111
  chmodSync(target, 0o600);
110
112
  }
@@ -184,7 +186,7 @@ function readSources(manifest) {
184
186
  }
185
187
  return { manifest, root, documents };
186
188
  }
187
- function modelFrom(sources) {
189
+ function modelFrom(sources, includeDefinitions = false) {
188
190
  const manifestKey = basename(sources.manifest);
189
191
  const manifest = parseFleetDocument(sources.manifest, sources.documents.get(manifestKey), 'strict').value;
190
192
  const agents = {};
@@ -199,15 +201,24 @@ function modelFrom(sources) {
199
201
  const id = basename(rel, extname(rel));
200
202
  agent_templates[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
201
203
  }
202
- return { manifest, agents, agent_templates };
204
+ const model = { manifest, agents, agent_templates };
205
+ if (includeDefinitions)
206
+ for (const kind of ['roles', 'brains', 'room_templates']) {
207
+ model[kind] = {};
208
+ for (const [rel, source] of sources.documents)
209
+ if (rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel))) {
210
+ model[kind][basename(rel, extname(rel))] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
211
+ }
212
+ }
213
+ return model;
203
214
  }
204
215
  function assertModel(value) {
205
216
  if (!value || typeof value !== 'object' || Array.isArray(value))
206
217
  throw new FleetError('invalid_request', 'model must be an object');
207
218
  const input = value;
208
- if (Object.keys(input).sort().join(',') !== 'agent_templates,agents,manifest')
209
- throw new FleetError('invalid_request', 'model allows exactly manifest, agents, and agent_templates');
210
- for (const key of ['manifest', 'agents', 'agent_templates'])
219
+ if (!['agent_templates,agents,manifest', 'agent_templates,agents,brains,manifest,roles,room_templates'].includes(Object.keys(input).sort().join(',')))
220
+ throw new FleetError('invalid_request', 'model requires manifest, agents, agent_templates and optionally all definition collections');
221
+ for (const key of Object.keys(input))
211
222
  if (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))
212
223
  throw new FleetError('invalid_request', `${key} must be an object`);
213
224
  for (const [id, doc] of Object.entries(input.agents)) {
@@ -222,6 +233,14 @@ function assertModel(value) {
222
233
  if (!doc || typeof doc !== 'object' || Array.isArray(doc))
223
234
  throw new FleetError('invalid_request', `Agent Template '${id}' must be an object`);
224
235
  }
236
+ for (const kind of ['roles', 'brains', 'room_templates']) {
237
+ for (const [id, doc] of Object.entries((input[kind] ?? {}))) {
238
+ if (!ROLE_NAME_RE.test(id))
239
+ throw new FleetError('invalid_request', `invalid definition ID '${id}'`);
240
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
241
+ throw new FleetError('invalid_request', `definition '${id}' must be an object`);
242
+ }
243
+ }
225
244
  return structuredClone(input);
226
245
  }
227
246
  function renderProposal(current, model) {
@@ -240,9 +259,21 @@ function renderProposal(current, model) {
240
259
  next.set(oldRel ?? join('agent_templates', `${id}.yaml`), oldRel
241
260
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
242
261
  }
243
- for (const [rel, source] of current.documents)
244
- if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
245
- next.set(rel, source);
262
+ for (const kind of ['roles', 'brains', 'room_templates']) {
263
+ if (model[kind] === undefined) {
264
+ for (const [rel, source] of current.documents)
265
+ if (rel.startsWith(kind + '/'))
266
+ next.set(rel, source);
267
+ continue;
268
+ }
269
+ for (const [rel, source] of current.documents)
270
+ if (rel.startsWith(kind + '/') && !['.yaml', '.yml'].includes(extname(rel)))
271
+ next.set(rel, source);
272
+ for (const [id, doc] of Object.entries(model[kind])) {
273
+ const oldRel = [...current.documents.keys()].find(rel => rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel)) && basename(rel, extname(rel)) === id);
274
+ next.set(oldRel ?? join(kind, `${id}.yaml`), oldRel ? render(current.documents.get(oldRel), doc) : stringify(doc));
275
+ }
276
+ }
246
277
  return next;
247
278
  }
248
279
  function render(source, model) {
@@ -395,9 +426,9 @@ function sourceDiff(before, after, label, secrets) {
395
426
  ...left.slice(left.length - tail, left.length - tail + trailing).map(x => ` ${x}`), ''].join('\n');
396
427
  }
397
428
  function proposalDiff(current, proposal) {
398
- const secrets = sensitiveValues(modelFrom(current));
429
+ const secrets = sensitiveValues(modelFrom(current, true));
399
430
  try {
400
- secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal })));
431
+ secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal }, true)));
401
432
  }
402
433
  catch { /* validation reports later */ }
403
434
  return [...new Set([...current.documents.keys(), ...proposal.keys()])].sort().map(rel => sourceDiff(current.documents.get(rel) ?? '', proposal.get(rel) ?? '', rel, secrets)).filter(Boolean).join('\n');
@@ -405,7 +436,7 @@ function proposalDiff(current, proposal) {
405
436
  function splitLines(source) { return source.replace(/\n$/, '').split('\n'); }
406
437
  function restartImpact(before, after) {
407
438
  const roles = [...new Set([...Object.keys(before.agents), ...Object.keys(after.agents)])]
408
- .filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id])).sort();
439
+ .filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id]) || JSON.stringify(before.roles) !== JSON.stringify(after.roles) || JSON.stringify(before.brains) !== JSON.stringify(after.brains)).sort();
409
440
  const watchdogScheduler = JSON.stringify(before.manifest.watchdogs) !== JSON.stringify(after.manifest.watchdogs);
410
441
  const scheduledLoops = JSON.stringify(before.manifest.loops) !== JSON.stringify(after.manifest.loops);
411
442
  const required = roles.length > 0 || watchdogScheduler || scheduledLoops;
@@ -0,0 +1,29 @@
1
+ import { type IncomingMessage } from 'node:http';
2
+ import type { WebAuth } from './auth.js';
3
+ /** Each entry exposes one dedicated backend listener. Endpoint dispatch stays upstream. */
4
+ export interface ServiceTarget {
5
+ prefix: string;
6
+ origin: string;
7
+ upstreamPrefix?: string;
8
+ /** Server-side service credentials; never taken from browser headers. */
9
+ headers?: Record<string, string>;
10
+ /** Dedicated credential-authenticated services reject direct browser context. */
11
+ stripBrowserContext?: boolean;
12
+ /**
13
+ * Service paths (below the prefix) where the account App may open a WebSocket with its device
14
+ * credential. A browser cannot set Authorization on a WebSocket, so only there it is accepted
15
+ * as a subprotocol.
16
+ */
17
+ deviceSocketPaths?: string[];
18
+ }
19
+ export interface GatewayOptions {
20
+ /** Backend mode is for a separate loopback machine-client listener; browsers are rejected. */
21
+ auth: WebAuth | 'backend';
22
+ fleetOrigin: string;
23
+ services: ServiceTarget[];
24
+ }
25
+ /** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
26
+ export declare function createPrefixGateway(options: GatewayOptions): {
27
+ server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
28
+ close(): Promise<void>;
29
+ };