@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.50

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 (330) hide show
  1. package/README.md +263 -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-removal-service.js +2 -0
  25. package/dist/application/role-repository.js +2 -2
  26. package/dist/application/room-layout-definitions.d.ts +45 -0
  27. package/dist/application/room-layout-definitions.js +130 -0
  28. package/dist/application/session-control.d.ts +30 -0
  29. package/dist/application/session-control.js +72 -3
  30. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  31. package/dist/application/supervisor-ours-tools.js +113 -0
  32. package/dist/application/task-layouts.d.ts +91 -0
  33. package/dist/application/task-layouts.js +249 -0
  34. package/dist/application/task-room-service.d.ts +99 -3
  35. package/dist/application/task-room-service.js +296 -31
  36. package/dist/atomic-file.d.ts +5 -0
  37. package/dist/atomic-file.js +62 -1
  38. package/dist/briefing.d.ts +2 -0
  39. package/dist/briefing.js +8 -1
  40. package/dist/build-info.json +7 -4
  41. package/dist/capabilities.d.ts +6 -1
  42. package/dist/capabilities.js +8 -0
  43. package/dist/cli.js +376 -34
  44. package/dist/client-profile.d.ts +5 -17
  45. package/dist/client-profile.js +4 -127
  46. package/dist/config.d.ts +28 -0
  47. package/dist/config.js +41 -6
  48. package/dist/creation.js +2 -2
  49. package/dist/daemon-recovery.d.ts +1 -8
  50. package/dist/daemon-recovery.js +8 -113
  51. package/dist/docs.d.ts +3 -3
  52. package/dist/docs.js +126 -22
  53. package/dist/doctor.d.ts +1 -1
  54. package/dist/doctor.js +55 -60
  55. package/dist/erased-resources.d.ts +9 -0
  56. package/dist/erased-resources.js +87 -0
  57. package/dist/fleet-command-audit.d.ts +7 -0
  58. package/dist/fleet-command-audit.js +26 -9
  59. package/dist/harness/claude-code.js +9 -1
  60. package/dist/harness/claude-runtime.d.ts +12 -0
  61. package/dist/harness/claude-runtime.js +43 -0
  62. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  63. package/dist/harness/codex-app-server-proxy.js +41 -5
  64. package/dist/harness/codex-reasoning.d.ts +5 -0
  65. package/dist/harness/codex-reasoning.js +12 -0
  66. package/dist/harness/codex-runtime.d.ts +11 -1
  67. package/dist/harness/codex-runtime.js +31 -4
  68. package/dist/harness/codex.d.ts +3 -2
  69. package/dist/harness/codex.js +20 -11
  70. package/dist/init-wizard.d.ts +2 -0
  71. package/dist/init-wizard.js +38 -19
  72. package/dist/isolation/policy.js +2 -0
  73. package/dist/isolation/types.d.ts +5 -0
  74. package/dist/managed-cli.d.ts +253 -0
  75. package/dist/managed-cli.js +923 -0
  76. package/dist/minimal-setup.d.ts +2 -0
  77. package/dist/minimal-setup.js +44 -0
  78. package/dist/monitor.d.ts +1 -34
  79. package/dist/monitor.js +24 -151
  80. package/dist/notifications/fleet-producer.d.ts +19 -0
  81. package/dist/notifications/fleet-producer.js +208 -0
  82. package/dist/notifications/outbox.d.ts +46 -0
  83. package/dist/notifications/outbox.js +158 -0
  84. package/dist/notifications/target-cleanup.d.ts +8 -0
  85. package/dist/notifications/target-cleanup.js +41 -0
  86. package/dist/ops.js +5 -1
  87. package/dist/owner-channel/channel.d.ts +3 -0
  88. package/dist/owner-channel/channel.js +34 -6
  89. package/dist/owner-channel/commands.d.ts +4 -0
  90. package/dist/owner-channel/commands.js +8 -0
  91. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  92. package/dist/owner-channel/ours-client.js +2 -17
  93. package/dist/paths.d.ts +2 -0
  94. package/dist/paths.js +2 -0
  95. package/dist/preset-migration.js +25 -0
  96. package/dist/resolved-plan.js +1 -0
  97. package/dist/rooms-tasks/add-member.d.ts +48 -0
  98. package/dist/rooms-tasks/add-member.js +83 -0
  99. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  100. package/dist/rooms-tasks/archived-absence.js +48 -0
  101. package/dist/rooms-tasks/cli.js +34 -5
  102. package/dist/rooms-tasks/close.d.ts +5 -2
  103. package/dist/rooms-tasks/close.js +208 -28
  104. package/dist/rooms-tasks/config.d.ts +1 -1
  105. package/dist/rooms-tasks/config.js +11 -4
  106. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  107. package/dist/rooms-tasks/cowork-adapter.js +8 -121
  108. package/dist/rooms-tasks/deletion.d.ts +1 -1
  109. package/dist/rooms-tasks/deletion.js +74 -5
  110. package/dist/rooms-tasks/erasure.d.ts +6 -0
  111. package/dist/rooms-tasks/erasure.js +169 -0
  112. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  113. package/dist/rooms-tasks/external-worker.js +3 -4
  114. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  115. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  116. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  117. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  118. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  119. package/dist/rooms-tasks/layout-cli.js +107 -0
  120. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  121. package/dist/rooms-tasks/layout-config.js +89 -0
  122. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  123. package/dist/rooms-tasks/layout-control.js +52 -0
  124. package/dist/rooms-tasks/layout-service.d.ts +42 -0
  125. package/dist/rooms-tasks/layout-service.js +185 -0
  126. package/dist/rooms-tasks/layout.d.ts +114 -0
  127. package/dist/rooms-tasks/layout.js +250 -0
  128. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  129. package/dist/rooms-tasks/live-readiness.js +85 -0
  130. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  131. package/dist/rooms-tasks/member-startup.js +2 -0
  132. package/dist/rooms-tasks/provision.d.ts +14 -0
  133. package/dist/rooms-tasks/provision.js +173 -46
  134. package/dist/rooms-tasks/room-state.d.ts +4 -2
  135. package/dist/rooms-tasks/room-state.js +53 -8
  136. package/dist/rooms-tasks/task-state.d.ts +42 -7
  137. package/dist/rooms-tasks/task-state.js +153 -23
  138. package/dist/rooms-tasks/types.d.ts +28 -0
  139. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  140. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  141. package/dist/rooms-tasks/workspace.d.ts +26 -0
  142. package/dist/rooms-tasks/workspace.js +195 -0
  143. package/dist/runner.d.ts +19 -3
  144. package/dist/runner.js +128 -45
  145. package/dist/session/acp.d.ts +9 -1
  146. package/dist/session/acp.js +31 -6
  147. package/dist/session/arbiter.d.ts +5 -0
  148. package/dist/session/arbiter.js +28 -2
  149. package/dist/session/codex-app-server.d.ts +5 -0
  150. package/dist/session/codex-app-server.js +36 -3
  151. package/dist/session/control.d.ts +12 -1
  152. package/dist/session/control.js +68 -3
  153. package/dist/session/conversation-normalizer.d.ts +2 -1
  154. package/dist/session/conversation-normalizer.js +1 -1
  155. package/dist/session/conversation-store.d.ts +1 -1
  156. package/dist/session/conversation-store.js +7 -11
  157. package/dist/session/conversation-types.d.ts +7 -3
  158. package/dist/session/task-notice.d.ts +14 -0
  159. package/dist/session/task-notice.js +13 -0
  160. package/dist/session/types.d.ts +6 -0
  161. package/dist/setup-tunnel-preflight.d.ts +34 -0
  162. package/dist/setup-tunnel-preflight.js +62 -0
  163. package/dist/socket-path.d.ts +2 -0
  164. package/dist/socket-path.js +22 -0
  165. package/dist/spawn.js +8 -1
  166. package/dist/startup-readiness.d.ts +13 -0
  167. package/dist/startup-readiness.js +38 -0
  168. package/dist/subscriptions/cli.d.ts +91 -0
  169. package/dist/subscriptions/cli.js +223 -0
  170. package/dist/subscriptions/launch.d.ts +37 -0
  171. package/dist/subscriptions/launch.js +93 -0
  172. package/dist/subscriptions/login.d.ts +75 -0
  173. package/dist/subscriptions/login.js +182 -0
  174. package/dist/subscriptions/observed.d.ts +7 -0
  175. package/dist/subscriptions/observed.js +44 -0
  176. package/dist/subscriptions/probe.d.ts +10 -0
  177. package/dist/subscriptions/probe.js +73 -0
  178. package/dist/subscriptions/service.d.ts +102 -0
  179. package/dist/subscriptions/service.js +364 -0
  180. package/dist/subscriptions/store.d.ts +71 -0
  181. package/dist/subscriptions/store.js +140 -0
  182. package/dist/supervisor/launchd.js +2 -1
  183. package/dist/supervisor/systemd.js +9 -23
  184. package/dist/temp-idle.d.ts +17 -0
  185. package/dist/temp-idle.js +42 -0
  186. package/dist/temp-lifecycle.d.ts +6 -1
  187. package/dist/temp-lifecycle.js +29 -13
  188. package/dist/tunnel-setup-completion.d.ts +7 -0
  189. package/dist/tunnel-setup-completion.js +13 -0
  190. package/dist/tunnel-setup-input.d.ts +19 -0
  191. package/dist/tunnel-setup-input.js +124 -0
  192. package/dist/web/agent-attachments.d.ts +29 -0
  193. package/dist/web/agent-attachments.js +162 -0
  194. package/dist/web/auth.d.ts +29 -1
  195. package/dist/web/auth.js +82 -7
  196. package/dist/web/available-port.d.ts +2 -0
  197. package/dist/web/available-port.js +23 -0
  198. package/dist/web/control.d.ts +3 -3
  199. package/dist/web/control.js +10 -8
  200. package/dist/web/fleet-config-service.d.ts +4 -1
  201. package/dist/web/fleet-config-service.js +46 -15
  202. package/dist/web/prefix-gateway.d.ts +32 -0
  203. package/dist/web/prefix-gateway.js +234 -0
  204. package/dist/web/runtime.d.ts +3 -1
  205. package/dist/web/runtime.js +116 -31
  206. package/dist/web/server.d.ts +16 -0
  207. package/dist/web/server.js +588 -53
  208. package/dist/web/service.d.ts +11 -2
  209. package/dist/web/service.js +30 -4
  210. package/dist/web/workspace-devices.d.ts +36 -0
  211. package/dist/web/workspace-devices.js +101 -0
  212. package/dist/web/workspace-tunnel.d.ts +1 -0
  213. package/dist/web/workspace-tunnel.js +34 -0
  214. package/dist/workspace-enrollment.d.ts +82 -0
  215. package/dist/workspace-enrollment.js +566 -0
  216. package/dist/workspace-replacement.d.ts +66 -0
  217. package/dist/workspace-replacement.js +220 -0
  218. package/docs/live-voice.md +13 -0
  219. package/docs/room-layouts.md +227 -0
  220. package/docs/task-workspaces.md +130 -0
  221. package/docs/validation/managed-cli-permissions.md +421 -0
  222. package/examples/room-layouts/fresh.yaml +27 -0
  223. package/examples/room-layouts/scoped.yaml +21 -0
  224. package/examples/room-layouts/shared.yaml +19 -0
  225. package/package.json +15 -18
  226. package/presets/README.md +78 -0
  227. package/presets/brain-catalog.json +8 -2
  228. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  229. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  230. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  231. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
  232. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  233. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  234. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  235. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  236. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  237. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  238. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  239. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  240. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  241. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  242. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  243. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  244. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  245. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  246. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  247. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  248. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  249. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  250. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  251. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  252. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  253. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  254. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  255. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  256. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  257. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  258. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  259. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  260. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  261. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  262. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  263. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  264. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  265. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  266. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  267. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  268. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  269. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  270. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  271. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  272. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  273. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  274. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  275. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  276. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  277. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  278. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  279. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  280. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  281. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  282. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  283. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  284. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  285. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  286. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  287. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  288. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  289. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  290. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  291. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  292. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  293. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  294. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  295. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  296. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  297. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  298. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  299. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  300. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  301. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  302. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  303. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  304. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  305. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  306. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  307. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  308. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  309. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  310. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  311. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  312. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  313. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  314. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  315. package/presets/fleet/roles/Coordinator.yaml +34 -11
  316. package/presets/fleet/roles/Engineer.yaml +40 -0
  317. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  318. package/presets/fleet/room_layouts/pair.yaml +24 -0
  319. package/presets/fleet/room_layouts/single.yaml +17 -0
  320. package/presets/fleet/room_layouts/team.yaml +30 -0
  321. package/presets/fleet/room_templates/engineering.yaml +19 -0
  322. package/presets/manifest.json +2 -2
  323. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  324. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  325. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  326. package/dist/web-app/icons/ours-fleet.svg +0 -4
  327. package/dist/web-app/index.html +0 -17
  328. package/dist/web-app/manifest.webmanifest +0 -15
  329. package/dist/web-app/offline.html +0 -18
  330. package/dist/web-app/sw.js +0 -51
@@ -0,0 +1,220 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { existsSync, lstatSync, readFileSync, rmSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { validateAccountOrigin } from './account-origin.js';
6
+ import { replaceFileAtomically } from './atomic-file.js';
7
+ import { stateRoot } from './paths.js';
8
+ import { decodeWorkspacePayload, enrollWorkspace, unregisterWorkspace, checkWorkspaceConnector } from './workspace-enrollment.js';
9
+ import { WorkspaceDeviceStore } from './web/workspace-devices.js';
10
+ import { webControlPath } from './web/control.js';
11
+ import { WebServiceManager } from './web/service.js';
12
+ const workspaceDir = () => join(stateRoot(), 'workspace');
13
+ const replacementPath = () => join(workspaceDir(), 'replacement.json');
14
+ const opaque = /^[\w-]{43}$/;
15
+ function binding(value) {
16
+ if (!value || !opaque.test(value.workspaceId) || !opaque.test(value.hostWorkspaceId) || (value.serverCid !== undefined && !/^[a-f0-9]{64}$/i.test(value.serverCid))
17
+ || (value.proofRootCid !== undefined && !/^[a-f0-9]{64}$/i.test(value.proofRootCid)))
18
+ throw Error('Existing workspace binding is invalid; restore it before tunnel setup');
19
+ return { ...value, appOrigin: validateAccountOrigin(value.appOrigin ?? 'https://app.ours.network') };
20
+ }
21
+ export function readWorkspaceBinding() {
22
+ const file = join(workspaceDir(), 'binding.json');
23
+ if (!existsSync(file))
24
+ return;
25
+ let value;
26
+ try {
27
+ value = JSON.parse(readFileSync(file, 'utf8'));
28
+ }
29
+ catch {
30
+ throw Error('Existing workspace binding is invalid');
31
+ }
32
+ return binding(value);
33
+ }
34
+ export function sameWorkspaceBinding(previous, payload) {
35
+ return previous.workspaceId === payload.challenge.workspaceId && previous.appOrigin === payload.appOrigin && (!previous.serverCid || previous.serverCid.toUpperCase() === payload.serverCid.toUpperCase());
36
+ }
37
+ /** A workspace ID is never retired to rerun or migrate that same workspace. */
38
+ export function requiresWorkspaceReplacement(previous, target, migrateAppOrigin = false) {
39
+ if (previous.workspaceId !== target.workspaceId) {
40
+ if (!previous.serverCid)
41
+ throw Error('Legacy registration has no recorded server identity. Rerun its own workspace setup before replacing it');
42
+ return true;
43
+ }
44
+ if (previous.serverCid && previous.serverCid.toUpperCase() !== target.serverCid.toUpperCase())
45
+ throw Error('Same-workspace setup must retain the enrollment server identity; existing setup preserved');
46
+ if (previous.appOrigin !== target.appOrigin && !migrateAppOrigin)
47
+ throw Error('Account origin change requires explicit --migrate-app-origin; existing setup preserved');
48
+ return false;
49
+ }
50
+ /** Ask before redeeming the one-use grant, whose destination is deliberately opaque. */
51
+ export async function confirmWorkspaceReplacement(previous, explicit = false, effects = {}) {
52
+ const write = effects.write ?? (value => { process.stderr.write(value); });
53
+ let hostname = '';
54
+ try {
55
+ const tunnel = JSON.parse(readFileSync(join(workspaceDir(), 'tunnel.json'), 'utf8'));
56
+ if (typeof tunnel.origin === 'string' && /^https:\/\/[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(tunnel.origin))
57
+ hostname = ` (${new URL(tunnel.origin).hostname})`;
58
+ }
59
+ catch { }
60
+ write(`This host is registered to workspace ${previous.workspaceId}${hostname} at ${previous.appOrigin}.\nReplacing it removes the old account registration and revokes its linked devices. The account service retires tunnel resources it knows; a migrated database may have no old resources to delete. Installation identities, agents and data are preserved.\n`);
61
+ if (explicit)
62
+ return true;
63
+ if (!(effects.interactive ?? process.stdin.isTTY)) {
64
+ write('Existing setup preserved. Run in a terminal to confirm, or pass --replace-registration to confirm explicitly.\n');
65
+ return false;
66
+ }
67
+ const ask = effects.ask ?? (async () => {
68
+ const terminal = createInterface({ input: process.stdin, output: process.stderr });
69
+ try {
70
+ return await terminal.question('Replace the existing registration if this command names another workspace? [y/N] ');
71
+ }
72
+ finally {
73
+ terminal.close();
74
+ }
75
+ });
76
+ let answer = '';
77
+ try {
78
+ answer = await ask();
79
+ }
80
+ catch { /* EOF, cancellation and input failure mean no consent. */ }
81
+ if (/^(y|yes)$/i.test(answer.trim()))
82
+ return true;
83
+ write('Existing setup preserved.\n');
84
+ return false;
85
+ }
86
+ export function saveWorkspaceReplacement(record) {
87
+ replaceFileAtomically(replacementPath(), JSON.stringify(record) + '\n', 0o600);
88
+ }
89
+ export function readWorkspaceReplacement() {
90
+ const file = replacementPath();
91
+ if (!existsSync(file))
92
+ return;
93
+ const stat = lstatSync(file);
94
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || stat.size > 65536)
95
+ throw Error('Unfinished replacement must be an owned private regular file');
96
+ let record;
97
+ try {
98
+ record = JSON.parse(readFileSync(file, 'utf8'));
99
+ }
100
+ catch {
101
+ throw Error('Unfinished replacement record is invalid');
102
+ }
103
+ if (![1, 2].includes(record.version) || !opaque.test(record.operationNonce) || typeof record.configuration !== 'string' || resolve(record.configuration) !== record.configuration
104
+ || (record.rootCid !== undefined && !/^[a-f0-9]{64}$/i.test(record.rootCid)) || (record.retired !== undefined && typeof record.retired !== 'boolean')
105
+ || (record.cleaned !== undefined && typeof record.cleaned !== 'boolean') || (record.dispatchAttempted !== undefined && typeof record.dispatchAttempted !== 'boolean') || (record.version === 2 && (record.dispatchAttempted === undefined || (!Number.isSafeInteger(record.operationExpiresAt) || record.operationExpiresAt <= 0 || record.operationExpiresAt > Date.now() + 86400000 + 60000))) || (record.cleaned && !record.retired) || (record.retired && !record.rootCid))
106
+ throw Error('Unfinished replacement record is invalid');
107
+ record.previous = binding(record.previous);
108
+ if (!record.previous.serverCid)
109
+ throw Error('Unfinished replacement has no pinned server identity');
110
+ record.payload = decodeWorkspacePayload(Buffer.from(JSON.stringify(record.payload)).toString('base64url'), { allowExpired: true });
111
+ return record;
112
+ }
113
+ export function clearWorkspaceReplacement() { rmSync(replacementPath(), { force: true }); }
114
+ /** Explicit recovery after the user removes an unsuccessful successor in the App. */
115
+ export function discardRetiredWorkspaceReplacement() {
116
+ const record = readWorkspaceReplacement(), current = readWorkspaceBinding();
117
+ if (!record?.retired || !record.cleaned)
118
+ throw Error('Only a fully retired and locally cleaned replacement can be discarded; resume or safely abandon this operation');
119
+ if (!current || current.workspaceId !== record.previous.workspaceId || current.hostWorkspaceId !== record.previous.hostWorkspaceId || current.appOrigin !== record.previous.appOrigin || current.serverCid?.toUpperCase() !== record.previous.serverCid.toUpperCase())
120
+ throw Error('The successor binding was committed or the binding changed; resume its setup instead of discarding recovery');
121
+ clearWorkspaceReplacement();
122
+ }
123
+ function checkReplacementService(configuration) {
124
+ const manager = new WebServiceManager(), service = manager.readMetadata();
125
+ if (service?.configuration && resolve(service.configuration) !== resolve(configuration))
126
+ throw Error('Installed web service uses another configuration; existing setup preserved');
127
+ if (!service && existsSync(webControlPath()))
128
+ throw Error('Stop the foreground Fleet web service before replacing its registration; existing setup preserved');
129
+ }
130
+ export async function beginWorkspaceReplacement(previous, payload, configuration, effects = {}) {
131
+ checkWorkspaceConnector(payload);
132
+ if (!requiresWorkspaceReplacement(previous, { workspaceId: payload.challenge.workspaceId, appOrigin: payload.appOrigin, serverCid: payload.serverCid }))
133
+ throw Error('The same workspace must never be retired');
134
+ checkReplacementService(configuration);
135
+ const record = { version: 2, previous: previous, payload, configuration: resolve(configuration), operationNonce: randomBytes(32).toString('base64url'), dispatchAttempted: false, operationExpiresAt: Date.now() + 86400000 - 5 * 60000 };
136
+ record.rootCid = await (effects.preflight ?? unregisterWorkspace)(record.previous, payload, record.operationNonce, effects.attach, { checkOnly: true, operationExpiresAt: record.operationExpiresAt });
137
+ saveWorkspaceReplacement(record);
138
+ return record;
139
+ }
140
+ /** Abandon only after a private receipt confirms no deletion and dispatch was unsent or terminally rejected. */
141
+ export async function abandonWorkspaceReplacement(effects = {}) {
142
+ const record = readWorkspaceReplacement();
143
+ if (!record)
144
+ throw Error('There is no unfinished replacement');
145
+ if (record.retired || record.cleaned || record.version !== 2)
146
+ throw Error('Retirement may already have completed; resume this replacement instead of abandoning it');
147
+ const current = readWorkspaceBinding();
148
+ if (!current || current.workspaceId !== record.previous.workspaceId || current.hostWorkspaceId !== record.previous.hostWorkspaceId || current.appOrigin !== record.previous.appOrigin || current.serverCid?.toUpperCase() !== record.previous.serverCid.toUpperCase())
149
+ throw Error('Workspace binding changed; replacement was not abandoned');
150
+ let deleted, rejected = false, expired = false, deadlineEnforced = false, oldBindingPresent = false;
151
+ const root = await (effects.inspect ?? unregisterWorkspace)(record.previous, record.payload, record.operationNonce, effects.attach, { checkOnly: true, operationExpiresAt: record.operationExpiresAt, onReceipt: status => { deleted = status.deleted; rejected = status.rejected === true; expired = status.expired === true; deadlineEnforced = status.deadlineEnforced === true; oldBindingPresent = status.oldBindingPresent === true; } });
152
+ if (root.toUpperCase() !== record.rootCid?.toUpperCase() || deleted !== false || (record.dispatchAttempted && !(rejected && !expired && Date.now() <= record.operationExpiresAt || oldBindingPresent && deadlineEnforced && Date.now() > record.operationExpiresAt + 5 * 60000)))
153
+ throw Error('Retirement may already have been sent; run --resume or abandon only after the account service confirms terminal rejection');
154
+ clearWorkspaceReplacement();
155
+ }
156
+ /** A failed/expired one-time command can be renewed only for its pinned successor. */
157
+ export function refreshWorkspaceReplacement(record, payload, configuration) {
158
+ if (resolve(configuration) !== record.configuration)
159
+ throw Error('Replacement requires its original configuration');
160
+ if (payload.challenge.workspaceId !== record.payload.challenge.workspaceId || payload.challenge.accountId !== record.payload.challenge.accountId
161
+ || payload.appOrigin !== record.payload.appOrigin || payload.serverCid.toUpperCase() !== record.payload.serverCid.toUpperCase()
162
+ || payload.hostname !== record.payload.hostname)
163
+ throw Error('Unfinished replacement requires a fresh command for the same target workspace');
164
+ record.payload = payload.connectorToken === undefined && record.payload.connectorToken !== undefined ? { ...payload, connectorToken: record.payload.connectorToken } : payload;
165
+ saveWorkspaceReplacement(record);
166
+ }
167
+ /** Only tunnel files and account-issued device capabilities belong to this registration. */
168
+ export async function clearReplacedWorkspaceLocalState() {
169
+ const manager = new WebServiceManager();
170
+ if (manager.readMetadata())
171
+ await manager.stop();
172
+ const devices = new WorkspaceDeviceStore();
173
+ try {
174
+ devices.revokeAll();
175
+ }
176
+ finally {
177
+ devices.close();
178
+ }
179
+ for (const file of ['tunnel.json', 'connector', 'pending-setup.json'])
180
+ rmSync(join(workspaceDir(), file), { force: true });
181
+ }
182
+ /** Persist every irreversible boundary; the original binding stays until enrollment writes its successor. */
183
+ export async function finishWorkspaceReplacement(record, effects = {}) {
184
+ const current = readWorkspaceBinding();
185
+ if (!current || (current.workspaceId === record.previous.workspaceId ? (current.appOrigin !== record.previous.appOrigin || current.serverCid?.toUpperCase() !== record.previous.serverCid.toUpperCase() || current.hostWorkspaceId !== record.previous.hostWorkspaceId) : !sameWorkspaceBinding(current, record.payload)))
186
+ throw Error('Workspace binding changed during replacement; review it before continuing');
187
+ // A saved successor binding may predate a refreshed challenge. Enrollment
188
+ // checks the exact proof receipt: it reuses confirmed proof, or signs the new
189
+ // challenge, rather than assuming every challenge for this binding is done.
190
+ if (!record.retired) {
191
+ checkReplacementService(record.configuration);
192
+ if (record.version === 2 && Date.now() > record.operationExpiresAt + 5 * 60000) {
193
+ if (record.payload.challenge.expiresAt <= Date.now())
194
+ throw Error('Retirement deadline passed. Obtain a fresh App command for the same target and pass --replace-registration to continue');
195
+ let deleted, enforced = false;
196
+ const root = await (effects.inspect ?? unregisterWorkspace)(record.previous, record.payload, record.operationNonce, effects.attach, { checkOnly: true, operationExpiresAt: record.operationExpiresAt, onReceipt: status => { deleted = status.deleted; enforced = status.deadlineEnforced === true; } });
197
+ if (root.toUpperCase() !== record.rootCid?.toUpperCase())
198
+ throw Error('Original proof root changed; existing recovery preserved');
199
+ if (deleted === false && enforced) {
200
+ // The expired command cannot act. Existing consent covers this exact
201
+ // previous/successor pair, including signed absence after a lost receipt.
202
+ record.operationNonce = randomBytes(32).toString('base64url');
203
+ record.operationExpiresAt = Date.now() + 86400000 - 5 * 60000;
204
+ record.dispatchAttempted = false;
205
+ saveWorkspaceReplacement(record);
206
+ }
207
+ }
208
+ record.rootCid = await (effects.unregister ?? unregisterWorkspace)(record.previous, record.payload, record.operationNonce, effects.attach, { operationExpiresAt: record.operationExpiresAt, beforeSend: () => { record.dispatchAttempted = true; saveWorkspaceReplacement(record); } });
209
+ record.retired = true;
210
+ saveWorkspaceReplacement(record);
211
+ }
212
+ if (!record.cleaned) {
213
+ await (effects.cleanup ?? clearReplacedWorkspaceLocalState)();
214
+ record.cleaned = true;
215
+ saveWorkspaceReplacement(record);
216
+ }
217
+ if (record.payload.challenge.expiresAt <= Date.now())
218
+ throw Error('Old registration was retired, but the replacement setup window expired. Get a fresh App command and pass --replace-registration; the retirement will not be repeated');
219
+ return (effects.enroll ?? enrollWorkspace)(record.payload, record.configuration, { preserveProfile: true, replacingWorkspace: record.previous.workspaceId, attach: effects.attach });
220
+ }
@@ -0,0 +1,13 @@
1
+ # Live voice managed-session boundary
2
+
3
+ App Live voice sends transcripts through existing ACP managed sessions and reads the actual agent conversation. Updated ACP and native Codex supervisors advertise `generation_bound_prompts`, `idle_bound_prompts` and `targeted_voice_interrupt`; `/api/v1/roles/:id/voice-capabilities` authenticates the browser and reports whether all are present.
4
+
5
+ `submit_voice_prompt` accepts `requireIdle: true`. Receipt replay precedes the idle check so retrying a lost acknowledgment does not create a second prompt or fail just because the first is running. New admission checks generation and idle/queue/tool/permission state inside the existing turn arbiter before queuing. Unrelated work is never interrupted to make room.
6
+
7
+ `interrupt_voice_v2` requires a command ID, expected session generation and exact prompt ID. A matching active prompt uses normal managed-session cancellation; a settled or different prompt is a harmless no-op. A stale generation is rejected. Receipts are idempotent and conflicting reuse of an interrupt command is rejected. The browser's `/interrupt` route selects this path only when the complete target is present, with no legacy unbound fallback.
8
+
9
+ Authenticated CSRF-protected `POST /api/v1/roles/:id/live-presence` takes `{ expectedSessionGeneration }`, validates the current generation before attaching `followConversation`, and streams NDJSON readiness/heartbeats. The HTTP connection owns one attended controller until disconnect. Close, generation change, auth expiry, slow consumer or attach failure detach it. The browser's ordinary workspace fetch supports this stream without provider credentials in a URL. Existing ACP permission expiry and controller detach grace remain in force.
10
+
11
+ The daemon/App changes and configuration instructions live in the corresponding `ours-app/docs/live-voice.md` and `ours-sdk` Live voice documentation. The daemon owns the OpenAI key and 60-second Realtime credential minting; Fleet never receives the provider key.
12
+
13
+ Coverage includes actual ACP/control-socket busy/retry/targeted-cancel tests, native Codex transport tests, authenticated presence-boundary tests and the App native browser/audio integration with separate Codex/Claude ACP protocol fixtures. Provider inference and physical-device acceptance are separate operator checks.
@@ -0,0 +1,227 @@
1
+ # YAML Room Layouts
2
+
3
+ Room Layout describes room goals, participants and their shared or independent
4
+ sessions. Rooms and agents have independent lifecycles.
5
+
6
+ Place a layout at `fleet/room_layouts/<name>.yaml` beside the split configuration
7
+ for `fleet.yaml`. Agent Templates remain in `fleet/agent_templates/`.
8
+
9
+ ```yaml
10
+ version: 1
11
+ participants:
12
+ product: {agent_template: Product}
13
+ developer: {agent_template: Developer}
14
+ qa: {agent_template: QA}
15
+ rooms:
16
+ discovery:
17
+ goal: Clarify the problem and acceptance criteria
18
+ members: [product, developer, qa]
19
+ design:
20
+ goal: Review implementation choices
21
+ members: [developer, qa]
22
+ amigos:
23
+ goal: Resolve a concrete acceptance question
24
+ members: [product, developer, qa]
25
+ roles: {product: product, developer: developer, qa: qa}
26
+ ```
27
+
28
+ Participant `instance_scope` controls instance reuse:
29
+
30
+ - `layout` (default): repeating a participant key reuses its exact running instance
31
+ and session within this layout run.
32
+ - `room`: each room using the key gets its own identity and session, lazily created
33
+ when that room opens. Repeated opens of the same active room reuse that instance.
34
+
35
+ For example, one shared process doctor and one independent critic per room:
36
+
37
+ ```yaml
38
+ version: 1
39
+ participants:
40
+ doctor: {agent_template: ProcessDoctor, instance_scope: layout}
41
+ critic: {agent_template: Critic, instance_scope: room}
42
+ rooms:
43
+ discovery:
44
+ goal: Agree on the problem
45
+ members: [doctor, critic]
46
+ design:
47
+ goal: Review the design
48
+ members: [doctor, critic]
49
+ ```
50
+
51
+ Different keys using the same Agent Template still create independent instances.
52
+ See the [shared](../examples/room-layouts/shared.yaml) and
53
+ [fresh](../examples/room-layouts/fresh.yaml) examples, and the mixed
54
+ [scoped](../examples/room-layouts/scoped.yaml) example. All choices can coexist.
55
+ Fresh agents receive their room context. Supply relevant evidence through ordinary
56
+ room messages or existing project documents when needed.
57
+
58
+ A participant without `agent_template` requires a binding. `layout instance`
59
+ returns a local exact reference (supervisor, agent, launch, CID and session).
60
+ Pass participant-to-reference YAML to `create --bindings`. A stopped or replaced instance is rejected. Cross-Fleet bindings require
61
+ an explicit owner grant, as described below.
62
+ Room-scoped participants require `agent_template`; an explicit binding for them
63
+ is rejected before creating resources. Bindings for layout-scoped participants
64
+ keep their existing behavior.
65
+
66
+ ```sh
67
+ ours-fleet layout validate work
68
+ ours-fleet layout create work --id example
69
+ ours-fleet layout open example design
70
+ ours-fleet layout open example discovery
71
+ ours-fleet layout open example amigos
72
+ ours-fleet layout close-room example amigos
73
+ ours-fleet layout status example
74
+ ours-fleet layout close example
75
+ ```
76
+
77
+ Any declared room may open first. `create` only saves the definition and resolved
78
+ Agent Templates; `open` lazily starts missing participants and admits all members
79
+ before sending their room context. The membership notification is informational. Existing room rules govern when
80
+ an agent contributes. Each room opens through an explicit command.
81
+
82
+ `close-room` archives a room and keeps agents available to other rooms. `close`
83
+ archives opened rooms and stops only temporary participants created by this layout
84
+ instance. Explicitly bound participants survive. Cleanup processes existing resources. A 3-Amigos room is an ordinary room with
85
+ complementary members.
86
+ Room-scoped instances follow the same cleanup rules: closing a room does not
87
+ retire them, and a closed room cannot reopen. A new layout run creates new instances.
88
+
89
+ Private state keeps the authorable definition unchanged. Its `participants` map
90
+ uses the original key for shared instances and `<room>:<participant>` for
91
+ room-scoped instances, with `participant` and `room` metadata. Declared but unopened
92
+ rooms have no running instance. Status and task read models distinguish these
93
+ entries and resolve their original Agent Template. Existing snapshots without
94
+ `instance_scope` retain their original keys and require no migration. Snapshots
95
+ using `room` require a Fleet version that supports this field; they are not a
96
+ downgrade format for older Fleet versions.
97
+
98
+ Supported room fields: `goal`, `members`, optional `roles`, `contract`,
99
+ `quiet_membership`, `anonymous`. `contract` is briefing text.
100
+ Top-level fields: `version: 1`, optional `description`, `participants`, `rooms`.
101
+ Participant fields: optional `agent_template`, `instance_scope: layout|room`.
102
+ Unknown fields, ambiguous YAML and invalid member references are rejected.
103
+ Existing templates and tasks keep their behavior.
104
+
105
+ Local Fleet control uses the existing authenticated control socket,
106
+ agent runtime, `spawnTemp` and session queue. Cross-Fleet bindings delegate
107
+ membership operations to their owning Fleet as described below. Private state
108
+ snapshots template definitions; public status hides their environment values.
109
+ Existing owner settings apply; owner CID/invite fingerprint are pinned. Unknown
110
+ mutation outcomes stop new work for inspection rather than blindly replaying a
111
+ spawn or room creation. `close` and `close-room` can still clean known resources.
112
+ Cleanup continues after individual errors and does not stop replacement or borrowed
113
+ instances. Confirmed stopped workers need no live control socket. An unknown
114
+ original outcome remains recorded, and cleanup reports incomplete until inspected;
115
+ it never claims that unknown resources were found or removed. Retrying cleanup
116
+ checks known resources and skips completed cleanup. Reopening an active layout room
117
+ checks its native state, CID and required seats and reports drift without repairing it.
118
+
119
+
120
+ Shared sessions retain available conversation context, not guaranteed perfect
121
+ recall or isolation between rooms. Compaction and model judgment still matter.
122
+ Rooms keep their existing turn scheduling.
123
+
124
+ Each layout instance creates distinct native room names, so several instances can
125
+ open the same declared room key. Participant roles must differ from the attached
126
+ owner role; a collision is rejected before creating resources.
127
+
128
+ Definitions are loaded for authoring commands. Retained instances use their saved
129
+ layout and Agent Templates. Status and cleanup operate on saved resources even
130
+ when the source YAML is broken or removed. New agent launches continue to use
131
+ Fleet's normal configuration and launch checks.
132
+
133
+ ## Borrowing an instance from another Fleet on the same host
134
+
135
+ Fleet A can open rooms using standalone temporary or persistent agents supervised
136
+ by Fleet B under another OS user. Both must select the same daemon instance and
137
+ Cowork gateway. An agent's original Fleet keeps its session, identity and lifecycle
138
+ control. This extends layout bindings; it does not add bindings to legacy
139
+ `room create` / RoomTemplate provisioning.
140
+
141
+ On the agent owner's Fleet B, use its existing web server and grant access to an
142
+ exact live standalone instance:
143
+
144
+ ```sh
145
+ ours-fleet web serve --port 49272 --no-open
146
+ # In another terminal belonging to Fleet B's owner:
147
+ ours-fleet layout share Architect --temporary --participant architect \
148
+ --server-url http://127.0.0.1:49272 --output ./architect.binding.yaml
149
+ ```
150
+
151
+ The command prints the grant ID and output path. It creates a binding YAML and
152
+ an adjacent `.token` file, both owner-only. The private owner record stores a
153
+ SHA-256 token digest, not the token. `share` never overwrites an existing export.
154
+ Transfer both files explicitly to Fleet A's user through your existing secure
155
+ file-transfer method, keeping the receiving credential file owned by that user
156
+ with mode `0600`. Do not share Fleet B's console session, client profile, daemon
157
+ credential, private state directory or supervisor socket. This feature creates
158
+ no additional server; Fleet B's existing web server must remain available.
159
+
160
+ Export each specialist under its logical layout participant key, then merge the
161
+ YAML mappings into one bindings document. Keep every adjacent token filename
162
+ unique. Relative credential paths resolve against the bindings file's directory.
163
+ The reusable layout contains participant factories or empty participant mappings,
164
+ never concrete credentials or running-instance references:
165
+
166
+ ```yaml
167
+ version: 1
168
+ participants:
169
+ architect: {}
170
+ developer: {}
171
+ doctor: {}
172
+ rooms:
173
+ product: {goal: Scope, members: [architect, doctor]}
174
+ design: {goal: Design, members: [architect, developer, doctor]}
175
+ delivery: {goal: Delivery, members: [developer, doctor]}
176
+ ```
177
+
178
+ On Fleet A:
179
+
180
+ ```sh
181
+ ours-fleet layout create project --id project-run --bindings ./bindings.yaml
182
+ ours-fleet layout open project-run product
183
+ ours-fleet layout open project-run design
184
+ ours-fleet layout open project-run delivery
185
+ ours-fleet layout close-room project-run product
186
+ ours-fleet layout close project-run
187
+ ```
188
+
189
+ The exact CID, launch and harness session are reused. Reopening an active room
190
+ checks existing membership without delivering duplicate context. Closing rooms
191
+ archives Cowork history and never stops borrowed instances. After acceptance,
192
+ the original Fleet B owner explicitly retires its standalone temporary specialists
193
+ using the existing lifecycle commands. Persistent specialists keep their ordinary
194
+ lifecycle. Mixed local-created and borrowed participants are supported.
195
+
196
+ An exported reference adds `remote` to the existing instance reference:
197
+
198
+ ```yaml
199
+ remote:
200
+ url: http://127.0.0.1:49272
201
+ grant_id: 11111111-1111-4111-8111-111111111111
202
+ credential_file: architect.binding.yaml.token
203
+ daemon_instance_id: 22222222-2222-4222-8222-222222222222
204
+ ```
205
+
206
+ The grant authorizes only `verify`, `join`, and `assign` for that exact instance.
207
+ Room admission validates the requested room ID and CID against shared Cowork and
208
+ uses an invite issued by Cowork on the owner side; caller-supplied contact invites
209
+ are rejected before contact mutation. Context delivery also requires its active seat, expected room CID and role in
210
+ Cowork. It cannot inspect inventory, spawn, stop, reconfigure, export identities,
211
+ or authenticate browser console routes. It grants the holder room admission and
212
+ room-context delivery, so transfer it only to the intended room creator.
213
+
214
+ Owner-side revocation does not stop the agent or remove existing memberships:
215
+
216
+ ```sh
217
+ ours-fleet layout revoke-binding <grant-id>
218
+ ```
219
+
220
+ Stopped/replaced sessions, a different daemon, revoked grants or unavailable
221
+ owner transport produce errors; no replacement is spawned. A lost response to a
222
+ membership/context mutation retains the existing layout `uncertain` marker.
223
+ Inspect the native room and owning Fleet before retrying; no mutation is blindly
224
+ replayed. Only direct loopback HTTP origins (`127.0.0.1`) are supported;
225
+ redirects, external hosts and forwarded requests are rejected. No cross-host
226
+ support, discovery protocol, mission service, or room conversation limits are
227
+ introduced.
@@ -0,0 +1,130 @@
1
+ # Task workspaces and artifact retention
2
+
3
+ New Tasks allocate a private workspace at
4
+ `$OURS_FLEET_HOME/.ours-fleet/workspaces/tasks/<task-id>` (the user's home is the
5
+ default for `OURS_FLEET_HOME`). New standalone Rooms use
6
+ `.ours-fleet/workspaces/rooms/<room-id>`. A linked Room shares its Task's workspace.
7
+ The absolute path, owner kind/ID and random ownership token are stored in the
8
+ record before directory allocation; `.fleet-workspace.json` proves ownership.
9
+ The path is visible in Task/Room JSON from both CLI and API. A folder of the same
10
+ name without the matching marker is never adopted.
11
+
12
+ Every newly managed member starts at that exact workspace, including retries.
13
+ An explicit template/member `cwd` remains in the sealed input execution plan as
14
+ source context, but the owned workspace overrides the effective launch cwd.
15
+ Copy or clone required source into the workspace; Fleet does not move or delete
16
+ the configured source directory. Briefings explain this rule. Use separate
17
+ per-agent branches/directories inside the workspace for concurrent edits.
18
+
19
+ All execution artifacts, dependency checkouts and worktrees belong inside the
20
+ workspace. Clone each repository there first, then use `git worktree add` with
21
+ destinations inside the same workspace. Git's native worktree registry is the
22
+ registry of record. Fleet audits `.git`, `commondir` and reverse `gitdir` links
23
+ before deletion; both the common repository and all registered worktrees must
24
+ be inside the workspace. An external repository's worktree inside the workspace,
25
+ or an internal repository's worktree outside it, blocks deletion. No broad
26
+ `git worktree prune`, branch deletion, reset or modification of unrelated Git
27
+ metadata is performed. Removing the owned repositories also removes their
28
+ complete internal worktree registries, including dirty and stale owned entries.
29
+
30
+ | Operation | Workspace and execution artifacts | Member runtime evidence |
31
+ | --- | --- | --- |
32
+ | Complete, fail, cancel | Retained while Task is recorded | Retained in Fleet recovery archives |
33
+ | Linked Room retirement or deletion | Task workspace retained | Retained |
34
+ | Internal standalone Room retirement | Room workspace retained | Retained |
35
+ | Explicit Task deletion | Removed after member retirement | All proven owned retired launches removed |
36
+ | Explicit standalone Room deletion | Removed after member retirement | All proven owned retired launches removed |
37
+
38
+ Internal room close means retirement, not destructive Task deletion.
39
+ The existing CLI `room close ID ID` remains a deprecated alias for explicit
40
+ `room delete ID ID`: it deletes standalone room-owned artifacts, but always
41
+ retains a linked Task workspace. Its repeated-ID confirmation is unchanged. To remove a Task's
42
+ artifacts, explicitly delete the Task. Cancellation is not deletion. Automatic
43
+ compatibility cleanup of legacy closed Rooms skips records with owned workspaces.
44
+ These choices reconcile close/remove wording with the requirement that completed,
45
+ recorded Tasks retain all artifacts.
46
+
47
+ Fleet's active member state (briefings, logs, session state and WORKLOG) remains
48
+ under `.ours-fleet/tmp` while supervisors run, then in
49
+ `.ours-fleet/recovery/temporary`. The launch stores the workspace descriptor.
50
+ Explicit deletion first proves managed agents retired, then identifies all
51
+ matching archived launches, including failed and replaced attempts, using the
52
+ workspace descriptor plus role and termination/launch evidence. It preflights
53
+ these archives and atomically transfers them to `.fleet-retired-agents` in the
54
+ workspace before removal. Unrelated archives are not adopted by a name prefix.
55
+ Explicit deletion also erases legacy archives with exact launch/action or room
56
+ provenance, matching private supervisor journals and admission/launch descriptors,
57
+ and the exact retired-launch rows in the global termination journal. It never
58
+ infers ownership from an Agent's shared `cwd`. Artifact removal uses a durable
59
+ manifest, ownership fingerprints and per-entry rename/removal checkpoints, so
60
+ an interrupted deletion can resume without deleting a replacement at the old path.
61
+ Task deletion acceptance receipts exist only during cleanup and are erased after
62
+ settlement (including retry after a crash between Task unlink and receipt cleanup).
63
+ Shared Fleet command audit and lifecycle delivery ledgers have exactly attributed
64
+ target labels, configuration and command arguments removed; unrelated entries are
65
+ preserved. Delivery/deduplication metadata and a content-free erased-resource ID
66
+ registry remain so a running writer cannot resurrect erased content. Infrastructure
67
+ logs without structured ownership, backups, and copies already delivered to other
68
+ identities are external retention boundaries; this is not a claim to erase every
69
+ external copy.
70
+
71
+ Provisioning serializes Task operation then Room close locks, including bounded
72
+ seat waits, so concurrent retries cannot publish competing members and deletion
73
+ cannot race a member launch. Local published Rooms are reused if the Task link
74
+ write was interrupted. Workspace allocation publishes a marked staging directory;
75
+ restart reuses the durable token. Deletion renames the validated workspace to a
76
+ token-specific sibling tombstone before removal. A crash during removal resumes
77
+ that exact tombstone; the Task record is unlinked last. Before collecting archives,
78
+ Task deletion durably checkpoints verified retirement evidence. Retries verify
79
+ any remaining original archive and still refuse replacement live member state
80
+ or a recreated identity even after the archive has been consumed. Failed deletion
81
+ remains retryable with its recorded error.
82
+
83
+ Deletion rejects traversal, foreign recorded paths, changed markers and symlinked
84
+ workspace ancestors. Ordinary artifact symlinks are unlinked without following
85
+ their targets. Git control symlinks and foreign pointers are refused. As with
86
+ Fleet's other filesystem lifecycle operations, a hostile same-UID process that
87
+ mutates paths during validation requires OS isolation; application checks are
88
+ not a security boundary against that process. Fleet enforces launch cwd and
89
+ safe deletion; unrestricted agents can still write outside cwd, so the artifact
90
+ placement rule also requires agent compliance and/or filesystem isolation.
91
+
92
+ ## Compatibility and limits
93
+
94
+ Records from before this feature without `workspace` remain unmanaged. Existing
95
+ agents retain their cwd and exact briefing/fingerprint so restart matching works.
96
+ Fleet never claims an old cwd, an outcome artifact path, or a legacy recovery
97
+ archive as owned merely because it was mentioned by a Task. Create a new managed
98
+ Task and explicitly copy desired artifacts to migrate; deleting a legacy Task
99
+ continues its previous lifecycle and does not delete arbitrary old files.
100
+
101
+ Changing `OURS_FLEET_HOME` does not silently rebase recorded absolute ownership
102
+ paths. Restore the original state root or explicitly migrate records and files
103
+ with the supervisors stopped; mismatched paths fail closed. Archive transfer
104
+ uses same-filesystem rename and reports a recoverable failure across mounts.
105
+
106
+ Cowork's remote create operation has no idempotency key in this interface. A crash
107
+ after remote creation but before local Room publication is an existing uncertain
108
+ remote result; this change deduplicates locally recorded rooms and does not claim
109
+ to solve that remote transaction window. Inspect remote room ownership before
110
+ retrying ambiguous remote creation. No services or installed Fleet code are
111
+ changed by building or testing this feature.
112
+
113
+
114
+ ## Broken provisioning and deletion recovery
115
+
116
+ Deletion accepts every lifecycle state. Missing temporary directories, identities,
117
+ remote rooms and already-erased artifacts are settled outcomes. Retry the same
118
+ explicit delete after an interruption. A missing local member CID is recovered
119
+ only from a matching authenticated seat in the pinned room (exact invite and
120
+ role, uniquely matched), or a supervisor journal tied to the selected daemon,
121
+ temporary instance and recorded creation action. These sources must agree.
122
+ The recovered CID is persisted before retirement consumes the evidence.
123
+
124
+ A contradictory CID, replacement launch, ambiguous seat, unreadable/corrupt
125
+ record, or unreachable authority is not evidence of absence. Cleanup retains its
126
+ cursor and reports the conflict instead of deleting an unrelated resource or
127
+ claiming complete erasure. An identity with no recoverable authenticated binding
128
+ requires restoring its actual ownership evidence; a name-only force removal is
129
+ not supported. Room deletion removes the linked Task's dead room/member links and preserves its workspace; use explicit
130
+ Task deletion when the request includes the associated Task and its artifacts.