@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
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute } from 'node:path';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_ROOM_KEYS as TRK, } from './types.js';
4
5
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
@@ -18,7 +19,7 @@ function rejectUnknown(obj, allowed, path, section) {
18
19
  export function fingerprint(invite) {
19
20
  return createHash('sha256').update(invite.trim()).digest('hex');
20
21
  }
21
- function resolveInvite(owner, vars, path) {
22
+ function resolveInvite(owner, vars, path, deferredInviteFile) {
22
23
  const inline = owner.public_invite;
23
24
  const file = owner.public_invite_file;
24
25
  if (inline && file)
@@ -28,6 +29,10 @@ function resolveInvite(owner, vars, path) {
28
29
  let raw;
29
30
  if (file) {
30
31
  const resolved = file.replace(/\$\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
32
+ // Tunnel setup writes this one file itself after enrollment; until then it may be absent.
33
+ // Only the literal absolute path counts: a relative spelling that happens to reach it does not.
34
+ if (!existsSync(resolved) && deferredInviteFile !== undefined && isAbsolute(deferredInviteFile) && resolved === deferredInviteFile)
35
+ return undefined;
31
36
  if (!existsSync(resolved))
32
37
  throw new RoomsTasksConfigError(path, `rooms.owner.public_invite_file: not found: ${resolved}`);
33
38
  raw = readFileSync(resolved, 'utf8').trim();
@@ -39,7 +44,7 @@ function resolveInvite(owner, vars, path) {
39
44
  throw new RoomsTasksConfigError(path, 'rooms.owner: invite is empty');
40
45
  return { value: raw, fingerprint: fingerprint(raw) };
41
46
  }
42
- export function validateRoomsConfig(raw, vars, path) {
47
+ export function validateRoomsConfig(raw, vars, path, deferredInviteFile) {
43
48
  if (!isPlainObject(raw))
44
49
  throw new RoomsTasksConfigError(path, 'rooms: must be a mapping');
45
50
  // Migration-only input: provider selection was exposed before rooms shipped,
@@ -54,7 +59,9 @@ export function validateRoomsConfig(raw, vars, path) {
54
59
  if (!isPlainObject(raw.cowork))
55
60
  throw new RoomsTasksConfigError(path, 'rooms.cowork: must be a mapping');
56
61
  rejectUnknown(raw.cowork, RCK, path, 'rooms.cowork');
57
- cowork = { config: raw.cowork.config };
62
+ if (raw.cowork.config !== undefined)
63
+ throw new RoomsTasksConfigError(path, 'rooms.cowork.config is no longer supported; remove it and configure ~/.ours-client/profile.json for all services');
64
+ cowork = {};
58
65
  }
59
66
  if (!isPlainObject(raw.owner))
60
67
  throw new RoomsTasksConfigError(path, 'rooms.owner: required and must be a mapping');
@@ -66,7 +73,7 @@ export function validateRoomsConfig(raw, vars, path) {
66
73
  if (!CID_RE.test(expectedCid))
67
74
  throw new RoomsTasksConfigError(path, 'rooms.owner.expected_cid: must be exactly 64 hexadecimal characters');
68
75
  const ownerRole = raw.owner.role ?? 'Owner';
69
- const invite = resolveInvite(raw.owner, vars, path);
76
+ const invite = resolveInvite(raw.owner, vars, path, deferredInviteFile);
70
77
  const owner = {
71
78
  provider: ownerProvider,
72
79
  expected_cid: expectedCid.toLowerCase(),
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Typed client for the ours-cowork v1 private management-socket protocol.
2
+ * Typed client for the ours-cowork v1 gateway management protocol.
3
3
  *
4
4
  * Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
5
- * by Cowork's Unix socket and projects the response into orchestration data.
5
+ * through the shared HTTP gateway and projects the response into orchestration data.
6
6
  */
7
7
  import type { Socket } from 'node:net';
8
8
  import type { RoomHistoryEvidence } from './types.js';
@@ -58,6 +58,10 @@ export interface CoworkAdapter {
58
58
  briefing: string;
59
59
  quiet_membership?: boolean;
60
60
  anonymous?: boolean;
61
+ activation_requirements?: Array<{
62
+ role: string;
63
+ count: number;
64
+ }>;
61
65
  }): Promise<CoworkRoomCreateResult>;
62
66
  acceptInvite(roomId: string, invite: string, opts: {
63
67
  role: string;
@@ -104,6 +108,4 @@ export interface CoworkAdapterOptions {
104
108
  home?: string;
105
109
  connect?: (path: string) => Socket;
106
110
  }
107
- /** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
108
- export declare function resolveCoworkSocketPath(options?: CoworkAdapterOptions): string;
109
111
  export declare function createCoworkAdapter(options?: CoworkAdapterOptions): CoworkAdapter;
@@ -1,20 +1,14 @@
1
1
  /**
2
- * Typed client for the ours-cowork v1 private management-socket protocol.
2
+ * Typed client for the ours-cowork v1 gateway management protocol.
3
3
  *
4
4
  * Cowork owns room state. Fleet only invokes the versioned JSON RPC exposed
5
- * by Cowork's Unix socket and projects the response into orchestration data.
5
+ * through the shared HTTP gateway and projects the response into orchestration data.
6
6
  */
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import { homedir } from 'node:os';
9
- import { join, resolve } from 'node:path';
10
- import { createConnection } from 'node:net';
11
7
  import { readClientProfile } from '../client-profile.js';
12
8
  import { coworkHttpCall } from './cowork-http.js';
13
- const RPC_VERSION = 1;
14
9
  const DEFAULT_TIMEOUT_MS = 30_000;
15
- const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
16
10
  export class CoworkUnavailableError extends Error {
17
- constructor(message = 'Cowork management socket is not reachable', options) {
11
+ constructor(message = 'Cowork gateway management is not reachable', options) {
18
12
  super(message, options);
19
13
  this.name = 'CoworkUnavailableError';
20
14
  }
@@ -171,121 +165,13 @@ function projectHistoryRecord(value) {
171
165
  }
172
166
  return undefined;
173
167
  }
174
- /** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
175
- export function resolveCoworkSocketPath(options = {}) {
176
- if (options.socketPath)
177
- return resolve(options.socketPath);
178
- const env = options.env ?? process.env;
179
- const home = options.home ?? homedir();
180
- const configuredPath = options.configPath ?? env.OURS_COWORK_CONFIG;
181
- const configPath = resolve(configuredPath ?? join(home, '.ours-cowork', 'config.json'));
182
- let stateDir = resolve(home, '.ours-cowork');
183
- if (existsSync(configPath)) {
184
- let parsed;
185
- try {
186
- parsed = JSON.parse(readFileSync(configPath, 'utf8'));
187
- }
188
- catch (error) {
189
- throw new CoworkProtocolError('config', `malformed config at ${configPath}`, 'invalid_config', { cause: error });
190
- }
191
- const config = object(parsed, 'config', 'config');
192
- if (config.version !== 1 || typeof config.stateDir !== 'string' || !config.stateDir)
193
- throw new CoworkProtocolError('config', `invalid Cowork v1 config at ${configPath}`, 'invalid_config');
194
- stateDir = resolve(config.stateDir);
195
- }
196
- else if (configuredPath !== undefined) {
197
- throw new CoworkProtocolError('config', `configured Cowork config does not exist: ${configPath}`, 'invalid_config');
198
- }
199
- if (env.OURS_COWORK_STATE_DIR)
200
- stateDir = resolve(env.OURS_COWORK_STATE_DIR);
201
- return join(stateDir, 'management.sock');
202
- }
203
- function rpcCall(socketPath, method, params, timeoutMs, connect) {
204
- const id = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
205
- const request = `${JSON.stringify({ version: RPC_VERSION, id, method, params })}\n`;
206
- return new Promise((resolveCall, rejectCall) => {
207
- const socket = connect(socketPath);
208
- let bytes = '';
209
- let size = 0;
210
- let settled = false;
211
- let connected = false;
212
- const finishError = (error) => {
213
- if (settled)
214
- return;
215
- settled = true;
216
- clearTimeout(timer);
217
- socket.destroy();
218
- rejectCall(error);
219
- };
220
- const timer = setTimeout(() => finishError(new CoworkUnavailableError(connected ? 'Cowork did not answer the management socket' : 'Cowork management socket is not reachable')), timeoutMs);
221
- socket.setEncoding('utf8');
222
- socket.once('connect', () => {
223
- connected = true;
224
- socket.write(request);
225
- });
226
- socket.on('data', (chunk) => {
227
- if (settled)
228
- return;
229
- const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
230
- size += Buffer.byteLength(text);
231
- if (size > MAX_RESPONSE_BYTES) {
232
- finishError(new CoworkProtocolError(method, 'response exceeded 4 MiB'));
233
- return;
234
- }
235
- bytes += text;
236
- const newline = bytes.indexOf('\n');
237
- if (newline < 0)
238
- return;
239
- if (bytes.slice(newline + 1).trim() !== '') {
240
- finishError(new CoworkProtocolError(method, 'daemon returned more than one response'));
241
- return;
242
- }
243
- let response;
244
- try {
245
- response = object(JSON.parse(bytes.slice(0, newline)), method, 'RPC response');
246
- }
247
- catch (error) {
248
- finishError(error instanceof CoworkProtocolError
249
- ? error : new CoworkProtocolError(method, 'daemon returned malformed JSON', 'protocol_error', { cause: error }));
250
- return;
251
- }
252
- if (response.version !== RPC_VERSION || response.id !== id
253
- || (Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result'))) {
254
- finishError(new CoworkProtocolError(method, 'daemon returned an invalid RPC response'));
255
- return;
256
- }
257
- settled = true;
258
- clearTimeout(timer);
259
- socket.destroy();
260
- if (Object.hasOwn(response, 'error')) {
261
- const error = object(response.error, method, 'RPC error');
262
- rejectCall(new CoworkProtocolError(method, typeof error.message === 'string' ? error.message : 'unknown RPC error', typeof error.code === 'string' ? error.code : 'rpc_error'));
263
- }
264
- else {
265
- resolveCall(response.result);
266
- }
267
- });
268
- socket.once('end', () => {
269
- if (!settled)
270
- finishError(new CoworkProtocolError(method, 'daemon closed without a complete response'));
271
- });
272
- socket.once('error', (error) => {
273
- if (!settled)
274
- finishError(new CoworkUnavailableError(error.code === 'EACCES' || error.code === 'EPERM'
275
- ? 'Cowork management socket access denied' : 'Cowork management socket is not reachable', { cause: error }));
276
- });
277
- });
278
- }
279
168
  export function createCoworkAdapter(options = {}) {
280
169
  const env = options.env ?? process.env;
281
- const explicitLocal = options.socketPath !== undefined || options.configPath !== undefined
282
- || env.OURS_COWORK_CONFIG !== undefined || !!env.OURS_COWORK_STATE_DIR;
283
- const profile = explicitLocal ? undefined : readClientProfile({ ...env, HOME: options.home ?? env.HOME });
284
- const remote = profile?.serverUrl ? profile : undefined;
285
- const socketPath = remote ? undefined : resolveCoworkSocketPath(options);
170
+ if (options.socketPath !== undefined || options.configPath !== undefined)
171
+ throw new CoworkProtocolError('config', 'Local Cowork selectors are unsupported; configure the shared gateway client profile', 'invalid_config');
172
+ const profile = readClientProfile({ ...env, HOME: options.home ?? env.HOME });
286
173
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
287
- const connect = options.connect ?? ((path) => createConnection(path));
288
- const call = (method, params) => remote ? coworkHttpCall(remote, method, params, timeoutMs) : rpcCall(socketPath, method, params, timeoutMs, connect);
174
+ const call = (method, params) => coworkHttpCall(profile, method, params, timeoutMs);
289
175
  return {
290
176
  async available() {
291
177
  try {
@@ -303,6 +189,7 @@ export function createCoworkAdapter(options = {}) {
303
189
  briefing: opts.briefing,
304
190
  ...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
305
191
  ...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
192
+ ...(opts.activation_requirements === undefined ? {} : { activation_requirements: opts.activation_requirements }),
306
193
  }), 'room.create');
307
194
  if (!result.identity_cid)
308
195
  throw new CoworkProtocolError('room.create', 'created room did not establish an identity CID');
@@ -3,7 +3,7 @@ import { type CoworkAdapter } from './cowork-adapter.js';
3
3
  import { type TaskDeletionAcceptance } from './task-state.js';
4
4
  import type { TaskDeletionActor, TaskRecord } from './types.js';
5
5
  export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
6
- type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
6
+ type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
7
7
  export interface TaskDeletionSettleDeps {
8
8
  roomClose?: RoomCloseDeps;
9
9
  /** Test seam for the temp-state existence proof. */
@@ -1,13 +1,17 @@
1
+ import { eraseMemberArtifacts } from './erasure.js';
2
+ import { proveArchivedAbsence, verifyArchivedAbsence, verifyArchivedMemberStillAbsent } from './archived-absence.js';
1
3
  import { existsSync } from 'node:fs';
2
4
  import { withFileLock } from '../atomic-file.js';
3
5
  import { agentDir } from '../paths.js';
6
+ import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
7
+ import { collectWorkspaceArchives } from './workspace-artifacts.js';
4
8
  import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
5
- import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
9
+ import { closeManagedRoom, identityCidPresent, inspectMember, assertMemberIdentityAbsent, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
6
10
  import { CoworkProtocolError } from './cowork-adapter.js';
7
11
  import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
8
12
  import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
9
13
  import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
10
- import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
14
+ import { advanceTaskDeletionMember, settleAbsentTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, beginTaskArchiveCleanup, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
11
15
  export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
12
16
  import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
13
17
  /**
@@ -40,6 +44,7 @@ function tolerantCowork(cowork) {
40
44
  }
41
45
  };
42
46
  return {
47
+ ...(cowork.getRoom ? { getRoom: cowork.getRoom.bind(cowork) } : {}),
43
48
  closeRoom: roomId => tolerate(() => cowork.closeRoom(roomId)),
44
49
  deleteRoom: roomId => tolerate(() => cowork.deleteRoom(roomId)),
45
50
  };
@@ -48,6 +53,7 @@ function cursorSeat(cursor) {
48
53
  return {
49
54
  role_name: cursor.name, identity_cid: cursor.identity_cid,
50
55
  slot: cursor.name, cowork_role: 'member', seat_state: 'active',
56
+ ...(cursor.launch_id ? { retirement: { phase: 'identity_absent', launch_id: cursor.launch_id, archive_path: cursor.archive_path, updated_at: cursor.updated_at } } : {}),
51
57
  };
52
58
  }
53
59
  /**
@@ -60,8 +66,15 @@ function cursorSeat(cursor) {
60
66
  async function retireCursorMember(taskId, cursor, deps) {
61
67
  let phase = cursor.phase;
62
68
  let launchId = cursor.launch_id;
63
- if (phase === 'identity_absent')
69
+ if (phase === 'identity_absent') {
70
+ if (!deps.roomClose?.inspectMember) {
71
+ if ((deps.hasTempState ?? (name => existsSync(agentDir(name, true))))(cursor.name))
72
+ throw new Error('Retired deletion member has replacement live state');
73
+ if (await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid))
74
+ throw new Error('Retired deletion member identity reappeared');
75
+ }
64
76
  return;
77
+ }
65
78
  const seat = cursorSeat(cursor);
66
79
  const roomClose = deps.roomClose ?? {};
67
80
  if (phase === 'pending') {
@@ -85,7 +98,16 @@ async function retireCursorMember(taskId, cursor, deps) {
85
98
  phase = 'stop_requested';
86
99
  launchId = ownership.launchId;
87
100
  }
101
+ if (!roomClose.inspectMember && !existsSync(agentDir(cursor.name, true))) {
102
+ await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
103
+ if (existsSync(agentDir(cursor.name, true)))
104
+ throw new Error('Deletion member acquired replacement state');
105
+ settleAbsentTaskDeletionMember(taskId, cursor.name);
106
+ return;
107
+ }
88
108
  if (phase === 'stop_requested') {
109
+ if (!roomClose.inspectMember && (await inspectMember(seat)).launchId !== launchId)
110
+ throw new Error('Deletion member launch changed before stop');
89
111
  await (roomClose.requestStop
90
112
  ?? (async (role) => { await stopTempSupervisor(role); }))(cursor.name);
91
113
  await (roomClose.waitForLivenessAbsent ?? waitForLivenessAbsent)(cursor.name, launchId);
@@ -145,10 +167,40 @@ export async function settleTaskDeletion(input) {
145
167
  upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
146
168
  await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
147
169
  const closed = getRoomRecord(record.room_id);
148
- importTaskDeletionRetirementEvidence(taskId, closed?.member_seats ?? record.member_seats);
170
+ const seats = closed?.member_seats ?? record.member_seats;
171
+ const proofs = [];
172
+ for (const seat of seats) {
173
+ if (!seat.identity_cid && !seat.retirement?.absence_verified && !['never-launched', 'absent-verified'].includes(seat.retirement?.launch_id ?? '')) {
174
+ const prior = task.deletion.archived_absences?.find(proof => proof.name === seat.role_name);
175
+ if (task.deletion.workspace_cleanup_started_at && prior && !existsSync(prior.archive_path)) {
176
+ await verifyArchivedMemberStillAbsent(prior);
177
+ proofs.push(prior);
178
+ }
179
+ else
180
+ proofs.push(await proveArchivedAbsence(seat));
181
+ }
182
+ }
183
+ // No await between the last live-state fence and durable checkpoint.
184
+ for (const proof of proofs) {
185
+ const latest = getRoomRecord(record.room_id)?.member_seats.find(seat => seat.role_name === proof.name);
186
+ if (existsSync(agentDir(proof.name, true)) || latest?.identity_cid
187
+ || latest?.launch?.launch_id !== proof.launch_id || latest.launch.action_id !== proof.action_id)
188
+ throw new Error('Archived absence seat changed before checkpoint');
189
+ }
190
+ importTaskDeletionRetirementEvidence(taskId, seats, proofs);
149
191
  await cowork.deleteRoom(record.room_id);
192
+ for (const proof of proofs)
193
+ await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
194
+ beginTaskArchiveCleanup(taskId);
195
+ await eraseMemberArtifacts('room', record.room_id, seats, [record.room_id]);
196
+ for (const proof of proofs)
197
+ if (existsSync(agentDir(proof.name, true)))
198
+ throw new Error('Archived member replacement before room unlink');
150
199
  deleteRoomRecord(record.room_id);
151
200
  }
201
+ const deletion = getDeletingTask(taskId).deletion;
202
+ for (const proof of deletion.archived_absences ?? [])
203
+ await (deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
152
204
  // Members whose room record is gone (crash after record deletion, or
153
205
  // legacy state): resume from the durable cursors.
154
206
  for (const cursor of getDeletingTask(taskId).deletion.members) {
@@ -176,7 +228,7 @@ export async function settleTaskDeletion(input) {
176
228
  completeTaskDeletionReceipt(taskId);
177
229
  return { task_id: taskId, deleted: false };
178
230
  }
179
- const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
231
+ const finalize = () => withFileLock(taskOperationLockPath(taskId), async () => {
180
232
  try {
181
233
  const task = getDeletingTask(taskId);
182
234
  if (task.deletion?.status !== 'pending')
@@ -185,8 +237,25 @@ export async function settleTaskDeletion(input) {
185
237
  throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
186
238
  if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
187
239
  throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
240
+ for (const proof of task.deletion.archived_absences ?? [])
241
+ await (task.deletion.workspace_cleanup_started_at && !existsSync(proof.archive_path) ? verifyArchivedMemberStillAbsent : verifyArchivedAbsence)(proof);
242
+ for (const proof of task.deletion.archived_absences ?? [])
243
+ if (existsSync(agentDir(proof.name, true)))
244
+ throw new Error('Archived member replacement before task unlink');
245
+ for (const name of task.deletion.absent_members ?? []) {
246
+ if (existsSync(agentDir(name, true)))
247
+ throw new Error('Absent member acquired replacement state');
248
+ await assertMemberIdentityAbsent({ role_name: name, slot: name, cowork_role: 'member', seat_state: 'removed' });
249
+ }
188
250
  if (cleanup.snapshotHash)
189
251
  releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
252
+ await eraseMemberArtifacts('task', taskId, task.deletion.members.map(cursorSeat), task.deletion.room_id ? [task.deletion.room_id] : []);
253
+ if (task.workspace) {
254
+ assertWorkspaceDeletable(task.workspace, 'task', taskId);
255
+ beginTaskArchiveCleanup(taskId);
256
+ collectWorkspaceArchives(task.workspace);
257
+ deleteWorkspace(task.workspace, 'task', taskId);
258
+ }
190
259
  unlinkDeletedTask(taskId);
191
260
  }
192
261
  catch (error) {
@@ -0,0 +1,6 @@
1
+ import type { RoomMemberSeat } from './types.js';
2
+ /** Erase only proven launch artifacts. Each atomic rename transfers ownership to
3
+ * a random, durable tombstone before recursive removal can destroy its proof.
4
+ * The caller holds the task/room lifecycle lock and has retired all writers.
5
+ */
6
+ export declare function eraseMemberArtifacts(owner: 'room' | 'task', id: string, seats: readonly RoomMemberSeat[], roomIds: readonly string[]): Promise<void>;
@@ -0,0 +1,169 @@
1
+ import { eraseResourcePresentations } from '../erased-resources.js';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { dirname, join, relative } from 'node:path';
5
+ import { parse } from 'yaml';
6
+ import { replaceFileAtomically } from '../atomic-file.js';
7
+ import { agentDir, stateRoot } from '../paths.js';
8
+ import { readTempSupervisor, tempSupervisorLiveness, eraseTerminationEvents } from '../temp-lifecycle.js';
9
+ import { assertSafeAncestors, auditWorkspaceGit } from './workspace.js';
10
+ function entries(path) {
11
+ try {
12
+ return readdirSync(path);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return [];
17
+ throw error;
18
+ }
19
+ }
20
+ function json(path) {
21
+ try {
22
+ if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
23
+ return undefined;
24
+ return JSON.parse(readFileSync(path, 'utf8'));
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT')
28
+ return undefined;
29
+ throw error;
30
+ }
31
+ }
32
+ function fingerprint(path) {
33
+ const stat = lstatSync(path);
34
+ const files = stat.isDirectory() ? ['creation.json', '.temp-supervisor.json', '.identity', 'role.yaml', 'state.json', 'instance.json'] : [''];
35
+ const hash = createHash('sha256');
36
+ for (const file of files) {
37
+ const target = file ? join(path, file) : path;
38
+ if (existsSync(target)) {
39
+ if (!lstatSync(target).isFile() || lstatSync(target).isSymbolicLink())
40
+ throw new Error('Unsafe erasure proof');
41
+ hash.update(file).update(readFileSync(target));
42
+ }
43
+ }
44
+ return hash.digest('hex');
45
+ }
46
+ /** Erase only proven launch artifacts. Each atomic rename transfers ownership to
47
+ * a random, durable tombstone before recursive removal can destroy its proof.
48
+ * The caller holds the task/room lifecycle lock and has retired all writers.
49
+ */
50
+ export async function eraseMemberArtifacts(owner, id, seats, roomIds) {
51
+ if (!/^[a-zA-Z0-9_-]{1,80}$/.test(id))
52
+ throw new Error('Invalid erasure owner');
53
+ const root = stateRoot();
54
+ const manifestPath = join(root, 'erasure', `${owner}-${id}.json`);
55
+ assertSafeAncestors(dirname(manifestPath));
56
+ let manifest = json(manifestPath);
57
+ const names = new Set(seats.map(s => s.role_name));
58
+ for (const name of names) {
59
+ if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
60
+ throw new Error('Invalid erasure member');
61
+ if (existsSync(agentDir(name, true)))
62
+ throw new Error(`Member '${name}' still has live state during erasure`);
63
+ }
64
+ if (!manifest) {
65
+ const paths = [];
66
+ const launches = seats.flatMap(seat => {
67
+ const launchId = seat.launch?.launch_id ?? seat.retirement?.launch_id;
68
+ return launchId ? [{ role: seat.role_name, launchId }] : [];
69
+ });
70
+ const actions = new Map(seats.map(s => [s.role_name, new Set([s.launch?.action_id].filter((v) => !!v))]));
71
+ const recovery = join(root, 'recovery', 'temporary');
72
+ assertSafeAncestors(recovery);
73
+ for (const entry of entries(recovery)) {
74
+ const path = join(recovery, entry);
75
+ const stat = lstatSync(path);
76
+ if (!stat.isDirectory() || stat.isSymbolicLink())
77
+ continue;
78
+ const supervisor = readTempSupervisor(path);
79
+ if (!supervisor || !names.has(supervisor.role))
80
+ continue;
81
+ const seat = seats.find(s => s.role_name === supervisor.role);
82
+ const creation = json(join(path, 'creation.json'));
83
+ let room;
84
+ const roleFile = join(path, 'role.yaml');
85
+ if (existsSync(roleFile) && lstatSync(roleFile).isFile() && !lstatSync(roleFile).isSymbolicLink()) {
86
+ const role = parse(readFileSync(roleFile, 'utf8'));
87
+ if (role?.name === supervisor.role)
88
+ room = role.roomMemberStartup?.room_id;
89
+ }
90
+ const exactLaunch = seat.launch?.launch_id === supervisor.launchId || seat.retirement?.launch_id === supervisor.launchId;
91
+ const exactAction = creation?.role === supervisor.role && creation.creationActionId === seat.launch?.action_id;
92
+ // Older attempts belong only when the launch descriptor pins the room.
93
+ if (!(exactLaunch && (exactAction || !seat.launch?.action_id)) && !(room && roomIds.includes(room)))
94
+ continue;
95
+ if (existsSync(join(path, '.identity')) && readFileSync(join(path, '.identity'), 'utf8').trim() !== supervisor.role)
96
+ throw new Error('Archive identity mismatch during erasure');
97
+ if (await tempSupervisorLiveness(path) !== 'stopped')
98
+ throw new Error('Archive writer is not stopped');
99
+ if (creation?.role === supervisor.role && typeof creation.creationActionId === 'string')
100
+ actions.get(supervisor.role).add(creation.creationActionId);
101
+ auditWorkspaceGit(path);
102
+ launches.push({ role: supervisor.role, launchId: supervisor.launchId });
103
+ paths.push(path);
104
+ }
105
+ const privateRoot = join(root, 'private-ours');
106
+ assertSafeAncestors(privateRoot);
107
+ for (const entry of entries(privateRoot)) {
108
+ const path = join(privateRoot, entry);
109
+ if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink())
110
+ continue;
111
+ if (entry === 'launches' || entry === 'room-inputs') {
112
+ for (const file of entries(path)) {
113
+ const child = join(path, file), value = json(child);
114
+ if (!value)
115
+ continue;
116
+ const launchOwned = entry === 'launches' && value.role === value.identity
117
+ && actions.get(value.role)?.has(value.action);
118
+ const roomOwned = entry === 'room-inputs' && roomIds.includes(value.room_id)
119
+ && names.has(value.identity_name);
120
+ const readyOwned = entry === 'room-inputs' && file.endsWith('.ready.json')
121
+ && roomIds.includes(value.room) && seats.some(seat => seat.invite_id === value.invite && (!seat.identity_cid || seat.identity_cid === value.cid));
122
+ if (launchOwned || roomOwned || readyOwned)
123
+ paths.push(child);
124
+ }
125
+ continue;
126
+ }
127
+ const state = json(join(path, 'state.json')), instance = json(join(path, 'instance.json'));
128
+ if (state?.lifetime === 'temporary' && actions.get(state.name)?.has(state.action)
129
+ && instance?.role === state.name && instance?.temporary === true && instance?.instance === state.instance) {
130
+ auditWorkspaceGit(path);
131
+ paths.push(path);
132
+ }
133
+ }
134
+ manifest = { launches, version: 1, token: randomUUID(), paths, phases: {}, stamps: Object.fromEntries(paths.map(path => {
135
+ const stat = lstatSync(path);
136
+ return [path, { dev: stat.dev, ino: stat.ino, proof: fingerprint(path) }];
137
+ })) };
138
+ mkdirSync(dirname(manifestPath), { recursive: true, mode: 0o700 });
139
+ replaceFileAtomically(manifestPath, JSON.stringify(manifest));
140
+ }
141
+ if (manifest.version !== 1 || !/^[a-f0-9-]{36}$/.test(manifest.token) || !Array.isArray(manifest.paths))
142
+ throw new Error('Invalid erasure manifest');
143
+ for (const path of manifest.paths) {
144
+ const rel = relative(root, path);
145
+ if (!rel.startsWith('recovery/temporary/') && !rel.startsWith('private-ours/'))
146
+ throw new Error('Invalid erasure artifact path');
147
+ if (rel.split('/').includes('..'))
148
+ throw new Error('Invalid erasure artifact traversal');
149
+ assertSafeAncestors(dirname(path));
150
+ const tombstone = `${path}.erasing-${manifest.token}`;
151
+ if (manifest.phases[path] === 'removed')
152
+ continue;
153
+ if (existsSync(path) && !existsSync(tombstone) && manifest.phases[path] !== 'renamed') {
154
+ const stat = lstatSync(path), stamp = manifest.stamps[path];
155
+ if (stat.isSymbolicLink() || !stamp || stamp.dev !== stat.dev || stamp.ino !== stat.ino || stamp.proof !== fingerprint(path))
156
+ throw new Error('Erasure source ownership changed');
157
+ renameSync(path, tombstone);
158
+ }
159
+ manifest.phases[path] = 'renamed';
160
+ replaceFileAtomically(manifestPath, JSON.stringify(manifest));
161
+ rmSync(tombstone, { recursive: true, force: true });
162
+ manifest.phases[path] = 'removed';
163
+ replaceFileAtomically(manifestPath, JSON.stringify(manifest));
164
+ }
165
+ eraseResourcePresentations([{ kind: owner, id }, ...roomIds.map(id => ({ kind: 'room', id })),
166
+ ...seats.map(seat => ({ kind: 'agent', id: seat.role_name }))]);
167
+ eraseTerminationEvents(manifest.launches);
168
+ rmSync(manifestPath, { force: true });
169
+ }
@@ -8,4 +8,4 @@ export declare function presentFleetWorkerLifecycle(presentations: FleetAuditPre
8
8
  sleep(ms: number): Promise<void>;
9
9
  }): Promise<void>;
10
10
  /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
11
- export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
11
+ export declare function launchFleetWorker(args: string[], operation: string, configPath?: string, binPath?: string): Promise<void>;
@@ -9,8 +9,7 @@ const LIFECYCLE_ACCEPT_TIMEOUT_MS = 10 * 60_000;
9
9
  const LIFECYCLE_RETRY_MS = 250;
10
10
  export function fleetWorkerEnv(source = process.env) {
11
11
  const inheritedKeys = [
12
- 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
13
- 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
12
+ 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_FLEET_SOCKET_ROOT', 'OURS_CONFIG',
14
13
  ];
15
14
  const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
16
15
  const lifecycleStateDir = source[FLEET_WORKER_LIFECYCLE_STATE_DIR_ENV]
@@ -52,8 +51,8 @@ export async function presentFleetWorkerLifecycle(presentations, deps = {
52
51
  }
53
52
  }
54
53
  /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
55
- export function launchFleetWorker(args, operation, configPath) {
56
- const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
54
+ export function launchFleetWorker(args, operation, configPath, binPath = process.argv[1]) {
55
+ const cli = [binPath, ...args, ...(configPath ? ['-c', configPath] : [])];
57
56
  const env = fleetWorkerEnv();
58
57
  const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
59
58
  const suffix = randomUUID().slice(0, 8);
@@ -0,0 +1,11 @@
1
+ import type { LayoutInstance } from './layout.js';
2
+ import type { LayoutControlRequest } from './layout-control.js';
3
+ export type LayoutBindingRequest = Omit<LayoutControlRequest, 'instance'> & {
4
+ roomId?: string;
5
+ roomRole?: string;
6
+ };
7
+ export declare const LAYOUT_BINDING_ACTIONS: readonly ["verify", "join", "assign"];
8
+ export declare const localLayoutReference: ({ remote: _remote, ...instance }: LayoutInstance) => LayoutInstance;
9
+ /** Initial support is the same-host, separate-OS-user use case from #194. */
10
+ export declare function layoutBindingOrigin(raw: string): string;
11
+ export declare function callRemoteLayoutBinding(instance: LayoutInstance, request: LayoutBindingRequest, daemonInstanceId: string, fetchImpl?: typeof fetch): Promise<unknown>;