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

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 +592 -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,42 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { replaceFileAtomically } from './atomic-file.js';
4
+ export const TEMP_CHAT_IDLE_MS = 24 * 60 * 60_000;
5
+ const FILE = 'chat-idle.json';
6
+ export function readChatIdle(dir) {
7
+ if (!existsSync(join(dir, FILE)))
8
+ return undefined;
9
+ const value = JSON.parse(readFileSync(join(dir, FILE), 'utf8'));
10
+ if (value.version !== 1 || !Number.isSafeInteger(value.lastActivityAt) || value.timeoutMs !== TEMP_CHAT_IDLE_MS)
11
+ throw new Error('Invalid persisted chat idle policy');
12
+ return value;
13
+ }
14
+ /** Only a standalone temporary supervisor owns this clock; browser reads never touch it. */
15
+ export class TemporaryChatIdle {
16
+ dir;
17
+ value;
18
+ busy = false;
19
+ constructor(dir, now) {
20
+ this.dir = dir;
21
+ this.value = readChatIdle(dir) ?? { version: 1, timeoutMs: TEMP_CHAT_IDLE_MS, lastActivityAt: now };
22
+ this.save();
23
+ }
24
+ save() { replaceFileAtomically(join(this.dir, FILE), JSON.stringify(this.value) + '\n', 0o600); }
25
+ observe(snapshot, queueDepth, now) {
26
+ // Unknown activity evidence never grants permission to retire a session.
27
+ const idle = snapshot.alive && snapshot.readiness === 'idle' && !snapshot.pendingPermissionId && queueDepth === 0 && snapshot.activity?.activeToolCalls === 0;
28
+ const update = Date.parse(snapshot.activity?.lastUpdateAt ?? '');
29
+ let next = this.value.lastActivityAt;
30
+ if (Number.isFinite(update) && update <= now)
31
+ next = Math.max(next, update);
32
+ if (!idle || this.busy)
33
+ next = Math.max(next, now);
34
+ this.busy = !idle;
35
+ // Active work checkpoints at most once a minute, with an immediate idle boundary write.
36
+ if (next > this.value.lastActivityAt && (idle || next - this.value.lastActivityAt >= 60_000)) {
37
+ this.value.lastActivityAt = next;
38
+ this.save();
39
+ }
40
+ return idle && now >= this.value.lastActivityAt && now - this.value.lastActivityAt >= this.value.timeoutMs;
41
+ }
42
+ }
@@ -5,7 +5,7 @@ export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
5
5
  export declare const TEMP_RECLAIM_BATCH = 32;
6
6
  export declare const TEMP_LAUNCH_GRACE_MS = 60000;
7
7
  export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
8
- export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
8
+ export type TempTerminationReason = 'idle-timeout' | 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
9
9
  export interface TempSupervisorRecord {
10
10
  version: 1;
11
11
  role: string;
@@ -44,6 +44,11 @@ export declare function makeTempSupervisorLauncher(options?: {
44
44
  }): SupervisorLauncher;
45
45
  export declare function markTempSupervisorActive(dir: string, pid?: number): Promise<void>;
46
46
  export declare function requestedTempStopReason(dir: string): 'operator-stop' | undefined;
47
+ /** Explicit erasure removes only exact retired role/launch events. */
48
+ export declare function eraseTerminationEvents(launches: ReadonlyArray<{
49
+ role: string;
50
+ launchId: string;
51
+ }>): void;
47
52
  /** Move retired state out of the live roster without deleting any evidence. */
48
53
  export declare function archiveTempState(role: string, reason: TempTerminationReason, outcome: TempTerminationRecord['outcome'], detail: string, now?: Date): string | undefined;
49
54
  /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { basename, join } from 'node:path';
4
- import { replaceFileAtomically, withFileLock } from './atomic-file.js';
4
+ import { replaceFileAtomically, withFileLock, withSynchronousFileLock } from './atomic-file.js';
5
5
  import { realExec } from './exec.js';
6
6
  import { stateRoot, tmpRoot } from './paths.js';
7
7
  export const TEMP_SUPERVISOR_FILE = '.temp-supervisor.json';
@@ -59,7 +59,7 @@ export function makeTempSupervisorLauncher(options = {}) {
59
59
  const supervisor = options.supervisor ?? process.env.OURS_FLEET_SUPERVISOR;
60
60
  return async (binPath, args, dir) => {
61
61
  const inherited = [
62
- 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'CODEX_HOME',
62
+ 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_FLEET_SOCKET_ROOT', 'CODEX_HOME',
63
63
  // Preserve runtime selection across the service-manager boundary, including
64
64
  // an explicit empty value which selects the bundle over manager defaults.
65
65
  'CODEX_PATH',
@@ -133,18 +133,34 @@ function appendTermination(dir, record) {
133
133
  replaceFileAtomically(join(dir, TEMP_GLOBAL_TERMINATION_MARKER), line);
134
134
  }
135
135
  function appendGlobalTermination(line, checkExisting = false) {
136
- mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
137
- const path = join(archiveRoot(), 'terminations.jsonl');
138
- // Recovery may revisit a .retiring directory after a crash between the
139
- // global append and final rename. Avoid duplicating that exact event.
140
- if (checkExisting) {
141
- try {
142
- if (readFileSync(path, 'utf8').split('\n').includes(line.trimEnd()))
143
- return;
136
+ withSynchronousFileLock(join(stateRoot(), 'locks', 'termination-journal'), () => {
137
+ mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
138
+ const path = join(archiveRoot(), 'terminations.jsonl');
139
+ // Recovery may revisit a .retiring directory after a crash between the
140
+ // global append and final rename. Avoid duplicating that exact event.
141
+ if (checkExisting) {
142
+ try {
143
+ if (readFileSync(path, 'utf8').split('\n').includes(line.trimEnd()))
144
+ return;
145
+ }
146
+ catch { /* the journal does not exist yet */ }
144
147
  }
145
- catch { /* the journal does not exist yet */ }
146
- }
147
- appendFileSync(path, line, { mode: 0o600 });
148
+ appendFileSync(path, line, { mode: 0o600 });
149
+ });
150
+ }
151
+ /** Explicit erasure removes only exact retired role/launch events. */
152
+ export function eraseTerminationEvents(launches) {
153
+ withSynchronousFileLock(join(stateRoot(), 'locks', 'termination-journal'), () => {
154
+ const path = join(archiveRoot(), 'terminations.jsonl');
155
+ if (!existsSync(path))
156
+ return;
157
+ const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
158
+ const kept = lines.filter(line => {
159
+ const row = JSON.parse(line);
160
+ return !launches.some(launch => launch.role === row.role && launch.launchId === row.launchId);
161
+ });
162
+ replaceFileAtomically(path, kept.length ? kept.join('\n') + '\n' : '');
163
+ });
148
164
  }
149
165
  /** Pick a sibling path without overwriting evidence from an earlier attempt. */
150
166
  function collisionSafeArchivePaths(targetBase, retiringBase) {
@@ -0,0 +1,7 @@
1
+ /** Keep the resumable record until both the QR and ordinary connection code were emitted. */
2
+ export declare function completeTunnelSetup(effects: {
3
+ link: () => Promise<unknown>;
4
+ qr: (code: string) => Promise<string>;
5
+ write: (text: string) => unknown;
6
+ clear: () => void;
7
+ }): Promise<void>;
@@ -0,0 +1,13 @@
1
+ /** Keep the resumable record until both the QR and ordinary connection code were emitted. */
2
+ export async function completeTunnelSetup(effects) {
3
+ try {
4
+ const link = await effects.link();
5
+ const code = Buffer.from(JSON.stringify(link)).toString('base64url');
6
+ const qr = await effects.qr(code);
7
+ await effects.write('Tunnel configured. Scan the QR or paste this private single-use connection code into the App.\n' + qr + '\n' + code + '\n');
8
+ effects.clear();
9
+ }
10
+ catch {
11
+ throw Error('Tunnel is configured but its connection code could not be printed. Run ours-fleet setup-tunnel --resume to retry.');
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ import { type WorkspacePayload } from './workspace-enrollment.js';
2
+ /** Read the same private-file transport as enrollment, without consuming the grant. */
3
+ export declare function readTunnelSetupFile(file: string): string;
4
+ export declare function readTunnelSetupStdin(input?: AsyncIterable<Buffer | string>): Promise<string>;
5
+ /** Only a minimal one-use v2 grant is allowed in shell arguments. */
6
+ export declare function readTunnelSetupArgument(input: string): string;
7
+ /** Redeem the grant by HTTPS POST; long-lived connector credentials remain off argv and URLs. */
8
+ export declare function redeemTunnelSetup(input: string, request?: typeof fetch): Promise<WorkspacePayload>;
9
+ /** Inspect only public destination metadata; this never consumes a one-use grant. */
10
+ export declare function inspectTunnelSetup(input: string, request?: typeof fetch): Promise<{
11
+ workspaceId: string;
12
+ serverCid: string;
13
+ appOrigin: string;
14
+ } | undefined>;
15
+ export declare function verifyTunnelSetupTarget(target: {
16
+ workspaceId: string;
17
+ serverCid: string;
18
+ appOrigin: string;
19
+ } | undefined, payload: WorkspacePayload): void;
@@ -0,0 +1,124 @@
1
+ import { lstatSync, readFileSync } from 'node:fs';
2
+ import { decodeWorkspacePayload } from './workspace-enrollment.js';
3
+ /** Read the same private-file transport as enrollment, without consuming the grant. */
4
+ export function readTunnelSetupFile(file) {
5
+ const stat = lstatSync(file);
6
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || stat.size > 32768)
7
+ throw Error('Setup payload requires an owned private regular file (chmod 600)');
8
+ return readFileSync(file, 'utf8');
9
+ }
10
+ export async function readTunnelSetupStdin(input = process.stdin) {
11
+ let value = '';
12
+ for await (const chunk of input) {
13
+ value += chunk.toString();
14
+ if (Buffer.byteLength(value) > 32768)
15
+ throw Error('Tunnel setup payload is too large');
16
+ }
17
+ return value;
18
+ }
19
+ /** Only a minimal one-use v2 grant is allowed in shell arguments. */
20
+ export function readTunnelSetupArgument(input) {
21
+ decodeTunnelSetupGrant(input);
22
+ return input;
23
+ }
24
+ function decodeTunnelSetupGrant(input) {
25
+ if (!/^[A-Za-z0-9_-]{1,32768}$/.test(input.trim()))
26
+ throw Error('Invalid tunnel setup payload');
27
+ let grant;
28
+ try {
29
+ grant = JSON.parse(Buffer.from(input.trim(), 'base64url').toString());
30
+ }
31
+ catch {
32
+ throw Error('Invalid tunnel setup payload');
33
+ }
34
+ if (grant?.version !== 2 || Object.keys(grant).sort().join(',') !== 'appOrigin,code,expiresAt,version'
35
+ || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(grant.appOrigin)
36
+ || !/^[A-Za-z0-9_-]{43}$/.test(grant.code) || !Number.isSafeInteger(grant.expiresAt)
37
+ || grant.expiresAt <= Date.now() || grant.expiresAt > Date.now() + 16 * 60000)
38
+ throw Error('Tunnel setup grant is invalid or expired; get a fresh command from the App');
39
+ return grant;
40
+ }
41
+ /** Redeem the grant by HTTPS POST; long-lived connector credentials remain off argv and URLs. */
42
+ export async function redeemTunnelSetup(input, request = fetch) {
43
+ if (!/^[A-Za-z0-9_-]{1,32768}$/.test(input.trim()))
44
+ throw Error('Invalid tunnel setup payload');
45
+ // v1 remains supported exclusively by the private file/stdin CLI transports.
46
+ let version;
47
+ try {
48
+ version = JSON.parse(Buffer.from(input.trim(), 'base64url').toString())?.version;
49
+ }
50
+ catch { }
51
+ if (version === 1)
52
+ return decodeWorkspacePayload(input);
53
+ const grant = decodeTunnelSetupGrant(input);
54
+ let response;
55
+ try {
56
+ response = await request(grant.appOrigin + '/account-api/workspace-install-redeem', {
57
+ method: 'POST', redirect: 'error', credentials: 'omit', signal: AbortSignal.timeout(30000),
58
+ headers: { 'Content-Type': 'application/json', Origin: grant.appOrigin }, body: JSON.stringify({ code: grant.code }),
59
+ });
60
+ }
61
+ catch {
62
+ throw Error('Could not reach the account service; setup grant redemption may be unconfirmed. Check the App before requesting a fresh command');
63
+ }
64
+ if (!response.ok) {
65
+ await response.body?.cancel();
66
+ throw Error(response.status === 410 ? 'Tunnel setup grant expired or was already used; get a fresh command from the App'
67
+ : `Tunnel setup grant redemption failed (HTTP ${response.status}); check the App before retrying`);
68
+ }
69
+ let result;
70
+ try {
71
+ result = await response.json();
72
+ }
73
+ catch {
74
+ throw Error('Invalid tunnel setup response');
75
+ }
76
+ if (typeof result?.payload !== 'string' || !/^[A-Za-z0-9_-]{1,32768}$/.test(result.payload))
77
+ throw Error('Invalid tunnel setup response');
78
+ const payload = decodeWorkspacePayload(result.payload);
79
+ if (payload.appOrigin !== grant.appOrigin)
80
+ throw Error('Tunnel setup response origin mismatch');
81
+ return payload;
82
+ }
83
+ /** Inspect only public destination metadata; this never consumes a one-use grant. */
84
+ export async function inspectTunnelSetup(input, request = fetch) {
85
+ let version;
86
+ try {
87
+ version = JSON.parse(Buffer.from(input.trim(), 'base64url').toString()).version;
88
+ }
89
+ catch { }
90
+ if (version === 1) {
91
+ const p = decodeWorkspacePayload(input);
92
+ return { workspaceId: p.challenge.workspaceId, serverCid: p.serverCid, appOrigin: p.appOrigin };
93
+ }
94
+ const grant = decodeTunnelSetupGrant(input);
95
+ let response;
96
+ try {
97
+ response = await request(grant.appOrigin + '/account-api/workspace-install-inspect', { method: 'POST', redirect: 'error', credentials: 'omit', signal: AbortSignal.timeout(15000), headers: { Origin: grant.appOrigin, 'Content-Type': 'application/json' }, body: JSON.stringify({ code: grant.code }) });
98
+ }
99
+ catch {
100
+ throw Error('Could not inspect the account setup command; existing setup and grant preserved');
101
+ }
102
+ if (response.status === 404 || response.status === 405) {
103
+ await response.body?.cancel();
104
+ return;
105
+ }
106
+ if (!response.ok) {
107
+ await response.body?.cancel();
108
+ throw Error('Setup command inspection failed; existing setup and grant preserved');
109
+ }
110
+ let target;
111
+ try {
112
+ target = await response.json();
113
+ }
114
+ catch {
115
+ throw Error('Invalid setup command inspection response');
116
+ }
117
+ if (!target || Object.keys(target).sort().join(',') !== 'serverCid,workspaceId' || typeof target.workspaceId !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(target.workspaceId) || typeof target.serverCid !== 'string' || !/^[a-f0-9]{64}$/i.test(target.serverCid))
118
+ throw Error('Invalid setup command inspection response');
119
+ return { ...target, appOrigin: grant.appOrigin };
120
+ }
121
+ export function verifyTunnelSetupTarget(target, payload) {
122
+ if (target && (target.workspaceId !== payload.challenge.workspaceId || target.serverCid.toUpperCase() !== payload.serverCid.toUpperCase() || target.appOrigin !== payload.appOrigin))
123
+ throw Error('Redeemed setup command differs from inspected destination; existing setup preserved');
124
+ }
@@ -0,0 +1,29 @@
1
+ export declare const MAX_ATTACHMENT_BYTES: number;
2
+ export declare const MAX_ATTACHMENT_BODY: number;
3
+ export interface AgentAttachment {
4
+ id: string;
5
+ name: string;
6
+ mimeType: string;
7
+ size: number;
8
+ sha256: string;
9
+ generation: string;
10
+ }
11
+ export declare function storeAgentAttachment(stateDir: string, generation: string, input: unknown): AgentAttachment;
12
+ export declare function attachmentPrompt(stateDir: string, generation: string, ids: unknown, text: string): string;
13
+ export declare function readAgentAttachment(stateDir: string, id: string): {
14
+ record: AgentAttachment;
15
+ bytes: Buffer;
16
+ };
17
+ interface AttachmentPresentation {
18
+ generation: string;
19
+ commandId: string;
20
+ text: string;
21
+ transportDigest: string;
22
+ ids: string[];
23
+ }
24
+ /** Written before admission so a lost HTTP response cannot lose the visible message. */
25
+ export declare function storeAttachmentPresentation(stateDir: string, value: AttachmentPresentation): void;
26
+ export declare function prepareAttachmentPresentation(stateDir: string, generation: string, commandId: string, ids: string[], text: string, transport: string): void;
27
+ /** Never infer attachments by stripping user text or interpreting a marker. */
28
+ export declare function presentAttachmentEvent<T>(stateDir: string | undefined, roleId: string, event: T): T;
29
+ export {};
@@ -0,0 +1,162 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { FleetError } from '../application/errors.js';
5
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
6
+ export const MAX_ATTACHMENT_BODY = Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 + 4096;
7
+ function fail(message) { throw new FleetError('invalid_request', message); }
8
+ const digest = (data) => createHash('sha256').update(data).digest('hex');
9
+ function directory(path, create = false, privateStorage = true) {
10
+ if (create && !existsSync(path))
11
+ mkdirSync(path, { mode: 0o700 });
12
+ const stat = lstatSync(path);
13
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & (privateStorage ? 0o022 : 0o002)))
14
+ fail('Attachment directory is not a private owned directory');
15
+ }
16
+ function read(path, max) {
17
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
18
+ try {
19
+ const stat = fstatSync(fd);
20
+ if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.size > max)
21
+ fail('Attachment is not a bounded owned regular file');
22
+ return readFileSync(fd);
23
+ }
24
+ finally {
25
+ closeSync(fd);
26
+ }
27
+ }
28
+ function immutable(path, bytes) {
29
+ try {
30
+ writeFileSync(path, bytes, { flag: 'wx', mode: 0o400 });
31
+ }
32
+ catch (error) {
33
+ if (error.code !== 'EEXIST')
34
+ throw error;
35
+ if (!read(path, bytes.length).equals(bytes))
36
+ fail('Attachment content changed');
37
+ }
38
+ }
39
+ function filename(name) {
40
+ const chars = Array.from(name.replace(/[^\p{L}\p{N}._-]/gu, '_').replace(/^\.+/, '_'));
41
+ while (Buffer.byteLength(chars.join('')) > 120)
42
+ chars.pop();
43
+ return chars.join('') || 'file';
44
+ }
45
+ export function storeAgentAttachment(stateDir, generation, input) {
46
+ if (!input || typeof input !== 'object')
47
+ fail('File data is required');
48
+ const body = input;
49
+ if (typeof body.name !== 'string' || !body.name.trim() || body.name.length > 240 || /[\x00-\x1f]/u.test(body.name))
50
+ fail('A file name of 1–240 characters is required');
51
+ if (typeof body.mimeType !== 'string' || !/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(body.mimeType))
52
+ fail('A valid media type is required');
53
+ if (typeof body.data !== 'string' || body.data.length > Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 || /[^A-Za-z0-9+/=]/.test(body.data))
54
+ fail('Invalid base64 file data or file exceeds 20 MiB');
55
+ const bytes = Buffer.from(body.data, 'base64');
56
+ if (bytes.length > MAX_ATTACHMENT_BYTES || bytes.toString('base64') !== body.data)
57
+ fail('Invalid file data or file exceeds 20 MiB');
58
+ const sha256 = digest(bytes), name = body.name, mimeType = body.mimeType;
59
+ const id = digest(JSON.stringify({ generation, name, mimeType, sha256 }));
60
+ const record = { id, name, mimeType, size: bytes.length, sha256, generation };
61
+ // Fleet runtime state directories may use the account group (0775). Upload storage remains private.
62
+ directory(stateDir, false, false);
63
+ const root = join(stateDir, 'web-attachments');
64
+ directory(root, true);
65
+ const dir = join(root, id);
66
+ directory(dir, true);
67
+ immutable(join(dir, 'content-' + filename(name)), bytes);
68
+ immutable(join(dir, 'metadata.json'), Buffer.from(JSON.stringify(record)));
69
+ return record;
70
+ }
71
+ export function attachmentPrompt(stateDir, generation, ids, text) {
72
+ if (!Array.isArray(ids) || ids.length < 1 || ids.length > 5 || new Set(ids).size !== ids.length)
73
+ fail('Select between 1 and 5 distinct attachments');
74
+ // Fleet runtime state directories may use the account group (0775). Upload storage remains private.
75
+ directory(stateDir, false, false);
76
+ const root = join(stateDir, 'web-attachments');
77
+ directory(root);
78
+ const files = ids.map(id => {
79
+ if (typeof id !== 'string' || !/^[a-f0-9]{64}$/.test(id))
80
+ fail('Invalid attachment ID');
81
+ const dir = join(root, id);
82
+ directory(dir);
83
+ const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
84
+ if (record.id !== id || record.generation !== generation)
85
+ throw new FleetError('stale_state', 'Attachment belongs to a different agent session');
86
+ const file = join(dir, 'content-' + filename(record.name)), bytes = read(file, MAX_ATTACHMENT_BYTES);
87
+ if (bytes.length !== record.size || digest(bytes) !== record.sha256 || digest(JSON.stringify({ generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
88
+ fail('Attachment content changed');
89
+ return { name: record.name, mimeType: record.mimeType, size: record.size, sha256: record.sha256, path: file };
90
+ });
91
+ return `${text}${text ? '\n\n' : ''}Attached files (available on this host; open the files to inspect their contents, and use the image viewer for images):\n${JSON.stringify(files, null, 2)}`;
92
+ }
93
+ /** Authenticated content access reuses the same ownership and integrity checks as admission. */
94
+ function attachmentMetadata(stateDir, id) {
95
+ if (!/^[a-f0-9]{64}$/.test(id))
96
+ fail('Invalid attachment ID');
97
+ directory(stateDir, false, false);
98
+ const root = join(stateDir, 'web-attachments');
99
+ directory(root);
100
+ const dir = join(root, id);
101
+ directory(dir);
102
+ const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
103
+ if (record.id !== id || typeof record.name !== 'string' || typeof record.mimeType !== 'string')
104
+ fail('Invalid attachment metadata');
105
+ if (digest(JSON.stringify({ generation: record.generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
106
+ fail('Attachment metadata changed');
107
+ return record;
108
+ }
109
+ export function readAgentAttachment(stateDir, id) {
110
+ let record;
111
+ try {
112
+ record = attachmentMetadata(stateDir, id);
113
+ }
114
+ catch (error) {
115
+ if (error.code === 'ENOENT')
116
+ throw new FleetError('resource_not_found', 'Attachment not found');
117
+ throw error;
118
+ }
119
+ const bytes = read(join(stateDir, 'web-attachments', id, 'content-' + filename(record.name)), MAX_ATTACHMENT_BYTES);
120
+ if (bytes.length !== record.size || digest(bytes) !== record.sha256)
121
+ fail('Attachment content changed');
122
+ return { record, bytes };
123
+ }
124
+ /** Written before admission so a lost HTTP response cannot lose the visible message. */
125
+ export function storeAttachmentPresentation(stateDir, value) {
126
+ directory(stateDir, false, false);
127
+ const root = join(stateDir, 'web-attachment-messages');
128
+ directory(root, true);
129
+ immutable(join(root, digest(JSON.stringify([value.generation, value.commandId])) + '.json'), Buffer.from(JSON.stringify(value)));
130
+ }
131
+ export function prepareAttachmentPresentation(stateDir, generation, commandId, ids, text, transport) {
132
+ storeAttachmentPresentation(stateDir, { generation, commandId, ids, text, transportDigest: digest(transport) });
133
+ }
134
+ /** Never infer attachments by stripping user text or interpreting a marker. */
135
+ export function presentAttachmentEvent(stateDir, roleId, event) {
136
+ const e = event;
137
+ if (!stateDir || e.kind !== 'prompt.admitted' || e.source !== 'owner_admin_console' || !e.commandId || e.payload?.text?.redacted)
138
+ return event;
139
+ const root = join(stateDir, 'web-attachment-messages');
140
+ if (!existsSync(root))
141
+ return event;
142
+ const path = join(root, digest(JSON.stringify([e.sessionGeneration, e.commandId])) + '.json');
143
+ if (!existsSync(path))
144
+ return event;
145
+ try {
146
+ directory(stateDir, false, false);
147
+ directory(root);
148
+ const value = JSON.parse(read(path, 1024 * 1024).toString());
149
+ if (value.generation !== e.sessionGeneration || value.commandId !== e.commandId || value.transportDigest !== digest(e.payload.text.text))
150
+ return event;
151
+ const attachments = value.ids.map(id => {
152
+ const record = attachmentMetadata(stateDir, id);
153
+ if (record.generation !== value.generation)
154
+ fail('Attachment session changed');
155
+ return { id, name: record.name, mimeType: record.mimeType, size: record.size, url: `/api/v1/roles/${encodeURIComponent(roleId)}/attachments/${id}` };
156
+ });
157
+ return { ...e, payload: { ...e.payload, text: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, displayText: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, attachments } };
158
+ }
159
+ catch {
160
+ return event;
161
+ }
162
+ }
@@ -1,3 +1,4 @@
1
+ import { WorkspaceDeviceStore } from './workspace-devices.js';
1
2
  import type { FastifyRequest } from 'fastify';
2
3
  import type { WebSocket } from 'ws';
3
4
  import { TrustedDeviceStore, type TrustedDeviceIssue } from './device-store.js';
@@ -26,6 +27,8 @@ export declare class WebAuth {
26
27
  private readonly now;
27
28
  private readonly devices;
28
29
  private readonly access;
30
+ private readonly workspaceDevices?;
31
+ readonly appOrigin: string;
29
32
  private _bootstrapSecret;
30
33
  private bootstrapExpiresAt;
31
34
  private bootstrapUsed;
@@ -33,8 +36,9 @@ export declare class WebAuth {
33
36
  private readonly sessionDevices;
34
37
  private readonly tickets;
35
38
  private readonly rates;
39
+ private readonly transports;
36
40
  private readonly sockets;
37
- constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig);
41
+ constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig, workspaceDevices?: WorkspaceDeviceStore | undefined, appOrigin?: string);
38
42
  get bootstrapSecret(): string;
39
43
  get origin(): string;
40
44
  get host(): string;
@@ -48,6 +52,8 @@ export declare class WebAuth {
48
52
  }): void;
49
53
  /** Mint a replacement for an operator-triggered reauthentication ceremony. */
50
54
  mintBootstrap(): string;
55
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
56
+ accountOrigin(request: Pick<FastifyRequest, 'headers'>): string | undefined;
51
57
  validateBoundary(request: FastifyRequest, requireOrigin: boolean): void;
52
58
  exchange(request: FastifyRequest): AuthResult;
53
59
  login(request: FastifyRequest, password: string): AuthResult;
@@ -61,8 +67,30 @@ export declare class WebAuth {
61
67
  };
62
68
  consumeTicket(request: FastifyRequest, value: string, purpose: Ticket['purpose'], roleId?: string): BrowserSession;
63
69
  bindSocket(sessionId: string, socket: WebSocket): void;
70
+ /** Raw service streams share the same logout/revocation boundary as native sockets. */
71
+ bindTransport(sessionId: string, close: () => void): () => void;
64
72
  clearSessions(): void;
65
73
  revokeAllTrustedDevices(): number;
74
+ workspaceIdentity(): {
75
+ workspaceId: string;
76
+ };
77
+ workspaceEnrollment(issuer?: string | null): {
78
+ enrollment: string;
79
+ workspaceId: string;
80
+ expiresAt: number;
81
+ };
82
+ enrollWorkspace(request: FastifyRequest, input: {
83
+ enrollment: string;
84
+ workspaceId: string;
85
+ label: string;
86
+ }): {
87
+ token: string;
88
+ device: import("./workspace-devices.js").WorkspaceDevice;
89
+ workspaceId: string;
90
+ };
91
+ listWorkspaceDevices(): import("./workspace-devices.js").WorkspaceDevice[];
92
+ revokeWorkspaceDevice(id: string): void;
93
+ private requireWorkspaceDevices;
66
94
  shutdown(): void;
67
95
  private createSession;
68
96
  private removeSession;