botmux 3.10.0 → 3.11.0

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 (539) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/herdr-backend.d.ts +4 -3
  3. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/herdr-backend.js +11 -8
  5. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  6. package/dist/adapters/backend/pty-backend.d.ts +1 -1
  7. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/pty-backend.js +4 -1
  9. package/dist/adapters/backend/pty-backend.js.map +1 -1
  10. package/dist/adapters/backend/riff-backend.d.ts +29 -5
  11. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/riff-backend.js +302 -31
  13. package/dist/adapters/backend/riff-backend.js.map +1 -1
  14. package/dist/adapters/backend/sandbox.d.ts +6 -0
  15. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  16. package/dist/adapters/backend/sandbox.js +7 -0
  17. package/dist/adapters/backend/sandbox.js.map +1 -1
  18. package/dist/adapters/backend/tmux-backend.d.ts +1 -1
  19. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  20. package/dist/adapters/backend/tmux-backend.js +4 -1
  21. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  22. package/dist/adapters/backend/tmux-pipe-backend.d.ts +1 -1
  23. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  24. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  25. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  26. package/dist/adapters/backend/types.d.ts +37 -3
  27. package/dist/adapters/backend/types.d.ts.map +1 -1
  28. package/dist/adapters/backend/types.js.map +1 -1
  29. package/dist/adapters/backend/zellij-backend.d.ts +3 -3
  30. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  31. package/dist/adapters/backend/zellij-backend.js +9 -3
  32. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  33. package/dist/adapters/backend/zellij-observe-backend.d.ts +3 -3
  34. package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
  35. package/dist/adapters/backend/zellij-observe-backend.js +12 -7
  36. package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
  37. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  38. package/dist/adapters/cli/codex-app.js +11 -2
  39. package/dist/adapters/cli/codex-app.js.map +1 -1
  40. package/dist/adapters/cli/read-isolation.d.ts +35 -2
  41. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  42. package/dist/adapters/cli/read-isolation.js +88 -7
  43. package/dist/adapters/cli/read-isolation.js.map +1 -1
  44. package/dist/adapters/cli/runner-input.d.ts +15 -14
  45. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  46. package/dist/adapters/cli/runner-input.js +64 -26
  47. package/dist/adapters/cli/runner-input.js.map +1 -1
  48. package/dist/adapters/cli/types.d.ts +20 -5
  49. package/dist/adapters/cli/types.d.ts.map +1 -1
  50. package/dist/bot-registry.d.ts +24 -0
  51. package/dist/bot-registry.d.ts.map +1 -1
  52. package/dist/bot-registry.js +110 -5
  53. package/dist/bot-registry.js.map +1 -1
  54. package/dist/cli/fleet-shutdown.d.ts +47 -0
  55. package/dist/cli/fleet-shutdown.d.ts.map +1 -0
  56. package/dist/cli/fleet-shutdown.js +387 -0
  57. package/dist/cli/fleet-shutdown.js.map +1 -0
  58. package/dist/cli/pm2-descriptor-guard.d.ts +19 -0
  59. package/dist/cli/pm2-descriptor-guard.d.ts.map +1 -0
  60. package/dist/cli/pm2-descriptor-guard.js +122 -0
  61. package/dist/cli/pm2-descriptor-guard.js.map +1 -0
  62. package/dist/cli/pm2-exact-start.d.ts +16 -0
  63. package/dist/cli/pm2-exact-start.d.ts.map +1 -0
  64. package/dist/cli/pm2-exact-start.js +63 -0
  65. package/dist/cli/pm2-exact-start.js.map +1 -0
  66. package/dist/cli/pm2-god-admission.d.ts +8 -0
  67. package/dist/cli/pm2-god-admission.d.ts.map +1 -0
  68. package/dist/cli/pm2-god-admission.js +25 -0
  69. package/dist/cli/pm2-god-admission.js.map +1 -0
  70. package/dist/cli/pm2-jlist.d.ts +13 -0
  71. package/dist/cli/pm2-jlist.d.ts.map +1 -0
  72. package/dist/cli/pm2-jlist.js +108 -0
  73. package/dist/cli/pm2-jlist.js.map +1 -0
  74. package/dist/cli/pm2-preflight.d.ts +10 -0
  75. package/dist/cli/pm2-preflight.d.ts.map +1 -0
  76. package/dist/cli/pm2-preflight.js +23 -0
  77. package/dist/cli/pm2-preflight.js.map +1 -0
  78. package/dist/cli/pm2-shutdown-capability.d.ts +27 -0
  79. package/dist/cli/pm2-shutdown-capability.d.ts.map +1 -0
  80. package/dist/cli/pm2-shutdown-capability.js +161 -0
  81. package/dist/cli/pm2-shutdown-capability.js.map +1 -0
  82. package/dist/cli/pm2-start-transaction.d.ts +55 -0
  83. package/dist/cli/pm2-start-transaction.d.ts.map +1 -0
  84. package/dist/cli/pm2-start-transaction.js +220 -0
  85. package/dist/cli/pm2-start-transaction.js.map +1 -0
  86. package/dist/cli/send-dispatch.d.ts +12 -0
  87. package/dist/cli/send-dispatch.d.ts.map +1 -1
  88. package/dist/cli/send-dispatch.js +31 -4
  89. package/dist/cli/send-dispatch.js.map +1 -1
  90. package/dist/cli/session-list-liveness.d.ts +1 -0
  91. package/dist/cli/session-list-liveness.d.ts.map +1 -1
  92. package/dist/cli/session-list-liveness.js +6 -0
  93. package/dist/cli/session-list-liveness.js.map +1 -1
  94. package/dist/cli/session-picker-layout.d.ts +26 -0
  95. package/dist/cli/session-picker-layout.d.ts.map +1 -0
  96. package/dist/cli/session-picker-layout.js +123 -0
  97. package/dist/cli/session-picker-layout.js.map +1 -0
  98. package/dist/cli/session-picker-viewport.d.ts +49 -0
  99. package/dist/cli/session-picker-viewport.d.ts.map +1 -0
  100. package/dist/cli/session-picker-viewport.js +46 -0
  101. package/dist/cli/session-picker-viewport.js.map +1 -0
  102. package/dist/cli/supervisor-shutdown-client.d.ts +31 -0
  103. package/dist/cli/supervisor-shutdown-client.d.ts.map +1 -0
  104. package/dist/cli/supervisor-shutdown-client.js +146 -0
  105. package/dist/cli/supervisor-shutdown-client.js.map +1 -0
  106. package/dist/cli/terminal-width.d.ts +48 -0
  107. package/dist/cli/terminal-width.d.ts.map +1 -0
  108. package/dist/cli/terminal-width.js +119 -0
  109. package/dist/cli/terminal-width.js.map +1 -0
  110. package/dist/cli/vc-agent.d.ts.map +1 -1
  111. package/dist/cli/vc-agent.js +1 -1
  112. package/dist/cli/vc-agent.js.map +1 -1
  113. package/dist/cli.d.ts.map +1 -1
  114. package/dist/cli.js +2269 -716
  115. package/dist/cli.js.map +1 -1
  116. package/dist/codex-app-runner.js +1630 -125
  117. package/dist/codex-app-runner.js.map +1 -1
  118. package/dist/core/ask-api.d.ts +8 -1
  119. package/dist/core/ask-api.d.ts.map +1 -1
  120. package/dist/core/ask-api.js +18 -0
  121. package/dist/core/ask-api.js.map +1 -1
  122. package/dist/core/ask-broker.d.ts +20 -0
  123. package/dist/core/ask-broker.d.ts.map +1 -1
  124. package/dist/core/ask-broker.js +467 -47
  125. package/dist/core/ask-broker.js.map +1 -1
  126. package/dist/core/ask-persist-store.d.ts +126 -0
  127. package/dist/core/ask-persist-store.d.ts.map +1 -0
  128. package/dist/core/ask-persist-store.js +188 -0
  129. package/dist/core/ask-persist-store.js.map +1 -0
  130. package/dist/core/ask-types.d.ts +80 -0
  131. package/dist/core/ask-types.d.ts.map +1 -1
  132. package/dist/core/ask-types.js +62 -0
  133. package/dist/core/ask-types.js.map +1 -1
  134. package/dist/core/cli-session-id-publisher.d.ts +20 -0
  135. package/dist/core/cli-session-id-publisher.d.ts.map +1 -0
  136. package/dist/core/cli-session-id-publisher.js +21 -0
  137. package/dist/core/cli-session-id-publisher.js.map +1 -0
  138. package/dist/core/command-handler.d.ts.map +1 -1
  139. package/dist/core/command-handler.js +427 -255
  140. package/dist/core/command-handler.js.map +1 -1
  141. package/dist/core/cost-calculator.d.ts +13 -0
  142. package/dist/core/cost-calculator.d.ts.map +1 -1
  143. package/dist/core/cost-calculator.js +385 -26
  144. package/dist/core/cost-calculator.js.map +1 -1
  145. package/dist/core/dashboard-ipc-server.d.ts +9 -0
  146. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  147. package/dist/core/dashboard-ipc-server.js +842 -288
  148. package/dist/core/dashboard-ipc-server.js.map +1 -1
  149. package/dist/core/explicit-session-backing-cleanup.d.ts +49 -0
  150. package/dist/core/explicit-session-backing-cleanup.d.ts.map +1 -0
  151. package/dist/core/explicit-session-backing-cleanup.js +76 -0
  152. package/dist/core/explicit-session-backing-cleanup.js.map +1 -0
  153. package/dist/core/idle-worker-sweeper.d.ts +20 -0
  154. package/dist/core/idle-worker-sweeper.d.ts.map +1 -1
  155. package/dist/core/idle-worker-sweeper.js +18 -0
  156. package/dist/core/idle-worker-sweeper.js.map +1 -1
  157. package/dist/core/inflight-input-tracker.d.ts +10 -0
  158. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  159. package/dist/core/inflight-input-tracker.js +7 -0
  160. package/dist/core/inflight-input-tracker.js.map +1 -1
  161. package/dist/core/managed-origin-attestation.d.ts +51 -0
  162. package/dist/core/managed-origin-attestation.d.ts.map +1 -0
  163. package/dist/core/managed-origin-attestation.js +208 -0
  164. package/dist/core/managed-origin-attestation.js.map +1 -0
  165. package/dist/core/managed-origin-capability.d.ts +62 -3
  166. package/dist/core/managed-origin-capability.d.ts.map +1 -1
  167. package/dist/core/managed-origin-capability.js +419 -9
  168. package/dist/core/managed-origin-capability.js.map +1 -1
  169. package/dist/core/pending-repo-journal.d.ts +6 -0
  170. package/dist/core/pending-repo-journal.d.ts.map +1 -0
  171. package/dist/core/pending-repo-journal.js +85 -0
  172. package/dist/core/pending-repo-journal.js.map +1 -0
  173. package/dist/core/persistent-backend.d.ts +6 -1
  174. package/dist/core/persistent-backend.d.ts.map +1 -1
  175. package/dist/core/persistent-backend.js +12 -6
  176. package/dist/core/persistent-backend.js.map +1 -1
  177. package/dist/core/process-start-identity.d.ts +3 -0
  178. package/dist/core/process-start-identity.d.ts.map +1 -0
  179. package/dist/core/process-start-identity.js +62 -0
  180. package/dist/core/process-start-identity.js.map +1 -0
  181. package/dist/core/raw-command-writer.d.ts +34 -0
  182. package/dist/core/raw-command-writer.d.ts.map +1 -0
  183. package/dist/core/raw-command-writer.js +48 -0
  184. package/dist/core/raw-command-writer.js.map +1 -0
  185. package/dist/core/reply-target.d.ts +85 -22
  186. package/dist/core/reply-target.d.ts.map +1 -1
  187. package/dist/core/reply-target.js +267 -32
  188. package/dist/core/reply-target.js.map +1 -1
  189. package/dist/core/resource-monitor/runtime.d.ts.map +1 -1
  190. package/dist/core/resource-monitor/runtime.js +4 -1
  191. package/dist/core/resource-monitor/runtime.js.map +1 -1
  192. package/dist/core/restart-report.d.ts +6 -1
  193. package/dist/core/restart-report.d.ts.map +1 -1
  194. package/dist/core/restart-report.js +22 -4
  195. package/dist/core/restart-report.js.map +1 -1
  196. package/dist/core/riff-shutdown-detach.d.ts +164 -0
  197. package/dist/core/riff-shutdown-detach.d.ts.map +1 -0
  198. package/dist/core/riff-shutdown-detach.js +787 -0
  199. package/dist/core/riff-shutdown-detach.js.map +1 -0
  200. package/dist/core/riff-worker-shutdown-readiness.d.ts +17 -0
  201. package/dist/core/riff-worker-shutdown-readiness.d.ts.map +1 -0
  202. package/dist/core/riff-worker-shutdown-readiness.js +21 -0
  203. package/dist/core/riff-worker-shutdown-readiness.js.map +1 -0
  204. package/dist/core/session-manager.d.ts +6 -6
  205. package/dist/core/session-manager.d.ts.map +1 -1
  206. package/dist/core/session-manager.js +1227 -594
  207. package/dist/core/session-manager.js.map +1 -1
  208. package/dist/core/session-marker.d.ts +1 -1
  209. package/dist/core/session-marker.d.ts.map +1 -1
  210. package/dist/core/session-marker.js +2 -2
  211. package/dist/core/session-marker.js.map +1 -1
  212. package/dist/core/session-mutation-guard.d.ts +17 -0
  213. package/dist/core/session-mutation-guard.d.ts.map +1 -0
  214. package/dist/core/session-mutation-guard.js +33 -0
  215. package/dist/core/session-mutation-guard.js.map +1 -0
  216. package/dist/core/shutdown-budgets.d.ts +27 -0
  217. package/dist/core/shutdown-budgets.d.ts.map +1 -0
  218. package/dist/core/shutdown-budgets.js +47 -0
  219. package/dist/core/shutdown-budgets.js.map +1 -0
  220. package/dist/core/supervisor-shutdown-ipc.d.ts +10 -0
  221. package/dist/core/supervisor-shutdown-ipc.d.ts.map +1 -0
  222. package/dist/core/supervisor-shutdown-ipc.js +10 -0
  223. package/dist/core/supervisor-shutdown-ipc.js.map +1 -0
  224. package/dist/core/supervisor-shutdown-protocol.d.ts +16 -0
  225. package/dist/core/supervisor-shutdown-protocol.d.ts.map +1 -0
  226. package/dist/core/supervisor-shutdown-protocol.js +16 -0
  227. package/dist/core/supervisor-shutdown-protocol.js.map +1 -0
  228. package/dist/core/trigger-final-suppression.d.ts +4 -0
  229. package/dist/core/trigger-final-suppression.d.ts.map +1 -1
  230. package/dist/core/trigger-final-suppression.js +6 -11
  231. package/dist/core/trigger-final-suppression.js.map +1 -1
  232. package/dist/core/trigger-session.d.ts +70 -0
  233. package/dist/core/trigger-session.d.ts.map +1 -1
  234. package/dist/core/trigger-session.js +923 -145
  235. package/dist/core/trigger-session.js.map +1 -1
  236. package/dist/core/types.d.ts +94 -1
  237. package/dist/core/types.d.ts.map +1 -1
  238. package/dist/core/types.js +9 -0
  239. package/dist/core/types.js.map +1 -1
  240. package/dist/core/worker-pool.d.ts +194 -13
  241. package/dist/core/worker-pool.d.ts.map +1 -1
  242. package/dist/core/worker-pool.js +2597 -547
  243. package/dist/core/worker-pool.js.map +1 -1
  244. package/dist/daemon.d.ts +76 -2
  245. package/dist/daemon.d.ts.map +1 -1
  246. package/dist/daemon.js +2255 -773
  247. package/dist/daemon.js.map +1 -1
  248. package/dist/dashboard/bot-payload.d.ts +20 -0
  249. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  250. package/dist/dashboard/bot-payload.js +24 -0
  251. package/dist/dashboard/bot-payload.js.map +1 -1
  252. package/dist/dashboard/feed-groups.d.ts +17 -0
  253. package/dist/dashboard/feed-groups.d.ts.map +1 -0
  254. package/dist/dashboard/feed-groups.js +75 -0
  255. package/dist/dashboard/feed-groups.js.map +1 -0
  256. package/dist/dashboard/session-card-model.d.ts +1 -1
  257. package/dist/dashboard/session-card-model.d.ts.map +1 -1
  258. package/dist/dashboard/session-card-model.js +8 -5
  259. package/dist/dashboard/session-card-model.js.map +1 -1
  260. package/dist/dashboard/web/bot-defaults-page.d.ts +4 -0
  261. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  262. package/dist/dashboard/web/bot-defaults-page.js +139 -46
  263. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  264. package/dist/dashboard/web/bot-defaults.d.ts +4 -0
  265. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  266. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  267. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  268. package/dist/dashboard/web/connectors-page.js +40 -3
  269. package/dist/dashboard/web/connectors-page.js.map +1 -1
  270. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  271. package/dist/dashboard/web/groups-page.js +95 -2
  272. package/dist/dashboard/web/groups-page.js.map +1 -1
  273. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  274. package/dist/dashboard/web/i18n.js +86 -40
  275. package/dist/dashboard/web/i18n.js.map +1 -1
  276. package/dist/dashboard/web/kanban-model.js +1 -1
  277. package/dist/dashboard/web/kanban-model.js.map +1 -1
  278. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  279. package/dist/dashboard/web/overview-page.js +3 -2
  280. package/dist/dashboard/web/overview-page.js.map +1 -1
  281. package/dist/dashboard/web/overview.d.ts +3 -0
  282. package/dist/dashboard/web/overview.d.ts.map +1 -1
  283. package/dist/dashboard/web/overview.js +2 -0
  284. package/dist/dashboard/web/overview.js.map +1 -1
  285. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  286. package/dist/dashboard/web/sessions-kanban.js +2 -0
  287. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  288. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  289. package/dist/dashboard/web/sessions-page.js +103 -2
  290. package/dist/dashboard/web/sessions-page.js.map +1 -1
  291. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  292. package/dist/dashboard/web/sessions.js +3 -2
  293. package/dist/dashboard/web/sessions.js.map +1 -1
  294. package/dist/dashboard/web/ui.d.ts +8 -0
  295. package/dist/dashboard/web/ui.d.ts.map +1 -1
  296. package/dist/dashboard/web/ui.js +15 -0
  297. package/dist/dashboard/web/ui.js.map +1 -1
  298. package/dist/dashboard-web/app.js +1 -1
  299. package/dist/dashboard-web/chunks/bot-defaults-page-GED4FEMS.js +10 -0
  300. package/dist/dashboard-web/chunks/chunk-6J5LS5AU.js +51 -0
  301. package/dist/dashboard-web/chunks/{chunk-ZU3UUKAR.js → chunk-HK7NIBJG.js} +1 -1
  302. package/dist/dashboard-web/chunks/{chunk-3UE7S77S.js → chunk-KRHWZEXU.js} +1 -1
  303. package/dist/dashboard-web/chunks/chunk-VBXJOJFH.js +1 -0
  304. package/dist/dashboard-web/chunks/connectors-page-EELM66G3.js +1 -0
  305. package/dist/dashboard-web/chunks/groups-page-WKUV6E42.js +8 -0
  306. package/dist/dashboard-web/chunks/{insights-page-T2ZX2RWL.js → insights-page-54UBPPOC.js} +1 -1
  307. package/dist/dashboard-web/chunks/{monitor-room-YNVBAGR6.js → monitor-room-PORHVKRH.js} +1 -1
  308. package/dist/dashboard-web/chunks/{monitoring-page-LHM55YWN.js → monitoring-page-XGA7AXNL.js} +1 -1
  309. package/dist/dashboard-web/chunks/{office-page-ATXVP7PW.js → office-page-AO3AFG45.js} +1 -1
  310. package/dist/dashboard-web/chunks/overview-page-F5BUTYR6.js +1 -0
  311. package/dist/dashboard-web/chunks/{roles-page-56VWM6CD.js → roles-page-M5GIASJU.js} +1 -1
  312. package/dist/dashboard-web/chunks/{schedules-page-S5K6U4EO.js → schedules-page-O232WQRR.js} +1 -1
  313. package/dist/dashboard-web/chunks/sessions-page-YFRPSTIL.js +6 -0
  314. package/dist/dashboard-web/chunks/{settings-page-QUZDQARR.js → settings-page-OJAZLZQT.js} +1 -1
  315. package/dist/dashboard-web/chunks/{skills-page-IFY7AEND.js → skills-page-EFZQPWV4.js} +1 -1
  316. package/dist/dashboard-web/chunks/{team-federation-page-C7FJQPFZ.js → team-federation-page-ZJVAP7HW.js} +1 -1
  317. package/dist/dashboard-web/chunks/{v3-page-2LMF3WWR.js → v3-page-GYWTRGO2.js} +1 -1
  318. package/dist/dashboard-web/chunks/{whiteboards-page-3XC6QRMY.js → whiteboards-page-MSDAG6P7.js} +1 -1
  319. package/dist/dashboard-web/style.css +255 -0
  320. package/dist/dashboard.js +172 -7
  321. package/dist/dashboard.js.map +1 -1
  322. package/dist/desktop/renderer/app.js +21 -0
  323. package/dist/desktop/renderer/app.js.map +1 -1
  324. package/dist/i18n/en.d.ts.map +1 -1
  325. package/dist/i18n/en.js +35 -9
  326. package/dist/i18n/en.js.map +1 -1
  327. package/dist/i18n/zh.d.ts.map +1 -1
  328. package/dist/i18n/zh.js +35 -9
  329. package/dist/i18n/zh.js.map +1 -1
  330. package/dist/im/lark/ask-card.d.ts +23 -0
  331. package/dist/im/lark/ask-card.d.ts.map +1 -1
  332. package/dist/im/lark/ask-card.js +94 -4
  333. package/dist/im/lark/ask-card.js.map +1 -1
  334. package/dist/im/lark/card-builder.d.ts +6 -1
  335. package/dist/im/lark/card-builder.d.ts.map +1 -1
  336. package/dist/im/lark/card-builder.js +112 -15
  337. package/dist/im/lark/card-builder.js.map +1 -1
  338. package/dist/im/lark/card-handler.d.ts +1 -1
  339. package/dist/im/lark/card-handler.d.ts.map +1 -1
  340. package/dist/im/lark/card-handler.js +496 -310
  341. package/dist/im/lark/card-handler.js.map +1 -1
  342. package/dist/im/lark/client.d.ts +7 -0
  343. package/dist/im/lark/client.d.ts.map +1 -1
  344. package/dist/im/lark/client.js +39 -26
  345. package/dist/im/lark/client.js.map +1 -1
  346. package/dist/im/lark/doc-comment.d.ts +7 -4
  347. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  348. package/dist/im/lark/doc-comment.js +19 -6
  349. package/dist/im/lark/doc-comment.js.map +1 -1
  350. package/dist/im/lark/event-dispatcher.d.ts +16 -0
  351. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  352. package/dist/im/lark/event-dispatcher.js +88 -23
  353. package/dist/im/lark/event-dispatcher.js.map +1 -1
  354. package/dist/im/lark/forward-followup-content.d.ts.map +1 -1
  355. package/dist/im/lark/forward-followup-content.js +9 -1
  356. package/dist/im/lark/forward-followup-content.js.map +1 -1
  357. package/dist/im/lark/grant-command.d.ts.map +1 -1
  358. package/dist/im/lark/grant-command.js +7 -4
  359. package/dist/im/lark/grant-command.js.map +1 -1
  360. package/dist/im/lark/md-card.d.ts +11 -0
  361. package/dist/im/lark/md-card.d.ts.map +1 -1
  362. package/dist/im/lark/md-card.js +46 -0
  363. package/dist/im/lark/md-card.js.map +1 -1
  364. package/dist/im/lark/message-parser.d.ts +23 -2
  365. package/dist/im/lark/message-parser.d.ts.map +1 -1
  366. package/dist/im/lark/message-parser.js +58 -3
  367. package/dist/im/lark/message-parser.js.map +1 -1
  368. package/dist/im/lark/overview-card.d.ts.map +1 -1
  369. package/dist/im/lark/overview-card.js +2 -1
  370. package/dist/im/lark/overview-card.js.map +1 -1
  371. package/dist/services/async-trigger-state.d.ts +6 -1
  372. package/dist/services/async-trigger-state.d.ts.map +1 -1
  373. package/dist/services/async-trigger-state.js +21 -0
  374. package/dist/services/async-trigger-state.js.map +1 -1
  375. package/dist/services/async-trigger-store.d.ts +23 -1
  376. package/dist/services/async-trigger-store.d.ts.map +1 -1
  377. package/dist/services/async-trigger-store.js +96 -13
  378. package/dist/services/async-trigger-store.js.map +1 -1
  379. package/dist/services/bridge-fallback-gate.d.ts +40 -0
  380. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  381. package/dist/services/bridge-fallback-gate.js +124 -24
  382. package/dist/services/bridge-fallback-gate.js.map +1 -1
  383. package/dist/services/codex-app-runner-protocol.d.ts +8 -0
  384. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
  385. package/dist/services/codex-app-runner-protocol.js +9 -2
  386. package/dist/services/codex-app-runner-protocol.js.map +1 -1
  387. package/dist/services/codex-bridge-queue.d.ts +1 -0
  388. package/dist/services/codex-bridge-queue.d.ts.map +1 -1
  389. package/dist/services/codex-bridge-queue.js +1 -0
  390. package/dist/services/codex-bridge-queue.js.map +1 -1
  391. package/dist/services/codex-service-tier.d.ts +2 -0
  392. package/dist/services/codex-service-tier.d.ts.map +1 -1
  393. package/dist/services/codex-service-tier.js +1 -0
  394. package/dist/services/codex-service-tier.js.map +1 -1
  395. package/dist/services/codex-transcript.d.ts +27 -0
  396. package/dist/services/codex-transcript.d.ts.map +1 -1
  397. package/dist/services/codex-transcript.js +340 -2
  398. package/dist/services/codex-transcript.js.map +1 -1
  399. package/dist/services/grant-policy.d.ts +3 -0
  400. package/dist/services/grant-policy.d.ts.map +1 -1
  401. package/dist/services/grant-policy.js +5 -0
  402. package/dist/services/grant-policy.js.map +1 -1
  403. package/dist/services/grant-prefs-store.d.ts +6 -3
  404. package/dist/services/grant-prefs-store.d.ts.map +1 -1
  405. package/dist/services/grant-prefs-store.js +42 -12
  406. package/dist/services/grant-prefs-store.js.map +1 -1
  407. package/dist/services/hook-runner.d.ts +20 -1
  408. package/dist/services/hook-runner.d.ts.map +1 -1
  409. package/dist/services/hook-runner.js +17 -13
  410. package/dist/services/hook-runner.js.map +1 -1
  411. package/dist/services/idempotency-store.d.ts +134 -0
  412. package/dist/services/idempotency-store.d.ts.map +1 -0
  413. package/dist/services/idempotency-store.js +299 -0
  414. package/dist/services/idempotency-store.js.map +1 -0
  415. package/dist/services/jsonl-cursor.d.ts +7 -3
  416. package/dist/services/jsonl-cursor.d.ts.map +1 -1
  417. package/dist/services/jsonl-cursor.js +55 -31
  418. package/dist/services/jsonl-cursor.js.map +1 -1
  419. package/dist/services/open-platform-rename.d.ts.map +1 -1
  420. package/dist/services/open-platform-rename.js +1 -108
  421. package/dist/services/open-platform-rename.js.map +1 -1
  422. package/dist/services/restart-intent-store.d.ts +22 -2
  423. package/dist/services/restart-intent-store.d.ts.map +1 -1
  424. package/dist/services/restart-intent-store.js +160 -18
  425. package/dist/services/restart-intent-store.js.map +1 -1
  426. package/dist/services/sandbox-store.d.ts +22 -0
  427. package/dist/services/sandbox-store.d.ts.map +1 -1
  428. package/dist/services/sandbox-store.js +37 -8
  429. package/dist/services/sandbox-store.js.map +1 -1
  430. package/dist/services/send-policy.d.ts +41 -33
  431. package/dist/services/send-policy.d.ts.map +1 -1
  432. package/dist/services/send-policy.js +39 -40
  433. package/dist/services/send-policy.js.map +1 -1
  434. package/dist/services/session-store.d.ts +66 -0
  435. package/dist/services/session-store.d.ts.map +1 -1
  436. package/dist/services/session-store.js +403 -67
  437. package/dist/services/session-store.js.map +1 -1
  438. package/dist/services/submit-notification.d.ts +3 -0
  439. package/dist/services/submit-notification.d.ts.map +1 -0
  440. package/dist/services/submit-notification.js +15 -0
  441. package/dist/services/submit-notification.js.map +1 -0
  442. package/dist/services/traex-transcript.d.ts +25 -2
  443. package/dist/services/traex-transcript.d.ts.map +1 -1
  444. package/dist/services/traex-transcript.js +133 -1
  445. package/dist/services/traex-transcript.js.map +1 -1
  446. package/dist/services/trigger-types.d.ts +13 -1
  447. package/dist/services/trigger-types.d.ts.map +1 -1
  448. package/dist/services/trigger-types.js +39 -0
  449. package/dist/services/trigger-types.js.map +1 -1
  450. package/dist/services/voice/index.d.ts +3 -2
  451. package/dist/services/voice/index.d.ts.map +1 -1
  452. package/dist/services/voice/index.js +5 -5
  453. package/dist/services/voice/index.js.map +1 -1
  454. package/dist/services/voice/openai.d.ts +2 -1
  455. package/dist/services/voice/openai.d.ts.map +1 -1
  456. package/dist/services/voice/openai.js +2 -1
  457. package/dist/services/voice/openai.js.map +1 -1
  458. package/dist/services/voice/sami.d.ts +5 -2
  459. package/dist/services/voice/sami.d.ts.map +1 -1
  460. package/dist/services/voice/sami.js +19 -4
  461. package/dist/services/voice/sami.js.map +1 -1
  462. package/dist/setup/bots-store.d.ts.map +1 -1
  463. package/dist/setup/bots-store.js +10 -4
  464. package/dist/setup/bots-store.js.map +1 -1
  465. package/dist/setup/lark-scopes.json +2 -0
  466. package/dist/setup/open-platform-automation.d.ts +9 -1
  467. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  468. package/dist/setup/open-platform-automation.js +44 -15
  469. package/dist/setup/open-platform-automation.js.map +1 -1
  470. package/dist/setup/open-platform-visibility.d.ts +42 -0
  471. package/dist/setup/open-platform-visibility.d.ts.map +1 -0
  472. package/dist/setup/open-platform-visibility.js +133 -0
  473. package/dist/setup/open-platform-visibility.js.map +1 -0
  474. package/dist/setup/verify-permissions.d.ts.map +1 -1
  475. package/dist/setup/verify-permissions.js +6 -0
  476. package/dist/setup/verify-permissions.js.map +1 -1
  477. package/dist/skills/definitions.js +2 -2
  478. package/dist/types.d.ts +290 -13
  479. package/dist/types.d.ts.map +1 -1
  480. package/dist/utils/bot-routing.d.ts +1 -0
  481. package/dist/utils/bot-routing.d.ts.map +1 -1
  482. package/dist/utils/bot-routing.js +1 -1
  483. package/dist/utils/bot-routing.js.map +1 -1
  484. package/dist/utils/child-env.d.ts +1 -1
  485. package/dist/utils/child-env.d.ts.map +1 -1
  486. package/dist/utils/child-env.js +9 -0
  487. package/dist/utils/child-env.js.map +1 -1
  488. package/dist/utils/codex-app-control.d.ts +438 -0
  489. package/dist/utils/codex-app-control.d.ts.map +1 -0
  490. package/dist/utils/codex-app-control.js +2336 -0
  491. package/dist/utils/codex-app-control.js.map +1 -0
  492. package/dist/utils/codex-app-dispatch-ledger.d.ts +78 -0
  493. package/dist/utils/codex-app-dispatch-ledger.d.ts.map +1 -0
  494. package/dist/utils/codex-app-dispatch-ledger.js +149 -0
  495. package/dist/utils/codex-app-dispatch-ledger.js.map +1 -0
  496. package/dist/utils/codex-app-turn-dispatch.d.ts +77 -0
  497. package/dist/utils/codex-app-turn-dispatch.d.ts.map +1 -0
  498. package/dist/utils/codex-app-turn-dispatch.js +141 -0
  499. package/dist/utils/codex-app-turn-dispatch.js.map +1 -0
  500. package/dist/utils/codex-app-turn-liveness.d.ts +125 -0
  501. package/dist/utils/codex-app-turn-liveness.d.ts.map +1 -0
  502. package/dist/utils/codex-app-turn-liveness.js +301 -0
  503. package/dist/utils/codex-app-turn-liveness.js.map +1 -0
  504. package/dist/utils/idle-detector.d.ts +12 -0
  505. package/dist/utils/idle-detector.d.ts.map +1 -1
  506. package/dist/utils/idle-detector.js +19 -0
  507. package/dist/utils/idle-detector.js.map +1 -1
  508. package/dist/utils/input-gate.d.ts +87 -0
  509. package/dist/utils/input-gate.d.ts.map +1 -1
  510. package/dist/utils/input-gate.js +86 -0
  511. package/dist/utils/input-gate.js.map +1 -1
  512. package/dist/utils/lark-upload.d.ts.map +1 -1
  513. package/dist/utils/lark-upload.js +52 -1
  514. package/dist/utils/lark-upload.js.map +1 -1
  515. package/dist/utils/pending-input-queue.d.ts +15 -0
  516. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  517. package/dist/utils/pending-input-queue.js +5 -1
  518. package/dist/utils/pending-input-queue.js.map +1 -1
  519. package/dist/utils/user-token.d.ts.map +1 -1
  520. package/dist/utils/user-token.js +53 -2
  521. package/dist/utils/user-token.js.map +1 -1
  522. package/dist/worker-ipc-preload.d.ts +12 -0
  523. package/dist/worker-ipc-preload.d.ts.map +1 -0
  524. package/dist/worker-ipc-preload.js +44 -0
  525. package/dist/worker-ipc-preload.js.map +1 -0
  526. package/dist/worker.js +2604 -325
  527. package/dist/worker.js.map +1 -1
  528. package/dist/workflows/v3/session-relay-client.d.ts +1 -0
  529. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
  530. package/dist/workflows/v3/session-relay-client.js +12 -3
  531. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  532. package/package.json +1 -1
  533. package/dist/dashboard-web/chunks/bot-defaults-page-LTVOUDIS.js +0 -10
  534. package/dist/dashboard-web/chunks/chunk-DCS35UMD.js +0 -1
  535. package/dist/dashboard-web/chunks/chunk-MFQPCVHY.js +0 -51
  536. package/dist/dashboard-web/chunks/connectors-page-5KWDIMKC.js +0 -1
  537. package/dist/dashboard-web/chunks/groups-page-FHOKYXMW.js +0 -8
  538. package/dist/dashboard-web/chunks/overview-page-QYI3KBDD.js +0 -1
  539. package/dist/dashboard-web/chunks/sessions-page-WBP4N7D2.js +0 -6
@@ -0,0 +1,2336 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, verify, } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { chmodSync, closeSync, constants as fsConstants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeSync, } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, isAbsolute, join, win32 } from 'node:path';
6
+ /**
7
+ * The launch boundary carries only a path to an owner-only, read-once file.
8
+ * That file contains a fresh Ed25519 private key. The runner consumes and
9
+ * unlinks it before app-server (and therefore model tools) can start. Only the
10
+ * public key is persisted by the worker.
11
+ */
12
+ export const CODEX_APP_CONTROL_BOOTSTRAP_ENV = 'BOTMUX_CODEX_APP_CONTROL_BOOTSTRAP';
13
+ export const CODEX_APP_CONTROL_LINE_MAX_BYTES = 4_096;
14
+ export const CODEX_APP_CONTROL_FINAL_MAX_BYTES = 1_048_576;
15
+ export const CODEX_APP_CONTROL_FINAL_CHUNK_BYTES = 1_536;
16
+ /** Keep control bootstrap/proof alive for the worker's existing cold-start cap. */
17
+ export const CODEX_APP_CONTROL_STARTUP_TIMEOUT_MS = 90_000;
18
+ /** Absolute per-connection challenge/accept deadline; transport activity does not extend it. */
19
+ export const CODEX_APP_CONTROL_HANDSHAKE_TIMEOUT_MS = 5_000;
20
+ const CONTROL_STATE_VERSION = 3;
21
+ const CONTROL_WIRE_VERSION = 2;
22
+ const CONTROL_BOOTSTRAP_VERSION = 3;
23
+ const CONTROL_LOCATOR_VERSION = 1;
24
+ const PRIVATE_DIRECTORY_MODE = 0o700;
25
+ const PRIVATE_FILE_MODE = 0o600;
26
+ const BOOTSTRAP_MAX_BYTES = 8_192;
27
+ const LOCATOR_MAX_BYTES = 4_096;
28
+ const WINDOWS_SYSTEM_SID = 'S-1-5-18';
29
+ const WINDOWS_PIPE_PREFIX = '\\\\?\\pipe\\botmux-codex-app-';
30
+ const WINDOWS_OWNER_PIPE_PREFIX = '\\\\?\\pipe\\botmux-codex-app-owner-';
31
+ const POSIX_SOCKET_LEAF_RE = /^endpoint-[a-f0-9]{32}\.sock$/;
32
+ const POSIX_OWNER_RECORD_RE = /^owner-[a-f0-9]{64}\.json$/;
33
+ const POSIX_OWNER_PENDING_RE = /^owner-[a-f0-9]{64}\.pending$/;
34
+ const POSIX_REAPER_RECORD_RE = /^reap-[a-f0-9]{64}\.json$/;
35
+ const POSIX_REAPER_PENDING_RE = /^reap-[a-f0-9]{64}\.pending$/;
36
+ const POSIX_DIRECTORY_GENERATION_RE = /^generation-([a-f0-9]{64})\.json$/;
37
+ const POSIX_LEASE_RECORD_MAX_BYTES = 4_096;
38
+ const windowsHardenedDirectories = new Set();
39
+ /**
40
+ * Arm the shared Codex App startup deadline. Keeping the scheduling in one
41
+ * helper prevents the runner auth gate, bootstrap cleanup, worker proof gate,
42
+ * and first-prompt hard cap from silently drifting to different lifetimes.
43
+ */
44
+ export function armCodexAppControlStartupTimeout(onTimeout, timeoutMs = CODEX_APP_CONTROL_STARTUP_TIMEOUT_MS) {
45
+ return setTimeout(onTimeout, timeoutMs);
46
+ }
47
+ export function armCodexAppControlHandshakeTimeout(onTimeout, timeoutMs = CODEX_APP_CONTROL_HANDSHAKE_TIMEOUT_MS) {
48
+ return setTimeout(onTimeout, timeoutMs);
49
+ }
50
+ /** Authentication and a signed busy/idle bit are not sufficient readiness.
51
+ * The runner must explicitly assert that its initialized stdin/app-server path
52
+ * accepts input. Missing/non-boolean values are protocol violations; `false`
53
+ * is a valid not-ready state that leaves the absolute proof deadline armed. */
54
+ export function codexAppSignedStateReadiness(payload) {
55
+ if (!payload || typeof payload !== 'object')
56
+ return 'invalid';
57
+ const acceptingInput = payload.acceptingInput;
58
+ if (typeof acceptingInput !== 'boolean')
59
+ return 'invalid';
60
+ return acceptingInput ? 'ready' : 'waiting';
61
+ }
62
+ /** Re-armable proof deadline shared by startup and authenticated disconnects. */
63
+ export class CodexAppControlProofDeadline {
64
+ timer;
65
+ arm(onTimeout, timeoutMs = CODEX_APP_CONTROL_STARTUP_TIMEOUT_MS) {
66
+ this.clear();
67
+ this.timer = armCodexAppControlStartupTimeout(() => {
68
+ this.timer = undefined;
69
+ onTimeout();
70
+ }, timeoutMs);
71
+ this.timer.unref?.();
72
+ }
73
+ clear() {
74
+ if (this.timer)
75
+ clearTimeout(this.timer);
76
+ this.timer = undefined;
77
+ }
78
+ get armed() {
79
+ return this.timer !== undefined;
80
+ }
81
+ }
82
+ /**
83
+ * Coalesce one signed control record while its worker-side effects are still
84
+ * awaiting a durable daemon ACK. Endpoint replacement can replay the same
85
+ * generation/sequence before the replay window is committed; every replay
86
+ * must observe the first application result instead of running queue or
87
+ * liveness effects again.
88
+ *
89
+ * The caller releases the entry only after committing the replay window (or
90
+ * after a rejected application). Keeping release outside the promise closes
91
+ * the resolution -> replay-commit gap.
92
+ */
93
+ export class CodexAppControlRecordApplicationGate {
94
+ inFlight = new Map();
95
+ run(generation, seq, apply) {
96
+ const key = this.key(generation, seq);
97
+ const existing = this.inFlight.get(key);
98
+ if (existing)
99
+ return existing;
100
+ const application = Promise.resolve().then(apply);
101
+ this.inFlight.set(key, application);
102
+ return application;
103
+ }
104
+ release(generation, seq, application) {
105
+ const key = this.key(generation, seq);
106
+ if (this.inFlight.get(key) === application)
107
+ this.inFlight.delete(key);
108
+ }
109
+ key(generation, seq) {
110
+ return `${generation}\0${seq}`;
111
+ }
112
+ }
113
+ /** Authentication alone is not readiness. Until the active generation has
114
+ * published a signed accepting-input state, an idle screen is fail-closed to
115
+ * working so reconnect replay cannot expose a false-idle interval. */
116
+ export function projectCodexAppControlReadinessStatus(base, readiness) {
117
+ if (base !== 'idle')
118
+ return base;
119
+ return readiness.controlProven
120
+ && readiness.signedStateObserved
121
+ && readiness.inputReady
122
+ ? base
123
+ : 'working';
124
+ }
125
+ const STATE_MAX_BYTES = 16_384;
126
+ const GENERATION_RE = /^[A-Za-z0-9_-]{43}$/;
127
+ const CHALLENGE_RE = /^[A-Za-z0-9_-]{43}$/;
128
+ const KEY_RE = /^[A-Za-z0-9_-]{32,512}$/;
129
+ const SIGNATURE_RE = /^[A-Za-z0-9_-]{86}$/;
130
+ const MAX_STATE_IDENTITIES = 4;
131
+ export function codexAppControlFilesystemPolicy(platform = process.platform) {
132
+ if (platform === 'win32') {
133
+ return {
134
+ useNoFollow: false,
135
+ verifyUid: false,
136
+ verifyExactMode: false,
137
+ chmodAfterCreate: false,
138
+ verifyPostUnlinkLinkCount: false,
139
+ fsyncDirectory: false,
140
+ };
141
+ }
142
+ return {
143
+ useNoFollow: true,
144
+ verifyUid: true,
145
+ verifyExactMode: true,
146
+ chmodAfterCreate: true,
147
+ verifyPostUnlinkLinkCount: true,
148
+ fsyncDirectory: true,
149
+ };
150
+ }
151
+ function ownerUid() {
152
+ return process.geteuid?.() ?? process.getuid?.();
153
+ }
154
+ function noFollowFlag(platform) {
155
+ if (!codexAppControlFilesystemPolicy(platform).useNoFollow)
156
+ return 0;
157
+ const flag = fsConstants.O_NOFOLLOW;
158
+ if (typeof flag !== 'number')
159
+ throw new Error('O_NOFOLLOW is required for Codex App control files');
160
+ return flag;
161
+ }
162
+ function exactMode(mode) {
163
+ return mode & 0o777;
164
+ }
165
+ function assertOwned(stat, label, platform) {
166
+ if (!codexAppControlFilesystemPolicy(platform).verifyUid)
167
+ return;
168
+ const uid = ownerUid();
169
+ if (uid !== undefined && stat.uid !== uid)
170
+ throw new Error(`${label} must be owned by the current uid`);
171
+ }
172
+ function isAbsoluteForPlatform(path, platform) {
173
+ return platform === 'win32' ? win32.isAbsolute(path) : isAbsolute(path);
174
+ }
175
+ function joinForPlatform(platform, ...parts) {
176
+ return platform === 'win32' ? win32.join(...parts) : join(...parts);
177
+ }
178
+ function dirnameForPlatform(platform, path) {
179
+ return platform === 'win32' ? win32.dirname(path) : dirname(path);
180
+ }
181
+ /**
182
+ * Windows never places control material under SESSION_DATA_DIR: that setting
183
+ * is bot/user supplied and may point at a shared tree. The fixed root is
184
+ * instead anchored in the current Windows profile and hardened before any
185
+ * secret or locator is created beneath it.
186
+ */
187
+ export function codexAppWindowsControlRoot(options = {}) {
188
+ const base = options.localAppData?.trim()
189
+ || (options.platform === 'win32' || options.platform === undefined
190
+ ? process.env.LOCALAPPDATA?.trim()
191
+ : undefined)
192
+ || options.homeDirectory?.trim()
193
+ || homedir();
194
+ // The locator protocol relies on local NTFS-style ACL and atomic-replace
195
+ // semantics. A UNC/redirected profile can have server-defined ACL and
196
+ // rename behavior, so do not silently place capabilities on it. The drive
197
+ // root itself remains configurable through LOCALAPPDATA/home for normal
198
+ // per-user profiles.
199
+ if (!base || !/^[A-Za-z]:[\\/]/.test(base) || !win32.isAbsolute(base)) {
200
+ throw new Error('Windows Codex App control root requires a local drive-qualified LOCALAPPDATA or home directory');
201
+ }
202
+ return win32.join(base, 'Botmux', 'codex-app-control');
203
+ }
204
+ function parseStrictCsvRow(row) {
205
+ const fields = [];
206
+ let cursor = 0;
207
+ while (cursor < row.length) {
208
+ if (row[cursor] !== '"')
209
+ return undefined;
210
+ cursor++;
211
+ let field = '';
212
+ let closed = false;
213
+ while (cursor < row.length) {
214
+ const ch = row[cursor++];
215
+ if (ch !== '"') {
216
+ field += ch;
217
+ continue;
218
+ }
219
+ if (row[cursor] === '"') {
220
+ field += '"';
221
+ cursor++;
222
+ continue;
223
+ }
224
+ closed = true;
225
+ break;
226
+ }
227
+ if (!closed)
228
+ return undefined;
229
+ fields.push(field);
230
+ if (cursor === row.length)
231
+ break;
232
+ if (row[cursor] !== ',')
233
+ return undefined;
234
+ cursor++;
235
+ if (cursor === row.length)
236
+ return undefined;
237
+ }
238
+ return fields;
239
+ }
240
+ export function parseWindowsCurrentSid(output) {
241
+ const rows = output.replace(/^\uFEFF/, '').split(/\r?\n/).filter(row => row.length > 0);
242
+ if (rows.length !== 1)
243
+ return undefined;
244
+ const fields = parseStrictCsvRow(rows[0]);
245
+ if (fields?.length !== 2)
246
+ return undefined;
247
+ const sid = fields[1];
248
+ return /^S-\d-(?:\d+-)+\d+$/i.test(sid) ? sid.toUpperCase() : undefined;
249
+ }
250
+ export function decodeWindowsAclSnapshot(raw) {
251
+ if (raw.length >= 2 && raw[0] === 0xff && raw[1] === 0xfe) {
252
+ return raw.subarray(2).toString('utf16le');
253
+ }
254
+ if (raw.length >= 4 && raw[1] === 0 && raw[3] === 0)
255
+ return raw.toString('utf16le');
256
+ return raw.toString('utf8');
257
+ }
258
+ function parseWindowsDaclSnapshot(snapshot) {
259
+ // `/save` layout is not a documented API. Locate the SDDL token by grammar,
260
+ // whether it follows the path on the same or next line. `D:\path` cannot
261
+ // match because a DACL has only uppercase control flags before its first ACE.
262
+ const daclMatch = snapshot.match(/D:[A-Z]*\([^\r\n]*/);
263
+ if (!daclMatch)
264
+ return undefined;
265
+ let dacl = daclMatch[0].slice(2).trim();
266
+ const saclStart = dacl.indexOf('S:');
267
+ if (saclStart >= 0)
268
+ dacl = dacl.slice(0, saclStart);
269
+ const firstAce = dacl.indexOf('(');
270
+ if (firstAce < 0)
271
+ return undefined;
272
+ const flags = dacl.slice(0, firstAce);
273
+ const aceText = dacl.slice(firstAce);
274
+ const aces = [];
275
+ let cursor = 0;
276
+ while (cursor < aceText.length) {
277
+ if (aceText[cursor] !== '(')
278
+ return undefined;
279
+ const end = aceText.indexOf(')', cursor + 1);
280
+ if (end < 0)
281
+ return undefined;
282
+ const fields = aceText.slice(cursor + 1, end).split(';');
283
+ if (fields.length !== 6)
284
+ return undefined;
285
+ aces.push({
286
+ type: fields[0],
287
+ flags: fields[1],
288
+ rights: fields[2],
289
+ objectGuid: fields[3],
290
+ inheritObjectGuid: fields[4],
291
+ trustee: fields[5],
292
+ });
293
+ cursor = end + 1;
294
+ }
295
+ return { flags, aces };
296
+ }
297
+ /** Locale-independent verification of the SDDL emitted by `icacls /save`. */
298
+ export function verifyWindowsCodexAppControlDacl(snapshot, currentSid, kind = 'directory') {
299
+ const parsed = parseWindowsDaclSnapshot(snapshot);
300
+ if (!parsed || !parsed.flags.includes('P'))
301
+ return false;
302
+ // P = protected DACL. AI/AR are canonical auto-inheritance metadata that
303
+ // icacls may retain even after inherited ACEs are removed; reject anything
304
+ // else and separately reject every inherited ACE below.
305
+ if (parsed.flags.replace(/AI|AR|P/g, ''))
306
+ return false;
307
+ const expected = new Set(currentSid.toUpperCase() === WINDOWS_SYSTEM_SID
308
+ ? [WINDOWS_SYSTEM_SID]
309
+ : [currentSid.toUpperCase(), WINDOWS_SYSTEM_SID]);
310
+ if (parsed.aces.length !== expected.size)
311
+ return false;
312
+ for (const ace of parsed.aces) {
313
+ const trustee = ace.trustee.toUpperCase() === 'SY'
314
+ ? WINDOWS_SYSTEM_SID
315
+ : ace.trustee.toUpperCase();
316
+ if (ace.type !== 'A' || ace.rights !== 'FA' || ace.objectGuid || ace.inheritObjectGuid
317
+ || ace.flags.includes('ID') || !expected.delete(trustee))
318
+ return false;
319
+ if (kind === 'directory') {
320
+ if (!ace.flags.includes('OI') || !ace.flags.includes('CI')
321
+ || ace.flags.replace(/OI|CI/g, ''))
322
+ return false;
323
+ }
324
+ else if (ace.flags)
325
+ return false;
326
+ }
327
+ return expected.size === 0;
328
+ }
329
+ function defaultWindowsControlCommandRunner(command, args) {
330
+ return spawnSync(command, args, {
331
+ encoding: 'utf8',
332
+ windowsHide: true,
333
+ shell: false,
334
+ timeout: 10_000,
335
+ maxBuffer: 1_048_576,
336
+ });
337
+ }
338
+ function runWindowsControlCommand(runner, command, args, label) {
339
+ const result = runner(command, args);
340
+ if (result.error || result.status !== 0) {
341
+ throw new Error(`${label} failed closed (status=${String(result.status)})`);
342
+ }
343
+ return result.stdout ?? '';
344
+ }
345
+ /**
346
+ * Remove inherited ACLs, grant only the current SID and SYSTEM full control,
347
+ * then validate the resulting protected DACL via locale-independent SDDL.
348
+ * Unknown explicit ACEs are never silently retained: verification fails.
349
+ */
350
+ function hardenWindowsCodexAppControlAcl(path, kind, runner = defaultWindowsControlCommandRunner) {
351
+ if (!path || !win32.isAbsolute(path)) {
352
+ throw new Error(`Windows Codex App control ${kind} must be absolute`);
353
+ }
354
+ if (kind === 'directory')
355
+ mkdirSync(path, { recursive: true });
356
+ const stat = lstatSync(path);
357
+ if ((kind === 'directory' ? !stat.isDirectory() : !stat.isFile())
358
+ || stat.isSymbolicLink() || (kind === 'file' && stat.nlink !== 1)) {
359
+ throw new Error(`Windows Codex App control ${kind} must be a real single-link ${kind}`);
360
+ }
361
+ const systemRoot = process.env.SystemRoot?.trim() || process.env.WINDIR?.trim();
362
+ if (!systemRoot || !win32.isAbsolute(systemRoot)) {
363
+ throw new Error('Windows SystemRoot is required for trusted ACL tools');
364
+ }
365
+ const whoamiCommand = win32.join(systemRoot, 'System32', 'whoami.exe');
366
+ const icaclsCommand = win32.join(systemRoot, 'System32', 'icacls.exe');
367
+ const whoami = runWindowsControlCommand(runner, whoamiCommand, ['/user', '/fo', 'csv', '/nh'], 'Windows current SID lookup');
368
+ const sid = parseWindowsCurrentSid(whoami);
369
+ if (!sid)
370
+ throw new Error('Windows current SID lookup returned no SID');
371
+ let lastVerificationError;
372
+ // Several workers can harden the shared per-user root concurrently. The
373
+ // transaction is idempotent; bounded full retries avoid snapshotting another
374
+ // worker's inheritance-removal→grant gap without ever accepting a loose ACL.
375
+ for (let attempt = 0; attempt < 3; attempt++) {
376
+ const snapshotPath = win32.join(kind === 'directory' ? path : win32.dirname(path), `.botmux-acl-${process.pid}-${randomBytes(16).toString('hex')}.txt`);
377
+ try {
378
+ runWindowsControlCommand(runner, icaclsCommand, [path, '/setowner', `*${sid}`, '/q'], 'Windows control owner assignment');
379
+ runWindowsControlCommand(runner, icaclsCommand, [path, '/inheritance:r', '/q'], 'Windows control ACL inheritance removal');
380
+ runWindowsControlCommand(runner, icaclsCommand, [path, '/grant:r', `*${sid}:${kind === 'directory' ? '(OI)(CI)F' : 'F'}`,
381
+ ...(sid === WINDOWS_SYSTEM_SID
382
+ ? []
383
+ : [`*${WINDOWS_SYSTEM_SID}:${kind === 'directory' ? '(OI)(CI)F' : 'F'}`]), '/q'], 'Windows control ACL grant');
384
+ runWindowsControlCommand(runner, icaclsCommand, [path, '/verify', '/q'], 'Windows control ACL canonical verification');
385
+ runWindowsControlCommand(runner, icaclsCommand, [path, '/save', snapshotPath, '/q'], 'Windows control ACL snapshot');
386
+ const snapshot = decodeWindowsAclSnapshot(readFileSync(snapshotPath));
387
+ if (verifyWindowsCodexAppControlDacl(snapshot, sid, kind))
388
+ return;
389
+ lastVerificationError = new Error(`Windows Codex App control ${kind} ACL is not exactly current SID + SYSTEM`);
390
+ }
391
+ catch (err) {
392
+ lastVerificationError = err instanceof Error ? err : new Error(String(err));
393
+ }
394
+ finally {
395
+ try {
396
+ unlinkSync(snapshotPath);
397
+ }
398
+ catch { /* no snapshot on command failure */ }
399
+ }
400
+ }
401
+ throw lastVerificationError ?? new Error(`Windows Codex App control ${kind} ACL verification failed`);
402
+ }
403
+ /**
404
+ * Remove inherited ACLs, set the current SID as owner, grant only that SID and
405
+ * SYSTEM full control, and verify the protected DACL. Unknown explicit ACEs
406
+ * are retained by icacls but cause exact verification to fail closed.
407
+ */
408
+ export function hardenWindowsCodexAppControlDirectory(directory, runner = defaultWindowsControlCommandRunner) {
409
+ hardenWindowsCodexAppControlAcl(directory, 'directory', runner);
410
+ }
411
+ /** Validate/harden a pre-existing fixed-name state file before trusting it. */
412
+ export function hardenWindowsCodexAppControlFile(path, runner = defaultWindowsControlCommandRunner) {
413
+ hardenWindowsCodexAppControlAcl(path, 'file', runner);
414
+ }
415
+ /** Create/validate a non-symlink owner-only directory. */
416
+ export function ensureCodexAppControlDirectory(directory, platform = process.platform) {
417
+ if (!directory || !isAbsoluteForPlatform(directory, platform)) {
418
+ throw new Error('Codex App control directory must be absolute');
419
+ }
420
+ if (platform === 'win32') {
421
+ const cacheKey = win32.normalize(directory).toLowerCase();
422
+ if (windowsHardenedDirectories.has(cacheKey)) {
423
+ const stat = lstatSync(directory);
424
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
425
+ windowsHardenedDirectories.delete(cacheKey);
426
+ throw new Error('Windows Codex App control directory changed after ACL hardening');
427
+ }
428
+ return;
429
+ }
430
+ hardenWindowsCodexAppControlDirectory(directory);
431
+ // Re-running whoami/icacls for every atomic locator replacement would
432
+ // block the worker event loop and amplify the named-pipe DoS boundary.
433
+ // Once exact ACL verification succeeds, cross-user mutation is excluded;
434
+ // same-SID compromise is outside this control plane's trust boundary.
435
+ windowsHardenedDirectories.add(cacheKey);
436
+ return;
437
+ }
438
+ mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
439
+ const stat = lstatSync(directory);
440
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
441
+ throw new Error('Codex App control directory must be a real directory');
442
+ }
443
+ assertOwned(stat, 'Codex App control directory', platform);
444
+ if (exactMode(stat.mode) !== PRIVATE_DIRECTORY_MODE) {
445
+ chmodSync(directory, PRIVATE_DIRECTORY_MODE);
446
+ const tightened = lstatSync(directory);
447
+ assertOwned(tightened, 'Codex App control directory', platform);
448
+ if (!tightened.isDirectory() || tightened.isSymbolicLink()
449
+ || exactMode(tightened.mode) !== PRIVATE_DIRECTORY_MODE) {
450
+ throw new Error('Codex App control directory could not be secured to 0700');
451
+ }
452
+ }
453
+ }
454
+ function sessionKey(sessionId) {
455
+ return createHash('sha256').update(sessionId, 'utf8').digest('hex');
456
+ }
457
+ export function codexAppControlStatePath(sessionDataDir, sessionId) {
458
+ if (process.platform === 'win32') {
459
+ return win32.join(codexAppWindowsControlRoot(), 'state', `${sessionKey(sessionId)}.json`);
460
+ }
461
+ return join(sessionDataDir, 'codex-app-control', `${sessionKey(sessionId)}.json`);
462
+ }
463
+ export function codexAppControlStatePathForPlatform(sessionDataDir, sessionId, options = {}) {
464
+ const platform = options.platform ?? process.platform;
465
+ if (platform === 'win32') {
466
+ return win32.join(codexAppWindowsControlRoot(options), 'state', `${sessionKey(sessionId)}.json`);
467
+ }
468
+ return join(sessionDataDir, 'codex-app-control', `${sessionKey(sessionId)}.json`);
469
+ }
470
+ export function codexAppControlSocketPath(directory, sessionId) {
471
+ // Unix-domain paths are short on macOS. A fixed 32-hex leaf keeps the path
472
+ // comfortably below the platform limit for normal BOT_HOME/tmp roots.
473
+ return join(directory, `${sessionKey(sessionId).slice(0, 32)}.sock`);
474
+ }
475
+ export function codexAppPosixControlRoot(uid = process.getuid?.()) {
476
+ return join('/tmp', `botmux-codex-app-${uid ?? 'unknown'}`);
477
+ }
478
+ export function codexAppPosixOwnerLeaseDirectory(controlRoot, sessionId) {
479
+ return join(controlRoot, 'leases', `${sessionKey(sessionId).slice(0, 32)}.lease`);
480
+ }
481
+ export function generateCodexAppPosixSocketEndpoint(socketDirectory) {
482
+ return join(socketDirectory, `endpoint-${randomBytes(16).toString('hex')}.sock`);
483
+ }
484
+ export function codexAppControlLocatorPath(controlRoot, sessionId, platform = process.platform) {
485
+ return joinForPlatform(platform, controlRoot, 'locators', `${sessionKey(sessionId)}.json`);
486
+ }
487
+ export function generateCodexAppWindowsPipeEndpoint() {
488
+ return `${WINDOWS_PIPE_PREFIX}${randomBytes(32).toString('hex')}`;
489
+ }
490
+ /**
491
+ * A fixed coordination pipe is held for the lifetime of one Windows worker
492
+ * process. It is not a control endpoint and is never put in a runner locator.
493
+ * Its only purpose is to serialize locator publishers across the daemon's
494
+ * kill-then-fork overlap window. The OS releases it if the old worker is
495
+ * terminated, avoiding a crash-stale filesystem lock.
496
+ */
497
+ export function codexAppWindowsOwnerPipeEndpoint(sessionId) {
498
+ return `${WINDOWS_OWNER_PIPE_PREFIX}${sessionKey(sessionId)}`;
499
+ }
500
+ /** Retry only a competing bind; every other owner-lease error fails closed. */
501
+ export async function acquireCodexAppControlOwnerLease(options) {
502
+ const timeoutMs = options.timeoutMs ?? 10_000;
503
+ const retryDelayMs = options.retryDelayMs ?? 50;
504
+ const now = options.now ?? Date.now;
505
+ const wait = options.wait ?? (delayMs => new Promise(resolve => setTimeout(resolve, delayMs)));
506
+ const deadline = now() + timeoutMs;
507
+ let lastBusyError;
508
+ while (true) {
509
+ try {
510
+ return await options.bind();
511
+ }
512
+ catch (err) {
513
+ if (err?.code !== 'EADDRINUSE')
514
+ throw err;
515
+ lastBusyError = err;
516
+ }
517
+ if (now() >= deadline) {
518
+ throw lastBusyError instanceof Error
519
+ ? lastBusyError
520
+ : new Error('Windows Codex App control owner lease timed out');
521
+ }
522
+ await wait(Math.min(retryDelayMs, Math.max(0, deadline - now())));
523
+ }
524
+ }
525
+ function linuxProcessStartToken(raw) {
526
+ const closeParen = raw.lastIndexOf(')');
527
+ if (closeParen < 0)
528
+ return undefined;
529
+ const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
530
+ return fields[19] || undefined;
531
+ }
532
+ export function codexAppPosixProcessProbeEnv(base = process.env) {
533
+ // `ps -o lstart` is formatted through locale and timezone. Pin both so the
534
+ // same live process cannot acquire a different token after the worker's
535
+ // ambient LC_*/TZ changes. Preserve PATH and the remaining launch context.
536
+ return { ...base, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' };
537
+ }
538
+ export function readCodexAppProcessStartToken(pid, platform = process.platform) {
539
+ if (!Number.isSafeInteger(pid) || pid <= 0 || platform === 'win32')
540
+ return undefined;
541
+ if (platform === 'linux') {
542
+ try {
543
+ return linuxProcessStartToken(readFileSync(`/proc/${pid}/stat`, 'utf8'));
544
+ }
545
+ catch {
546
+ return undefined;
547
+ }
548
+ }
549
+ const result = spawnSync('/bin/ps', ['-p', String(pid), '-o', 'lstart='], {
550
+ encoding: 'utf8',
551
+ env: codexAppPosixProcessProbeEnv(),
552
+ shell: false,
553
+ timeout: 5_000,
554
+ maxBuffer: 64 * 1024,
555
+ });
556
+ if (result.error || result.status !== 0)
557
+ return undefined;
558
+ const token = result.stdout.trim();
559
+ return token || undefined;
560
+ }
561
+ function inspectCodexAppOwnerProcess(pid, expectedStartToken, platform) {
562
+ try {
563
+ process.kill(pid, 0);
564
+ }
565
+ catch (err) {
566
+ const code = err.code;
567
+ if (code === 'ESRCH')
568
+ return 'dead';
569
+ // EPERM proves that a process occupies the pid. Any other probe failure is
570
+ // also fail-closed: it must not authorize stale-owner deletion.
571
+ if (code === 'EPERM')
572
+ return 'alive';
573
+ return 'unknown';
574
+ }
575
+ const actualStartToken = readCodexAppProcessStartToken(pid, platform);
576
+ if (!actualStartToken)
577
+ return 'unknown';
578
+ // lstart has one-second resolution on non-Linux POSIX. A same-second PID
579
+ // reuse can therefore look alive and block stale recovery, but cannot grant
580
+ // ownership or reap a live owner: the ambiguity remains fail-closed.
581
+ return actualStartToken === expectedStartToken ? 'alive' : 'dead';
582
+ }
583
+ function validPosixFileIdentity(value) {
584
+ if (!value || typeof value !== 'object')
585
+ return false;
586
+ const identity = value;
587
+ return typeof identity.dev === 'string' && /^(?:0|[1-9]\d*)$/.test(identity.dev)
588
+ && typeof identity.ino === 'string' && /^(?:0|[1-9]\d*)$/.test(identity.ino);
589
+ }
590
+ function validPosixDirectoryIdentity(value, requireGeneration) {
591
+ if (!validPosixFileIdentity(value))
592
+ return false;
593
+ const generation = value.generation;
594
+ return requireGeneration
595
+ ? typeof generation === 'string' && /^[a-f0-9]{64}$/.test(generation)
596
+ : generation === undefined;
597
+ }
598
+ function validPosixOwnerTarget(value) {
599
+ if (!value || typeof value !== 'object')
600
+ return false;
601
+ const target = value;
602
+ const processIdentityValid = (target.pid === null && target.processStartToken === null)
603
+ || (Number.isSafeInteger(target.pid) && target.pid > 0
604
+ && typeof target.processStartToken === 'string'
605
+ && target.processStartToken.length > 0 && target.processStartToken.length <= 256);
606
+ return typeof target.nonce === 'string' && /^[a-f0-9]{64}$/.test(target.nonce)
607
+ && validPosixFileIdentity(target.recordIdentity)
608
+ && processIdentityValid;
609
+ }
610
+ function validPosixOwnerRecord(value, sessionId, expectedRole) {
611
+ if (!value || typeof value !== 'object')
612
+ return false;
613
+ const record = value;
614
+ const roleValid = record.role === 'owner' || record.role === 'reaper';
615
+ const targetValid = record.role === 'owner'
616
+ ? record.targetOwner === undefined
617
+ : validPosixOwnerTarget(record.targetOwner);
618
+ const versionValid = record.version === 2
619
+ ? validPosixDirectoryIdentity(record.directoryIdentity, false)
620
+ : record.version === 3 && validPosixDirectoryIdentity(record.directoryIdentity, true);
621
+ return versionValid
622
+ && roleValid
623
+ && (expectedRole === undefined || record.role === expectedRole)
624
+ && record.sessionId === sessionId
625
+ && typeof record.nonce === 'string' && /^[a-f0-9]{64}$/.test(record.nonce)
626
+ && Number.isSafeInteger(record.pid) && record.pid > 0
627
+ && typeof record.processStartToken === 'string'
628
+ && record.processStartToken.length > 0 && record.processStartToken.length <= 256
629
+ && typeof record.createdAtMs === 'number' && Number.isFinite(record.createdAtMs)
630
+ && targetValid;
631
+ }
632
+ function readPosixOwnerRecord(path, sessionId, platform, expectedRole = 'owner') {
633
+ try {
634
+ const parsed = JSON.parse(readOwnedRegularFile(path, 4_096, 'Codex App POSIX owner record', platform));
635
+ return validPosixOwnerRecord(parsed, sessionId, expectedRole) ? parsed : undefined;
636
+ }
637
+ catch {
638
+ return undefined;
639
+ }
640
+ }
641
+ function errnoCode(err) {
642
+ return err?.code;
643
+ }
644
+ function posixLeaseRaceError(message) {
645
+ return Object.assign(new Error(message), { code: 'EAGAIN' });
646
+ }
647
+ function posixFileIdentity(stat) {
648
+ return { dev: stat.dev.toString(10), ino: stat.ino.toString(10) };
649
+ }
650
+ function samePosixFileIdentity(left, right) {
651
+ return left.dev === right.dev && left.ino === right.ino;
652
+ }
653
+ function samePosixStatIdentity(before, after) {
654
+ return before.dev === after.dev && before.ino === after.ino;
655
+ }
656
+ function exactBigIntMode(mode) {
657
+ return Number(mode & 511n);
658
+ }
659
+ function assertPosixOwned(stat, label, platform) {
660
+ if (!codexAppControlFilesystemPolicy(platform).verifyUid)
661
+ return;
662
+ const uid = ownerUid();
663
+ if (uid !== undefined && stat.uid !== BigInt(uid)) {
664
+ throw new Error(`${label} must be owned by the current uid`);
665
+ }
666
+ }
667
+ function assertPrivatePosixLeaseRecord(stat, label, platform) {
668
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n
669
+ || exactBigIntMode(stat.mode) !== PRIVATE_FILE_MODE) {
670
+ throw new Error(`${label} must be a single-link regular 0600 file`);
671
+ }
672
+ assertPosixOwned(stat, label, platform);
673
+ }
674
+ function posixLeaseRecordNonce(name) {
675
+ const match = name.match(/^(?:owner|reap)-([a-f0-9]{64})\.(?:json|pending)$/);
676
+ return match?.[1];
677
+ }
678
+ function posixLeaseRecordRole(name) {
679
+ if (POSIX_OWNER_RECORD_RE.test(name) || POSIX_OWNER_PENDING_RE.test(name))
680
+ return 'owner';
681
+ if (POSIX_REAPER_RECORD_RE.test(name) || POSIX_REAPER_PENDING_RE.test(name))
682
+ return 'reaper';
683
+ return undefined;
684
+ }
685
+ /**
686
+ * Observe one lease actor record without treating a crash-partial payload as
687
+ * insecure metadata. Wrong type/owner/mode/link count fails hard; a secure
688
+ * empty/truncated/oversized file is returned as a grace-reclaimable record.
689
+ */
690
+ function observePosixLeaseRecord(path, name, sessionId, platform, nowMs, label) {
691
+ let before;
692
+ try {
693
+ before = lstatSync(path, { bigint: true });
694
+ }
695
+ catch (err) {
696
+ if (errnoCode(err) === 'ENOENT')
697
+ return undefined;
698
+ throw err;
699
+ }
700
+ assertPrivatePosixLeaseRecord(before, label, platform);
701
+ const nonce = posixLeaseRecordNonce(name);
702
+ if (!nonce)
703
+ throw new Error(`${label} has an invalid filename`);
704
+ const role = posixLeaseRecordRole(name);
705
+ if (!role)
706
+ throw new Error(`${label} has an invalid role`);
707
+ let record;
708
+ if (before.size > 0n && before.size <= BigInt(POSIX_LEASE_RECORD_MAX_BYTES)) {
709
+ try {
710
+ const parsed = JSON.parse(readOwnedRegularFile(path, POSIX_LEASE_RECORD_MAX_BYTES, label, platform));
711
+ if (validPosixOwnerRecord(parsed, sessionId, role) && parsed.nonce === nonce) {
712
+ record = parsed;
713
+ }
714
+ }
715
+ catch (err) {
716
+ // JSON syntax failures are crash-partial data. A path race is handled by
717
+ // the post-read identity check below and never reclassified as stale.
718
+ if (errnoCode(err) === 'ENOENT')
719
+ return undefined;
720
+ }
721
+ }
722
+ let after;
723
+ try {
724
+ after = lstatSync(path, { bigint: true });
725
+ }
726
+ catch (err) {
727
+ if (errnoCode(err) === 'ENOENT')
728
+ return undefined;
729
+ throw err;
730
+ }
731
+ assertPrivatePosixLeaseRecord(after, label, platform);
732
+ if (!samePosixStatIdentity(before, after))
733
+ return undefined;
734
+ return {
735
+ path,
736
+ name,
737
+ nonce,
738
+ role,
739
+ stat: after,
740
+ ageMs: Math.max(0, nowMs - Number(after.mtimeMs)),
741
+ ...(record ? { record } : {}),
742
+ };
743
+ }
744
+ function observePosixLeaseDirectory(directory, platform) {
745
+ let before;
746
+ try {
747
+ before = lstatSync(directory, { bigint: true });
748
+ }
749
+ catch (err) {
750
+ if (errnoCode(err) === 'ENOENT')
751
+ return undefined;
752
+ throw err;
753
+ }
754
+ assertPosixLeaseDirectoryStat(before, platform);
755
+ let names;
756
+ try {
757
+ names = readdirSync(directory).sort();
758
+ }
759
+ catch (err) {
760
+ if (errnoCode(err) === 'ENOENT')
761
+ return undefined;
762
+ throw err;
763
+ }
764
+ let after;
765
+ try {
766
+ after = lstatSync(directory, { bigint: true });
767
+ }
768
+ catch (err) {
769
+ if (errnoCode(err) === 'ENOENT')
770
+ return undefined;
771
+ throw err;
772
+ }
773
+ if (!samePosixStatIdentity(before, after))
774
+ return undefined;
775
+ assertPosixLeaseDirectoryStat(after, platform);
776
+ const generationNames = names.filter(name => POSIX_DIRECTORY_GENERATION_RE.test(name));
777
+ const generationStats = new Map();
778
+ for (const candidateName of generationNames) {
779
+ const candidateGeneration = candidateName.match(POSIX_DIRECTORY_GENERATION_RE)?.[1];
780
+ const candidatePath = join(directory, candidateName);
781
+ try {
782
+ const candidateStat = lstatSync(candidatePath, { bigint: true });
783
+ assertPrivatePosixLeaseRecord(candidateStat, 'Codex App POSIX directory generation', platform);
784
+ const persistedGeneration = readOwnedRegularFile(candidatePath, 128, 'Codex App POSIX directory generation', platform);
785
+ const verifiedStat = lstatSync(candidatePath, { bigint: true });
786
+ assertPrivatePosixLeaseRecord(verifiedStat, 'Codex App POSIX directory generation', platform);
787
+ if (!candidateGeneration || persistedGeneration !== candidateGeneration
788
+ || !samePosixStatIdentity(candidateStat, verifiedStat)) {
789
+ throw new Error('Codex App POSIX directory generation verification failed');
790
+ }
791
+ generationStats.set(candidateName, verifiedStat);
792
+ }
793
+ catch (err) {
794
+ if (errnoCode(err) === 'ENOENT')
795
+ return undefined;
796
+ throw err;
797
+ }
798
+ }
799
+ if (generationNames.length > 1) {
800
+ return {
801
+ stat: after,
802
+ identity: posixFileIdentity(after),
803
+ names,
804
+ generationNames,
805
+ generationStats,
806
+ };
807
+ }
808
+ const generationName = generationNames[0];
809
+ if (!generationName)
810
+ return { stat: after, identity: posixFileIdentity(after), names };
811
+ const generation = generationName.match(POSIX_DIRECTORY_GENERATION_RE)?.[1];
812
+ return {
813
+ stat: after,
814
+ identity: { ...posixFileIdentity(after), generation },
815
+ names,
816
+ generationName,
817
+ generationStat: generationStats.get(generationName),
818
+ };
819
+ }
820
+ function createStagedPosixLeaseRecord(input) {
821
+ try {
822
+ createExclusiveFile(input.pendingPath, JSON.stringify(input.record), `${input.label} pending file`, input.platform);
823
+ // A complete synced inode becomes authoritative in one namespace step.
824
+ // SIGKILL during the write can therefore leave only a `.pending` file.
825
+ renameSync(input.pendingPath, input.finalPath);
826
+ fsyncDirectory(input.directory, input.platform);
827
+ const persisted = readPosixOwnerRecord(input.finalPath, input.sessionId, input.platform, input.record.role);
828
+ if (!persisted || persisted.nonce !== input.record.nonce) {
829
+ throw new Error(`${input.label} verification failed`);
830
+ }
831
+ }
832
+ finally {
833
+ try {
834
+ unlinkSync(input.pendingPath);
835
+ }
836
+ catch { /* renamed or crash-partial cleanup */ }
837
+ }
838
+ }
839
+ /**
840
+ * Rename the exact observed random-name inode out of the lease directory.
841
+ * Random actor filenames make this a compare-and-swap boundary: a concurrent
842
+ * cleaner can only receive ENOENT and can never unlink a successor's record.
843
+ */
844
+ function retireObservedPosixLeaseRecord(observed, leasesRoot, sessionId, platform) {
845
+ let current;
846
+ try {
847
+ current = lstatSync(observed.path, { bigint: true });
848
+ }
849
+ catch (err) {
850
+ if (errnoCode(err) === 'ENOENT')
851
+ return false;
852
+ throw err;
853
+ }
854
+ assertPrivatePosixLeaseRecord(current, 'Codex App POSIX lease record', platform);
855
+ if (!samePosixStatIdentity(observed.stat, current))
856
+ return false;
857
+ const retired = join(leasesRoot, `.retired-${sessionKey(sessionId).slice(0, 16)}-${observed.nonce}-${randomBytes(16).toString('hex')}`);
858
+ try {
859
+ renameSync(observed.path, retired);
860
+ }
861
+ catch (err) {
862
+ if (errnoCode(err) === 'ENOENT')
863
+ return false;
864
+ throw err;
865
+ }
866
+ try {
867
+ const moved = lstatSync(retired, { bigint: true });
868
+ assertPrivatePosixLeaseRecord(moved, 'Retired Codex App POSIX lease record', platform);
869
+ if (!samePosixStatIdentity(observed.stat, moved)) {
870
+ throw new Error('Codex App POSIX lease record changed during retirement');
871
+ }
872
+ }
873
+ finally {
874
+ try {
875
+ unlinkSync(retired);
876
+ }
877
+ catch { /* crash residue is outside the authority directory */ }
878
+ }
879
+ return true;
880
+ }
881
+ /** Remove the exact generation marker only while it is the directory's sole
882
+ * remaining entry. The marker gives each mkdir generation an unforgeable
883
+ * identity even when the filesystem immediately reuses the same dev+ino. */
884
+ function retirePosixDirectoryGeneration(directory, observed, leasesRoot, sessionId, platform) {
885
+ if (!observed.generationName || !observed.generationStat)
886
+ return false;
887
+ const current = observePosixLeaseDirectory(directory, platform);
888
+ if (!current || current.names.length !== 1
889
+ || current.generationName !== observed.generationName
890
+ || current.identity.generation !== observed.identity.generation
891
+ || !samePosixFileIdentity(current.identity, observed.identity)
892
+ || !current.generationStat
893
+ || !samePosixStatIdentity(current.generationStat, observed.generationStat))
894
+ return false;
895
+ const markerPath = join(directory, observed.generationName);
896
+ const retired = join(leasesRoot, `.retired-generation-${sessionKey(sessionId).slice(0, 16)}-${observed.identity.generation}`);
897
+ try {
898
+ renameSync(markerPath, retired);
899
+ }
900
+ catch (err) {
901
+ if (errnoCode(err) === 'ENOENT')
902
+ return false;
903
+ throw err;
904
+ }
905
+ try {
906
+ const moved = lstatSync(retired, { bigint: true });
907
+ assertPrivatePosixLeaseRecord(moved, 'Retired Codex App POSIX directory generation', platform);
908
+ if (!samePosixStatIdentity(observed.generationStat, moved)) {
909
+ throw new Error('Codex App POSIX directory generation changed during retirement');
910
+ }
911
+ }
912
+ finally {
913
+ try {
914
+ unlinkSync(retired);
915
+ }
916
+ catch { /* crash residue is outside authority */ }
917
+ }
918
+ try {
919
+ rmdirSync(directory);
920
+ }
921
+ catch (err) {
922
+ if (errnoCode(err) !== 'ENOENT' && errnoCode(err) !== 'ENOTEMPTY')
923
+ throw err;
924
+ }
925
+ return true;
926
+ }
927
+ /** Recover only the crash residue in which the directory contains two or more
928
+ * individually valid generation markers and no actor can still be live. Each
929
+ * exact marker inode is moved out before unlink, so a concurrent replacement
930
+ * turns into a retry instead of deleting successor authority. */
931
+ function retireAmbiguousPosixDirectoryGenerations(directory, observed, leasesRoot, sessionId, platform) {
932
+ const observedNames = observed.generationNames;
933
+ const observedStats = observed.generationStats;
934
+ if (!observedNames || observedNames.length < 2 || !observedStats)
935
+ return false;
936
+ const current = observePosixLeaseDirectory(directory, platform);
937
+ if (!current
938
+ || !samePosixFileIdentity(current.identity, observed.identity)
939
+ || current.names.length !== observedNames.length
940
+ || current.generationNames?.length !== observedNames.length
941
+ || current.generationNames.some((name, index) => name !== observedNames[index]))
942
+ return false;
943
+ for (const name of observedNames) {
944
+ const expectedStat = observedStats.get(name);
945
+ const currentStat = current.generationStats?.get(name);
946
+ if (!expectedStat || !currentStat || !samePosixStatIdentity(expectedStat, currentStat)) {
947
+ return false;
948
+ }
949
+ }
950
+ for (const name of observedNames) {
951
+ const expectedStat = observedStats.get(name);
952
+ const markerPath = join(directory, name);
953
+ const retired = join(leasesRoot, `.retired-generation-${sessionKey(sessionId).slice(0, 16)}-${randomBytes(16).toString('hex')}`);
954
+ try {
955
+ renameSync(markerPath, retired);
956
+ }
957
+ catch (err) {
958
+ if (errnoCode(err) === 'ENOENT')
959
+ return false;
960
+ throw err;
961
+ }
962
+ try {
963
+ const moved = lstatSync(retired, { bigint: true });
964
+ assertPrivatePosixLeaseRecord(moved, 'Retired Codex App POSIX directory generation', platform);
965
+ if (!samePosixStatIdentity(expectedStat, moved)) {
966
+ throw new Error('Codex App POSIX directory generation changed during retirement');
967
+ }
968
+ }
969
+ finally {
970
+ try {
971
+ unlinkSync(retired);
972
+ }
973
+ catch { /* residue is outside authority */ }
974
+ }
975
+ }
976
+ try {
977
+ rmdirSync(directory);
978
+ }
979
+ catch (err) {
980
+ if (errnoCode(err) !== 'ENOENT' && errnoCode(err) !== 'ENOTEMPTY')
981
+ throw err;
982
+ }
983
+ return true;
984
+ }
985
+ function assertPosixLeaseDirectoryStat(stat, platform) {
986
+ if (!stat.isDirectory() || stat.isSymbolicLink() || exactBigIntMode(stat.mode) !== PRIVATE_DIRECTORY_MODE) {
987
+ throw new Error('Codex App POSIX owner lease must be a real 0700 directory');
988
+ }
989
+ assertPosixOwned(stat, 'Codex App POSIX owner lease', platform);
990
+ }
991
+ function posixLeaseRecordMatchesDirectory(observed, directory) {
992
+ if (!observed.record)
993
+ return false;
994
+ const identity = 'identity' in directory ? directory.identity : directory;
995
+ if (!samePosixFileIdentity(observed.record.directoryIdentity, identity))
996
+ return false;
997
+ return observed.record.version === 2
998
+ ? identity.generation === undefined
999
+ : observed.record.directoryIdentity.generation === identity.generation;
1000
+ }
1001
+ function posixOwnerTarget(observed) {
1002
+ return {
1003
+ nonce: observed.nonce,
1004
+ recordIdentity: posixFileIdentity(observed.stat),
1005
+ pid: observed.record?.pid ?? null,
1006
+ processStartToken: observed.record?.processStartToken ?? null,
1007
+ };
1008
+ }
1009
+ function posixReaperTargetsOwner(reaper, owner) {
1010
+ const target = reaper.record?.targetOwner;
1011
+ if (!target || target.nonce !== owner.nonce
1012
+ || !samePosixFileIdentity(target.recordIdentity, posixFileIdentity(owner.stat)))
1013
+ return false;
1014
+ if (!owner.record)
1015
+ return target.pid === null && target.processStartToken === null;
1016
+ return target.pid === owner.record.pid
1017
+ && target.processStartToken === owner.record.processStartToken;
1018
+ }
1019
+ /**
1020
+ * Process-lifetime publisher lease for POSIX.
1021
+ *
1022
+ * Both owners and stale cleaners publish complete, random-name actor records.
1023
+ * An actor record is written to `.pending`, synced, and atomically renamed to
1024
+ * `.json`, so SIGKILL cannot create a partial authoritative record. Existing
1025
+ * partial records remain recoverable after a grace interval. Every complete
1026
+ * actor is bound to the exact directory dev+ino it intended to mutate; reapers
1027
+ * additionally bind the exact owner-record inode/process tuple. A cleaner moves
1028
+ * the exact observed random-name inode out of the authority directory before
1029
+ * deleting it; that rename is the CAS boundary which prevents a delayed actor
1030
+ * from poisoning or deleting a successor's record after path replacement.
1031
+ */
1032
+ export async function acquireCodexAppPosixOwnerLease(options) {
1033
+ const platform = options.platform ?? process.platform;
1034
+ if (platform === 'win32')
1035
+ throw new Error('POSIX owner lease is unavailable on Windows');
1036
+ const now = options.now ?? Date.now;
1037
+ const wait = options.wait ?? (delayMs => new Promise(resolve => setTimeout(resolve, delayMs)));
1038
+ const timeoutMs = options.timeoutMs ?? 10_000;
1039
+ const retryDelayMs = options.retryDelayMs ?? 50;
1040
+ const initializationGraceMs = options.initializationGraceMs ?? 1_000;
1041
+ const pid = options.pid ?? process.pid;
1042
+ const processStartToken = options.processStartToken
1043
+ ?? readCodexAppProcessStartToken(pid, platform);
1044
+ if (!processStartToken) {
1045
+ throw new Error('Cannot verify current process start token for Codex App POSIX owner lease');
1046
+ }
1047
+ const inspectOwner = options.inspectOwner
1048
+ ?? ((ownerPid, token) => inspectCodexAppOwnerProcess(ownerPid, token, platform));
1049
+ ensureCodexAppControlDirectory(options.controlRoot, platform);
1050
+ const leasesRoot = join(options.controlRoot, 'leases');
1051
+ ensureCodexAppControlDirectory(leasesRoot, platform);
1052
+ const directory = codexAppPosixOwnerLeaseDirectory(options.controlRoot, options.sessionId);
1053
+ const deadline = now() + timeoutMs;
1054
+ const retry = async () => {
1055
+ if (now() >= deadline)
1056
+ throw new Error('Codex App POSIX owner lease timed out');
1057
+ await wait(Math.min(retryDelayMs, Math.max(0, deadline - now())));
1058
+ };
1059
+ acquireLoop: while (true) {
1060
+ const nonce = randomBytes(32).toString('hex');
1061
+ const directoryGeneration = randomBytes(32).toString('hex');
1062
+ const generationName = `generation-${directoryGeneration}.json`;
1063
+ const generationPath = join(directory, generationName);
1064
+ const ownerRecordPath = join(directory, `owner-${nonce}.json`);
1065
+ const ownerPendingPath = join(directory, `owner-${nonce}.pending`);
1066
+ let createdDirectory = false;
1067
+ try {
1068
+ mkdirSync(directory, { mode: PRIVATE_DIRECTORY_MODE });
1069
+ createdDirectory = true;
1070
+ const ownedDirectoryStat = lstatSync(directory, { bigint: true });
1071
+ assertPosixLeaseDirectoryStat(ownedDirectoryStat, platform);
1072
+ createExclusiveFile(generationPath, directoryGeneration, 'Codex App POSIX directory generation', platform);
1073
+ fsyncDirectory(directory, platform);
1074
+ const ownedDirectoryIdentity = {
1075
+ ...posixFileIdentity(ownedDirectoryStat),
1076
+ generation: directoryGeneration,
1077
+ };
1078
+ if (options.onOwnerDirectoryCreated) {
1079
+ await options.onOwnerDirectoryCreated(directory);
1080
+ }
1081
+ const record = {
1082
+ version: 3,
1083
+ role: 'owner',
1084
+ sessionId: options.sessionId,
1085
+ nonce,
1086
+ pid,
1087
+ processStartToken,
1088
+ createdAtMs: now(),
1089
+ directoryIdentity: ownedDirectoryIdentity,
1090
+ };
1091
+ try {
1092
+ createStagedPosixLeaseRecord({
1093
+ directory,
1094
+ finalPath: ownerRecordPath,
1095
+ pendingPath: ownerPendingPath,
1096
+ record,
1097
+ sessionId: options.sessionId,
1098
+ label: 'Codex App POSIX owner record',
1099
+ platform,
1100
+ });
1101
+ const persisted = readPosixOwnerRecord(ownerRecordPath, options.sessionId, platform, 'owner');
1102
+ if (!persisted || persisted.nonce !== nonce) {
1103
+ throw new Error('Codex App POSIX owner record verification failed');
1104
+ }
1105
+ if (options.onOwnerRecordPublished) {
1106
+ await options.onOwnerRecordPublished(directory, ownerRecordPath);
1107
+ }
1108
+ const installed = observePosixLeaseDirectory(directory, platform);
1109
+ if (!installed || !samePosixStatIdentity(installed.stat, ownedDirectoryStat)
1110
+ || installed.identity.generation !== directoryGeneration
1111
+ || installed.names.length !== 2
1112
+ || !installed.names.includes(generationName)
1113
+ || !installed.names.includes(basename(ownerRecordPath))) {
1114
+ throw posixLeaseRaceError('Codex App POSIX owner directory changed during record publication');
1115
+ }
1116
+ }
1117
+ catch (err) {
1118
+ try {
1119
+ unlinkSync(ownerPendingPath);
1120
+ }
1121
+ catch { /* not installed */ }
1122
+ try {
1123
+ unlinkSync(ownerRecordPath);
1124
+ }
1125
+ catch { /* not installed */ }
1126
+ try {
1127
+ unlinkSync(generationPath);
1128
+ }
1129
+ catch { /* replacement directory owns a different generation */ }
1130
+ try {
1131
+ rmdirSync(directory);
1132
+ }
1133
+ catch { /* competing reaper or residue */ }
1134
+ throw err;
1135
+ }
1136
+ let released = false;
1137
+ const isOwned = () => {
1138
+ if (released)
1139
+ return false;
1140
+ try {
1141
+ const currentDirectory = observePosixLeaseDirectory(directory, platform);
1142
+ if (!currentDirectory
1143
+ || !samePosixFileIdentity(currentDirectory.identity, ownedDirectoryIdentity))
1144
+ return false;
1145
+ const currentOwner = observePosixLeaseRecord(ownerRecordPath, basename(ownerRecordPath), options.sessionId, platform, now(), 'Codex App POSIX owner record');
1146
+ if (!currentOwner || currentOwner.record?.nonce !== nonce
1147
+ || !posixLeaseRecordMatchesDirectory(currentOwner, ownedDirectoryIdentity))
1148
+ return false;
1149
+ for (const name of currentDirectory.names.filter(candidate => (POSIX_REAPER_RECORD_RE.test(candidate) || POSIX_REAPER_PENDING_RE.test(candidate)))) {
1150
+ const reaper = observePosixLeaseRecord(join(directory, name), name, options.sessionId, platform, now(), 'Codex App POSIX reaper record');
1151
+ if (!reaper)
1152
+ return false;
1153
+ // A delayed cleaner may publish into a replacement directory. Only
1154
+ // a complete actor bound to this inode and this exact owner tuple
1155
+ // can suspend authority. Partial/foreign actors never authorize a
1156
+ // takeover and are reclaimed by the acquisition loop.
1157
+ if (posixLeaseRecordMatchesDirectory(reaper, currentDirectory)
1158
+ && posixReaperTargetsOwner(reaper, currentOwner))
1159
+ return false;
1160
+ }
1161
+ return true;
1162
+ }
1163
+ catch {
1164
+ return false;
1165
+ }
1166
+ };
1167
+ return {
1168
+ directory,
1169
+ ownerRecordPath,
1170
+ isOwned,
1171
+ release: () => {
1172
+ if (released)
1173
+ return;
1174
+ released = true;
1175
+ const persisted = readPosixOwnerRecord(ownerRecordPath, options.sessionId, platform, 'owner');
1176
+ if (persisted?.nonce === nonce
1177
+ && samePosixFileIdentity(persisted.directoryIdentity, ownedDirectoryIdentity)) {
1178
+ try {
1179
+ unlinkSync(ownerRecordPath);
1180
+ }
1181
+ catch { /* already reaped */ }
1182
+ }
1183
+ try {
1184
+ unlinkSync(ownerPendingPath);
1185
+ }
1186
+ catch { /* never published or already retired */ }
1187
+ try {
1188
+ const currentDirectory = observePosixLeaseDirectory(directory, platform);
1189
+ if (currentDirectory
1190
+ && currentDirectory.identity.generation === directoryGeneration
1191
+ && currentDirectory.names.length === 1
1192
+ && currentDirectory.names[0] === generationName) {
1193
+ retirePosixDirectoryGeneration(directory, currentDirectory, leasesRoot, options.sessionId, platform);
1194
+ }
1195
+ }
1196
+ catch { /* another contender owns cleanup */ }
1197
+ },
1198
+ };
1199
+ }
1200
+ catch (err) {
1201
+ if (createdDirectory && (errnoCode(err) === 'EAGAIN' || errnoCode(err) === 'ENOENT')) {
1202
+ await retry();
1203
+ continue;
1204
+ }
1205
+ if (createdDirectory || errnoCode(err) !== 'EEXIST')
1206
+ throw err;
1207
+ }
1208
+ options.onContended?.();
1209
+ const snapshot = observePosixLeaseDirectory(directory, platform);
1210
+ if (!snapshot) {
1211
+ await retry();
1212
+ continue;
1213
+ }
1214
+ const knownNames = snapshot.names.filter(name => (POSIX_OWNER_RECORD_RE.test(name)
1215
+ || POSIX_OWNER_PENDING_RE.test(name)
1216
+ || POSIX_REAPER_RECORD_RE.test(name)
1217
+ || POSIX_REAPER_PENDING_RE.test(name)
1218
+ || POSIX_DIRECTORY_GENERATION_RE.test(name)));
1219
+ if (knownNames.length !== snapshot.names.length) {
1220
+ throw new Error('Codex App POSIX owner lease contains an unknown record');
1221
+ }
1222
+ if ((snapshot.generationNames?.length ?? 0) > 1) {
1223
+ let retiredActor = false;
1224
+ for (const name of snapshot.names.filter(candidate => (POSIX_OWNER_RECORD_RE.test(candidate)
1225
+ || POSIX_OWNER_PENDING_RE.test(candidate)
1226
+ || POSIX_REAPER_RECORD_RE.test(candidate)
1227
+ || POSIX_REAPER_PENDING_RE.test(candidate)))) {
1228
+ const observed = observePosixLeaseRecord(join(directory, name), name, options.sessionId, platform, now(), 'Codex App POSIX ambiguous-generation actor');
1229
+ if (!observed) {
1230
+ await retry();
1231
+ continue acquireLoop;
1232
+ }
1233
+ if (observed.record) {
1234
+ const status = inspectOwner(observed.record.pid, observed.record.processStartToken);
1235
+ if (status !== 'dead') {
1236
+ await retry();
1237
+ continue acquireLoop;
1238
+ }
1239
+ }
1240
+ else if (observed.ageMs < initializationGraceMs) {
1241
+ await retry();
1242
+ continue acquireLoop;
1243
+ }
1244
+ if (!retireObservedPosixLeaseRecord(observed, leasesRoot, options.sessionId, platform)) {
1245
+ await retry();
1246
+ continue acquireLoop;
1247
+ }
1248
+ retiredActor = true;
1249
+ }
1250
+ if (retiredActor)
1251
+ continue;
1252
+ const ageMs = Math.max(0, now() - Number(snapshot.stat.mtimeMs));
1253
+ if (ageMs < initializationGraceMs
1254
+ || !retireAmbiguousPosixDirectoryGenerations(directory, snapshot, leasesRoot, options.sessionId, platform)) {
1255
+ await retry();
1256
+ }
1257
+ continue;
1258
+ }
1259
+ // A live/unknown cleaner is authority and blocks fail-closed. A dead or
1260
+ // crash-partial cleaner has a unique filename, so retiring that exact inode
1261
+ // cannot clobber a successor cleaner.
1262
+ const reaperNames = snapshot.names.filter(name => (POSIX_REAPER_RECORD_RE.test(name) || POSIX_REAPER_PENDING_RE.test(name)));
1263
+ if (reaperNames.length > 0) {
1264
+ let retiredAny = false;
1265
+ for (const name of reaperNames) {
1266
+ const observed = observePosixLeaseRecord(join(directory, name), name, options.sessionId, platform, now(), 'Codex App POSIX reaper record');
1267
+ if (!observed) {
1268
+ await retry();
1269
+ continue acquireLoop;
1270
+ }
1271
+ if (observed.record && !posixLeaseRecordMatchesDirectory(observed, snapshot)) {
1272
+ // The actor was published by a delayed cleaner that pinned an older
1273
+ // directory inode. It has no authority in this replacement directory,
1274
+ // regardless of whether that process is still alive.
1275
+ }
1276
+ else if (observed.record) {
1277
+ const status = inspectOwner(observed.record.pid, observed.record.processStartToken);
1278
+ if (status !== 'dead') {
1279
+ await retry();
1280
+ continue acquireLoop;
1281
+ }
1282
+ }
1283
+ else if (observed.ageMs < initializationGraceMs) {
1284
+ await retry();
1285
+ continue acquireLoop;
1286
+ }
1287
+ if (!retireObservedPosixLeaseRecord(observed, leasesRoot, options.sessionId, platform)) {
1288
+ await retry();
1289
+ continue acquireLoop;
1290
+ }
1291
+ retiredAny = true;
1292
+ }
1293
+ if (retiredAny)
1294
+ continue;
1295
+ }
1296
+ const ownerNames = snapshot.names.filter(name => (POSIX_OWNER_RECORD_RE.test(name) || POSIX_OWNER_PENDING_RE.test(name)));
1297
+ const observedOwners = [];
1298
+ let retiredForeignOwner = false;
1299
+ for (const name of ownerNames) {
1300
+ const observed = observePosixLeaseRecord(join(directory, name), name, options.sessionId, platform, now(), 'Codex App POSIX owner record');
1301
+ if (!observed) {
1302
+ await retry();
1303
+ continue acquireLoop;
1304
+ }
1305
+ if (observed.record && !posixLeaseRecordMatchesDirectory(observed, snapshot)) {
1306
+ // This can only be a creator that pinned an older directory and resumed
1307
+ // after the fixed path was replaced. Its process liveness is irrelevant:
1308
+ // the record never held authority over this inode.
1309
+ if (!retireObservedPosixLeaseRecord(observed, leasesRoot, options.sessionId, platform)) {
1310
+ await retry();
1311
+ continue acquireLoop;
1312
+ }
1313
+ retiredForeignOwner = true;
1314
+ continue;
1315
+ }
1316
+ observedOwners.push(observed);
1317
+ }
1318
+ if (retiredForeignOwner)
1319
+ continue;
1320
+ if (observedOwners.length > 1) {
1321
+ let retiredNonAuthoritativeOwner = false;
1322
+ for (const observed of observedOwners) {
1323
+ const reclaimable = observed.record
1324
+ ? inspectOwner(observed.record.pid, observed.record.processStartToken) === 'dead'
1325
+ : observed.ageMs >= initializationGraceMs;
1326
+ if (!reclaimable)
1327
+ continue;
1328
+ // A creator can be paused after mkdir while another contender replaces
1329
+ // the empty inode, then resume and publish into that replacement before
1330
+ // noticing another owner. Exact actor-file CAS makes dead/partial losing
1331
+ // candidates independently reclaimable without touching a live/unknown
1332
+ // same-directory candidate.
1333
+ if (!retireObservedPosixLeaseRecord(observed, leasesRoot, options.sessionId, platform)) {
1334
+ await retry();
1335
+ continue acquireLoop;
1336
+ }
1337
+ retiredNonAuthoritativeOwner = true;
1338
+ }
1339
+ if (retiredNonAuthoritativeOwner)
1340
+ continue;
1341
+ // Multiple live/unknown candidates are ambiguous. None can be newly
1342
+ // granted by this contender; wait for a creator to finish its directory
1343
+ // CAS or for liveness to become provable.
1344
+ await retry();
1345
+ continue;
1346
+ }
1347
+ if (observedOwners.length === 0) {
1348
+ const ageMs = Math.max(0, now() - Number(snapshot.stat.mtimeMs));
1349
+ if (ageMs < initializationGraceMs) {
1350
+ await retry();
1351
+ continue;
1352
+ }
1353
+ if (snapshot.generationName) {
1354
+ if (!retirePosixDirectoryGeneration(directory, snapshot, leasesRoot, options.sessionId, platform))
1355
+ await retry();
1356
+ }
1357
+ else {
1358
+ try {
1359
+ rmdirSync(directory);
1360
+ }
1361
+ catch (err) {
1362
+ if (errnoCode(err) !== 'ENOENT' && errnoCode(err) !== 'ENOTEMPTY')
1363
+ throw err;
1364
+ await retry();
1365
+ }
1366
+ }
1367
+ continue;
1368
+ }
1369
+ const observedOwner = observedOwners[0];
1370
+ if (observedOwner.record) {
1371
+ const status = inspectOwner(observedOwner.record.pid, observedOwner.record.processStartToken);
1372
+ if (status !== 'dead') {
1373
+ await retry();
1374
+ continue;
1375
+ }
1376
+ }
1377
+ else if (observedOwner.ageMs < initializationGraceMs) {
1378
+ await retry();
1379
+ continue;
1380
+ }
1381
+ const reaperNonce = randomBytes(32).toString('hex');
1382
+ const reaperPath = join(directory, `reap-${reaperNonce}.json`);
1383
+ const reaperPendingPath = join(directory, `reap-${reaperNonce}.pending`);
1384
+ const reaperRecord = {
1385
+ version: snapshot.identity.generation ? 3 : 2,
1386
+ role: 'reaper',
1387
+ sessionId: options.sessionId,
1388
+ nonce: reaperNonce,
1389
+ pid,
1390
+ processStartToken,
1391
+ createdAtMs: now(),
1392
+ directoryIdentity: snapshot.identity,
1393
+ targetOwner: posixOwnerTarget(observedOwner),
1394
+ };
1395
+ try {
1396
+ if (options.onBeforeReaperRecordPublished) {
1397
+ await options.onBeforeReaperRecordPublished(directory);
1398
+ }
1399
+ createStagedPosixLeaseRecord({
1400
+ directory,
1401
+ finalPath: reaperPath,
1402
+ pendingPath: reaperPendingPath,
1403
+ record: reaperRecord,
1404
+ sessionId: options.sessionId,
1405
+ label: 'Codex App POSIX reaper record',
1406
+ platform,
1407
+ });
1408
+ if (options.onReaperRecordPublished) {
1409
+ await options.onReaperRecordPublished(directory, reaperPath);
1410
+ }
1411
+ const reaperDirectory = observePosixLeaseDirectory(directory, platform);
1412
+ if (!reaperDirectory || !samePosixStatIdentity(reaperDirectory.stat, snapshot.stat)) {
1413
+ throw posixLeaseRaceError('Codex App POSIX owner directory changed during reaper election');
1414
+ }
1415
+ }
1416
+ catch (err) {
1417
+ try {
1418
+ unlinkSync(reaperPendingPath);
1419
+ }
1420
+ catch { /* never installed */ }
1421
+ try {
1422
+ unlinkSync(reaperPath);
1423
+ }
1424
+ catch { /* never installed */ }
1425
+ if (errnoCode(err) === 'EEXIST' || errnoCode(err) === 'ENOENT'
1426
+ || errnoCode(err) === 'EAGAIN') {
1427
+ await retry();
1428
+ continue;
1429
+ }
1430
+ throw err;
1431
+ }
1432
+ try {
1433
+ const electedSnapshot = observePosixLeaseDirectory(directory, platform);
1434
+ if (!electedSnapshot)
1435
+ continue;
1436
+ const electedReapers = [];
1437
+ let electionMustRetry = false;
1438
+ for (const name of electedSnapshot.names.filter(candidate => (POSIX_REAPER_RECORD_RE.test(candidate) || POSIX_REAPER_PENDING_RE.test(candidate)))) {
1439
+ const observed = observePosixLeaseRecord(join(directory, name), name, options.sessionId, platform, now(), 'Codex App POSIX reaper record');
1440
+ if (!observed || !observed.record) {
1441
+ electionMustRetry = true;
1442
+ break;
1443
+ }
1444
+ if (!posixLeaseRecordMatchesDirectory(observed, electedSnapshot)
1445
+ || !posixReaperTargetsOwner(observed, observedOwner)) {
1446
+ electionMustRetry = true;
1447
+ break;
1448
+ }
1449
+ if (observed.nonce !== reaperNonce) {
1450
+ const status = inspectOwner(observed.record.pid, observed.record.processStartToken);
1451
+ if (status !== 'alive') {
1452
+ // Unknown actors block fail-closed; dead actors are reclaimed by
1453
+ // the outer observation loop before a new election.
1454
+ electionMustRetry = true;
1455
+ break;
1456
+ }
1457
+ }
1458
+ electedReapers.push(observed);
1459
+ }
1460
+ if (electionMustRetry)
1461
+ continue;
1462
+ electedReapers.sort((a, b) => ((a.record.createdAtMs - b.record.createdAtMs) || a.nonce.localeCompare(b.nonce)));
1463
+ // Simultaneous contenders may both have published before observing each
1464
+ // other. The oldest deterministic actor proceeds; all others withdraw.
1465
+ if (electedReapers[0]?.nonce !== reaperNonce)
1466
+ continue;
1467
+ const currentOwner = observePosixLeaseRecord(observedOwner.path, observedOwner.name, options.sessionId, platform, now(), 'Codex App POSIX owner record');
1468
+ if (!currentOwner || !samePosixStatIdentity(currentOwner.stat, observedOwner.stat)
1469
+ || (currentOwner.record && !posixLeaseRecordMatchesDirectory(currentOwner, electedSnapshot))
1470
+ || !posixReaperTargetsOwner(electedReapers.find(candidate => candidate.nonce === reaperNonce), currentOwner))
1471
+ continue;
1472
+ if (currentOwner.record) {
1473
+ if (inspectOwner(currentOwner.record.pid, currentOwner.record.processStartToken) !== 'dead')
1474
+ continue;
1475
+ }
1476
+ else if (currentOwner.ageMs < initializationGraceMs) {
1477
+ continue;
1478
+ }
1479
+ if (!retireObservedPosixLeaseRecord(currentOwner, leasesRoot, options.sessionId, platform)) {
1480
+ continue;
1481
+ }
1482
+ }
1483
+ finally {
1484
+ try {
1485
+ unlinkSync(reaperPendingPath);
1486
+ }
1487
+ catch { /* renamed or already retired */ }
1488
+ try {
1489
+ unlinkSync(reaperPath);
1490
+ }
1491
+ catch { /* another cleaner retired it */ }
1492
+ }
1493
+ const emptied = observePosixLeaseDirectory(directory, platform);
1494
+ if (emptied?.generationName && emptied.names.length === 1) {
1495
+ retirePosixDirectoryGeneration(directory, emptied, leasesRoot, options.sessionId, platform);
1496
+ }
1497
+ else {
1498
+ try {
1499
+ rmdirSync(directory);
1500
+ }
1501
+ catch (err) {
1502
+ if (errnoCode(err) !== 'ENOENT' && errnoCode(err) !== 'ENOTEMPTY')
1503
+ throw err;
1504
+ }
1505
+ }
1506
+ }
1507
+ }
1508
+ export function generateCodexAppControlEpoch() {
1509
+ return randomBytes(32).toString('base64url');
1510
+ }
1511
+ export function isValidCodexAppWindowsPipeEndpoint(endpoint) {
1512
+ return typeof endpoint === 'string'
1513
+ && /^\\\\\?\\pipe\\botmux-codex-app-[a-f0-9]{64}$/.test(endpoint);
1514
+ }
1515
+ function isValidCodexAppPosixSocketEndpoint(endpoint, locatorPath, expectedControlRoot, expectedSessionId, platform) {
1516
+ if (typeof endpoint !== 'string' || !locatorPath || !expectedControlRoot
1517
+ || !isAbsolute(endpoint) || !isAbsolute(locatorPath) || !isAbsolute(expectedControlRoot)) {
1518
+ return false;
1519
+ }
1520
+ // Do not infer trust from an attacker-selected locator path. The caller must
1521
+ // supply the already trusted control root and both paths must be the exact
1522
+ // canonical children for this session.
1523
+ if (locatorPath !== codexAppControlLocatorPath(expectedControlRoot, expectedSessionId, platform))
1524
+ return false;
1525
+ const endpointDirectory = join(expectedControlRoot, 'sockets');
1526
+ const leaf = basename(endpoint);
1527
+ return POSIX_SOCKET_LEAF_RE.test(leaf)
1528
+ && endpoint === join(endpointDirectory, leaf)
1529
+ // Darwin's sockaddr_un.sun_path is 104 bytes including the terminator.
1530
+ && Buffer.byteLength(endpoint, 'utf8') < 104;
1531
+ }
1532
+ export function validateCodexAppControlLocator(value, expectedSessionId, options = {}) {
1533
+ if (!value || typeof value !== 'object')
1534
+ return undefined;
1535
+ const record = value;
1536
+ const platform = options.platform ?? process.platform;
1537
+ const endpointValid = platform === 'win32'
1538
+ ? isValidCodexAppWindowsPipeEndpoint(record.endpoint)
1539
+ : isValidCodexAppPosixSocketEndpoint(record.endpoint, options.locatorPath, options.expectedControlRoot, expectedSessionId, platform);
1540
+ if (record.version !== CONTROL_LOCATOR_VERSION
1541
+ || record.sessionId !== expectedSessionId
1542
+ || !isValidGeneration(record.epoch)
1543
+ || !endpointValid)
1544
+ return undefined;
1545
+ return record;
1546
+ }
1547
+ function isValidGeneration(value) {
1548
+ return typeof value === 'string' && GENERATION_RE.test(value);
1549
+ }
1550
+ function isValidChallenge(value) {
1551
+ return typeof value === 'string' && CHALLENGE_RE.test(value);
1552
+ }
1553
+ function importPublicKey(encoded) {
1554
+ return createPublicKey({ key: Buffer.from(encoded, 'base64url'), format: 'der', type: 'spki' });
1555
+ }
1556
+ function importPrivateKey(encoded) {
1557
+ return createPrivateKey({ key: Buffer.from(encoded, 'base64url'), format: 'der', type: 'pkcs8' });
1558
+ }
1559
+ function validIdentity(value) {
1560
+ if (!value || typeof value !== 'object')
1561
+ return false;
1562
+ const record = value;
1563
+ if (!isValidGeneration(record.generation)
1564
+ || typeof record.publicKey !== 'string' || !KEY_RE.test(record.publicKey)
1565
+ || typeof record.createdAtMs !== 'number' || !Number.isFinite(record.createdAtMs))
1566
+ return false;
1567
+ try {
1568
+ return importPublicKey(record.publicKey).asymmetricKeyType === 'ed25519';
1569
+ }
1570
+ catch {
1571
+ return false;
1572
+ }
1573
+ }
1574
+ function validState(value) {
1575
+ if (!value || typeof value !== 'object')
1576
+ return false;
1577
+ const record = value;
1578
+ if (record.version !== CONTROL_STATE_VERSION
1579
+ || (record.status !== 'pending' && record.status !== 'active')
1580
+ || !Array.isArray(record.identities)
1581
+ || record.identities.length < 1
1582
+ || record.identities.length > MAX_STATE_IDENTITIES
1583
+ || !record.identities.every(validIdentity)
1584
+ || new Set(record.identities.map(identity => identity.generation)).size !== record.identities.length
1585
+ || typeof record.updatedAtMs !== 'number' || !Number.isFinite(record.updatedAtMs)
1586
+ || (record.status === 'active' && record.identities.length !== 1)
1587
+ || (record.activatedAtMs !== undefined
1588
+ && (typeof record.activatedAtMs !== 'number' || !Number.isFinite(record.activatedAtMs))))
1589
+ return false;
1590
+ return true;
1591
+ }
1592
+ function assertRegularSingleLinkWithinLimit(stat, maxBytes, label, platform) {
1593
+ const policy = codexAppControlFilesystemPolicy(platform);
1594
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1
1595
+ || (policy.verifyExactMode && exactMode(stat.mode) !== PRIVATE_FILE_MODE)
1596
+ || stat.size <= 0 || stat.size > maxBytes) {
1597
+ const mode = policy.verifyExactMode ? ' 0600' : '';
1598
+ throw new Error(`${label} must be a single-link regular${mode} file within the size limit`);
1599
+ }
1600
+ assertOwned(stat, label, platform);
1601
+ }
1602
+ function sameFileIdentity(before, after) {
1603
+ return Number.isFinite(before.dev) && Number.isFinite(before.ino)
1604
+ && before.dev === after.dev && before.ino === after.ino;
1605
+ }
1606
+ function readOwnedRegularFile(path, maxBytes, label, platform = process.platform) {
1607
+ const before = lstatSync(path);
1608
+ assertRegularSingleLinkWithinLimit(before, maxBytes, label, platform);
1609
+ const fd = openSync(path, fsConstants.O_RDONLY | noFollowFlag(platform));
1610
+ try {
1611
+ const stat = fstatSync(fd);
1612
+ assertRegularSingleLinkWithinLimit(stat, maxBytes, label, platform);
1613
+ if (!sameFileIdentity(before, stat))
1614
+ throw new Error(`${label} changed between lstat and open`);
1615
+ return readFileSync(fd, 'utf8');
1616
+ }
1617
+ finally {
1618
+ closeSync(fd);
1619
+ }
1620
+ }
1621
+ export function readCodexAppControlState(path, platform = process.platform) {
1622
+ try {
1623
+ const parsed = JSON.parse(readOwnedRegularFile(path, STATE_MAX_BYTES, 'Codex App control state', platform));
1624
+ return validState(parsed) ? parsed : undefined;
1625
+ }
1626
+ catch {
1627
+ return undefined;
1628
+ }
1629
+ }
1630
+ function createExclusiveFile(path, contents, label, platform = process.platform) {
1631
+ const policy = codexAppControlFilesystemPolicy(platform);
1632
+ const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag(platform), PRIVATE_FILE_MODE);
1633
+ try {
1634
+ if (policy.chmodAfterCreate)
1635
+ fchmodSync(fd, PRIVATE_FILE_MODE);
1636
+ const stat = fstatSync(fd);
1637
+ if (!stat.isFile() || stat.nlink !== 1
1638
+ || (policy.verifyExactMode && exactMode(stat.mode) !== PRIVATE_FILE_MODE)) {
1639
+ throw new Error(`${label} was not created as a single-link regular private file`);
1640
+ }
1641
+ assertOwned(stat, label, platform);
1642
+ const data = Buffer.from(contents, 'utf8');
1643
+ let offset = 0;
1644
+ while (offset < data.length)
1645
+ offset += writeSync(fd, data, offset, data.length - offset);
1646
+ fsyncSync(fd);
1647
+ }
1648
+ finally {
1649
+ closeSync(fd);
1650
+ }
1651
+ }
1652
+ function fsyncDirectory(directory, platform) {
1653
+ if (!codexAppControlFilesystemPolicy(platform).fsyncDirectory)
1654
+ return;
1655
+ const fd = openSync(directory, fsConstants.O_RDONLY | noFollowFlag(platform));
1656
+ try {
1657
+ const stat = fstatSync(fd);
1658
+ if (!stat.isDirectory())
1659
+ throw new Error('Codex App control state parent must be a directory');
1660
+ assertOwned(stat, 'Codex App control state parent', platform);
1661
+ fsyncSync(fd);
1662
+ }
1663
+ finally {
1664
+ closeSync(fd);
1665
+ }
1666
+ }
1667
+ /** Symlink-safe atomic state replacement inside a private directory. */
1668
+ export function writeCodexAppControlState(path, state) {
1669
+ writeCodexAppControlStateForPlatform(path, state, process.platform);
1670
+ }
1671
+ export function writeCodexAppControlStateForPlatform(path, state, platform) {
1672
+ if (!validState(state))
1673
+ throw new Error('Codex App control state is invalid');
1674
+ const directory = dirnameForPlatform(platform, path);
1675
+ ensureCodexAppControlDirectory(directory, platform);
1676
+ const tmp = joinForPlatform(platform, directory, `.${sessionKey(path)}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
1677
+ try {
1678
+ createExclusiveFile(tmp, JSON.stringify(state), 'Codex App control state temp file', platform);
1679
+ renameSync(tmp, path);
1680
+ // The file itself was synced before rename; sync the parent as well so a
1681
+ // proved generation does not silently roll back after a host crash.
1682
+ fsyncDirectory(directory, platform);
1683
+ const persisted = readCodexAppControlState(path, platform);
1684
+ if (!persisted || persisted.status !== state.status
1685
+ || persisted.identities.map(identity => identity.generation).join(',')
1686
+ !== state.identities.map(identity => identity.generation).join(',')) {
1687
+ throw new Error('Codex App control state verification failed after rename');
1688
+ }
1689
+ }
1690
+ finally {
1691
+ try {
1692
+ unlinkSync(tmp);
1693
+ }
1694
+ catch { /* renamed or never created */ }
1695
+ }
1696
+ }
1697
+ export function readCodexAppControlLocator(path, expectedSessionId, platform = process.platform, expectedControlRoot = platform === 'win32'
1698
+ ? undefined
1699
+ : codexAppPosixControlRoot()) {
1700
+ try {
1701
+ const value = JSON.parse(readOwnedRegularFile(path, LOCATOR_MAX_BYTES, 'Codex App control locator', platform));
1702
+ return validateCodexAppControlLocator(value, expectedSessionId, {
1703
+ platform,
1704
+ locatorPath: path,
1705
+ expectedControlRoot,
1706
+ });
1707
+ }
1708
+ catch {
1709
+ return undefined;
1710
+ }
1711
+ }
1712
+ export function writeCodexAppControlLocator(path, locator, platform = process.platform, expectedControlRoot = platform === 'win32'
1713
+ ? undefined
1714
+ : codexAppPosixControlRoot()) {
1715
+ if (!validateCodexAppControlLocator(locator, locator.sessionId, {
1716
+ platform,
1717
+ locatorPath: path,
1718
+ expectedControlRoot,
1719
+ })) {
1720
+ throw new Error('Codex App control locator is invalid');
1721
+ }
1722
+ const directory = dirnameForPlatform(platform, path);
1723
+ ensureCodexAppControlDirectory(directory, platform);
1724
+ const tmp = joinForPlatform(platform, directory, `.${sessionKey(path)}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
1725
+ try {
1726
+ createExclusiveFile(tmp, JSON.stringify(locator), 'Codex App control locator temp file', platform);
1727
+ renameSync(tmp, path);
1728
+ fsyncDirectory(directory, platform);
1729
+ const persisted = readCodexAppControlLocator(path, locator.sessionId, platform, expectedControlRoot);
1730
+ if (!persisted || persisted.epoch !== locator.epoch || persisted.endpoint !== locator.endpoint) {
1731
+ throw new Error('Codex App control locator verification failed after rename');
1732
+ }
1733
+ }
1734
+ finally {
1735
+ try {
1736
+ unlinkSync(tmp);
1737
+ }
1738
+ catch { /* renamed or never created */ }
1739
+ }
1740
+ }
1741
+ /**
1742
+ * The ordering contract is security-sensitive on every platform: the random
1743
+ * pipe/socket must already be bound before its locator becomes visible.
1744
+ */
1745
+ export async function bindThenPublishCodexAppControlLocator(input) {
1746
+ const locator = validateCodexAppControlLocator({
1747
+ version: CONTROL_LOCATOR_VERSION,
1748
+ sessionId: input.sessionId,
1749
+ epoch: input.epoch,
1750
+ endpoint: input.endpoint,
1751
+ }, input.sessionId, {
1752
+ platform: input.platform,
1753
+ locatorPath: input.locatorPath,
1754
+ expectedControlRoot: input.expectedControlRoot,
1755
+ });
1756
+ if (!locator)
1757
+ throw new Error('Codex App control endpoint publication metadata is invalid');
1758
+ await input.listen(locator.endpoint);
1759
+ if (input.isCurrent && !input.isCurrent()) {
1760
+ input.retire?.();
1761
+ return undefined;
1762
+ }
1763
+ try {
1764
+ input.publish(locator);
1765
+ }
1766
+ catch (err) {
1767
+ input.retire?.();
1768
+ throw err;
1769
+ }
1770
+ return locator;
1771
+ }
1772
+ export function shouldFailCodexAppControlChannel(input) {
1773
+ return !input.stopping && input.channelId === input.currentChannelId;
1774
+ }
1775
+ /**
1776
+ * Runner-side endpoint policy for locator rotations. A never-accepted
1777
+ * locator may be retried (the independently random, protected epoch is still
1778
+ * required for acceptance) until the shared startup deadline. Once accepted,
1779
+ * the pipe name is permanently burned and only a newly published endpoint can
1780
+ * be used.
1781
+ */
1782
+ export class CodexAppControlEndpointTracker {
1783
+ attemptsByEndpoint = new Map();
1784
+ acceptedEndpoints = new Set();
1785
+ take(locator) {
1786
+ if (this.acceptedEndpoints.has(locator.endpoint))
1787
+ return undefined;
1788
+ const attempts = this.attemptsByEndpoint.get(locator.endpoint) ?? 0;
1789
+ this.attemptsByEndpoint.set(locator.endpoint, attempts + 1);
1790
+ return locator.endpoint;
1791
+ }
1792
+ noteAccepted(endpoint) {
1793
+ if (!this.attemptsByEndpoint.has(endpoint)) {
1794
+ throw new Error('Cannot accept an unattempted Codex App control endpoint');
1795
+ }
1796
+ this.acceptedEndpoints.add(endpoint);
1797
+ }
1798
+ wasAttempted(endpoint) {
1799
+ return this.attemptsByEndpoint.has(endpoint);
1800
+ }
1801
+ attemptCount(endpoint) {
1802
+ return this.attemptsByEndpoint.get(endpoint) ?? 0;
1803
+ }
1804
+ wasAccepted(endpoint) {
1805
+ return this.acceptedEndpoints.has(endpoint);
1806
+ }
1807
+ }
1808
+ /** Read and select one locator endpoint; missing/corrupt files are a poll miss. */
1809
+ export function takeCodexAppControlLocatorEndpoint(input) {
1810
+ const platform = input.platform ?? process.platform;
1811
+ const locator = readCodexAppControlLocator(input.locatorPath, input.sessionId, platform, input.expectedControlRoot ?? (platform === 'win32' ? undefined : codexAppPosixControlRoot()));
1812
+ if (!locator)
1813
+ return undefined;
1814
+ const endpoint = input.tracker.take(locator);
1815
+ return endpoint ? { endpoint, epoch: locator.epoch } : undefined;
1816
+ }
1817
+ const PERSISTENT_BACKEND_TYPES = new Set(['tmux', 'herdr', 'zellij']);
1818
+ /** Missing/corrupt/legacy generations cannot prove a warm reattach. */
1819
+ export function shouldColdStartCodexAppReattach(input) {
1820
+ return input.cliId === 'codex-app'
1821
+ && input.isReattach
1822
+ && PERSISTENT_BACKEND_TYPES.has(input.backendType)
1823
+ && !input.persistedState;
1824
+ }
1825
+ /** Create a fresh asymmetric candidate and one O_EXCL private bootstrap. */
1826
+ export function createCodexAppControlBootstrap(directory, sessionId, target = codexAppControlSocketPath(directory, sessionId), platform = process.platform) {
1827
+ ensureCodexAppControlDirectory(directory, platform);
1828
+ const resolvedTarget = typeof target === 'string'
1829
+ ? { kind: 'endpoint', socketPath: target }
1830
+ : target;
1831
+ if (resolvedTarget.kind === 'endpoint'
1832
+ && !isAbsoluteForPlatform(resolvedTarget.socketPath, platform)) {
1833
+ throw new Error('Codex App control socket path must be absolute');
1834
+ }
1835
+ if (resolvedTarget.kind === 'locator'
1836
+ && !isAbsoluteForPlatform(resolvedTarget.locatorPath, platform)) {
1837
+ throw new Error('Codex App control locator path must be absolute');
1838
+ }
1839
+ const generation = randomBytes(32).toString('base64url');
1840
+ const pair = generateKeyPairSync('ed25519');
1841
+ const publicKey = pair.publicKey.export({ format: 'der', type: 'spki' }).toString('base64url');
1842
+ const privateKey = pair.privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64url');
1843
+ const identity = { generation, publicKey, createdAtMs: Date.now() };
1844
+ const key = sessionKey(sessionId).slice(0, 16);
1845
+ const path = joinForPlatform(platform, directory, `${key}.${randomBytes(16).toString('hex')}.bootstrap`);
1846
+ createExclusiveFile(path, JSON.stringify({
1847
+ version: CONTROL_BOOTSTRAP_VERSION,
1848
+ sessionId,
1849
+ generation,
1850
+ privateKey,
1851
+ ...(resolvedTarget.kind === 'endpoint'
1852
+ ? { socketPath: resolvedTarget.socketPath }
1853
+ : { locatorPath: resolvedTarget.locatorPath }),
1854
+ }), 'Codex App control bootstrap', platform);
1855
+ return { path, identity };
1856
+ }
1857
+ /** Remove crash-orphaned one-shot files for exactly one session generation. */
1858
+ export function cleanupStaleCodexAppControlBootstraps(directory, sessionId, platform = process.platform) {
1859
+ ensureCodexAppControlDirectory(directory, platform);
1860
+ const key = sessionKey(sessionId).slice(0, 16);
1861
+ const pattern = new RegExp(`^${key}\\.[a-f0-9]{32}\\.bootstrap$`);
1862
+ for (const name of readdirSync(directory)) {
1863
+ if (!pattern.test(name))
1864
+ continue;
1865
+ try {
1866
+ unlinkSync(joinForPlatform(platform, directory, name));
1867
+ }
1868
+ catch { /* raced with runner consume */ }
1869
+ }
1870
+ }
1871
+ /**
1872
+ * One-shot consume on one O_NOFOLLOW fd. The file is unlinked before its bytes
1873
+ * are read and the post-unlink link count must be zero. The private key is
1874
+ * imported here so callers do not retain or forward its encoded form.
1875
+ */
1876
+ export function consumeCodexAppControlBootstrap(path, expectedSessionId, platform = process.platform) {
1877
+ if (!path || !isAbsoluteForPlatform(path, platform)) {
1878
+ throw new Error('Codex App control bootstrap path must be absolute');
1879
+ }
1880
+ let fd;
1881
+ try {
1882
+ const policy = codexAppControlFilesystemPolicy(platform);
1883
+ const beforePath = lstatSync(path);
1884
+ assertRegularSingleLinkWithinLimit(beforePath, BOOTSTRAP_MAX_BYTES, 'Codex App control bootstrap', platform);
1885
+ fd = openSync(path, fsConstants.O_RDONLY | noFollowFlag(platform));
1886
+ const before = fstatSync(fd);
1887
+ assertRegularSingleLinkWithinLimit(before, BOOTSTRAP_MAX_BYTES, 'Codex App control bootstrap', platform);
1888
+ if (!sameFileIdentity(beforePath, before)) {
1889
+ throw new Error('Codex App control bootstrap changed between lstat and open');
1890
+ }
1891
+ unlinkSync(path);
1892
+ const after = fstatSync(fd);
1893
+ if (policy.verifyPostUnlinkLinkCount && after.nlink !== 0) {
1894
+ throw new Error('Codex App control bootstrap was not consumed by unlink');
1895
+ }
1896
+ const decoded = JSON.parse(readFileSync(fd, 'utf8'));
1897
+ const hasSocketPath = typeof decoded.socketPath === 'string'
1898
+ && isAbsoluteForPlatform(decoded.socketPath, platform);
1899
+ const hasLocatorPath = typeof decoded.locatorPath === 'string'
1900
+ && isAbsoluteForPlatform(decoded.locatorPath, platform);
1901
+ const posixLocatorPathValid = platform === 'win32' || !hasLocatorPath
1902
+ || decoded.locatorPath === codexAppControlLocatorPath(codexAppPosixControlRoot(), typeof decoded.sessionId === 'string' ? decoded.sessionId : '', platform);
1903
+ if (decoded.version !== CONTROL_BOOTSTRAP_VERSION
1904
+ || typeof decoded.sessionId !== 'string'
1905
+ || (expectedSessionId !== undefined && decoded.sessionId !== expectedSessionId)
1906
+ || !isValidGeneration(decoded.generation)
1907
+ || typeof decoded.privateKey !== 'string' || !KEY_RE.test(decoded.privateKey)
1908
+ || hasSocketPath === hasLocatorPath
1909
+ // Windows always follows its protected locator. Current POSIX workers
1910
+ // also emit locators; a legacy direct socket bootstrap remains accepted
1911
+ // because the read-once bootstrap itself is the launch capability. Any
1912
+ // POSIX locator is nevertheless pinned to the fixed per-UID root here.
1913
+ || (platform === 'win32' && !hasLocatorPath)
1914
+ || !posixLocatorPathValid) {
1915
+ throw new Error('Codex App control bootstrap is invalid');
1916
+ }
1917
+ const privateKey = importPrivateKey(decoded.privateKey);
1918
+ if (privateKey.asymmetricKeyType !== 'ed25519')
1919
+ throw new Error('Codex App control private key is invalid');
1920
+ return {
1921
+ generation: decoded.generation,
1922
+ privateKey,
1923
+ ...(hasSocketPath ? { socketPath: decoded.socketPath } : {}),
1924
+ ...(hasLocatorPath ? { locatorPath: decoded.locatorPath } : {}),
1925
+ };
1926
+ }
1927
+ catch (err) {
1928
+ try {
1929
+ unlinkSync(path);
1930
+ }
1931
+ catch { /* remove a rejected bootstrap/symlink */ }
1932
+ throw err;
1933
+ }
1934
+ finally {
1935
+ if (fd !== undefined)
1936
+ closeSync(fd);
1937
+ }
1938
+ }
1939
+ export function mergeCodexAppControlCandidate(existing, candidate, nowMs = Date.now()) {
1940
+ if (!validIdentity(candidate)) {
1941
+ throw new Error('Codex App control candidate is invalid');
1942
+ }
1943
+ const identities = [candidate, ...(existing?.identities ?? [])]
1944
+ .filter((identity, index, all) => all.findIndex(item => item.generation === identity.generation) === index)
1945
+ .slice(0, MAX_STATE_IDENTITIES);
1946
+ return {
1947
+ version: CONTROL_STATE_VERSION,
1948
+ status: 'pending',
1949
+ identities,
1950
+ updatedAtMs: nowMs,
1951
+ };
1952
+ }
1953
+ export function activateCodexAppControlIdentity(state, generation, nowMs = Date.now()) {
1954
+ const identity = state.identities.find(candidate => candidate.generation === generation);
1955
+ if (!identity)
1956
+ throw new Error('Codex App control generation is not a persisted candidate');
1957
+ return {
1958
+ version: CONTROL_STATE_VERSION,
1959
+ status: 'active',
1960
+ identities: [identity],
1961
+ updatedAtMs: nowMs,
1962
+ activatedAtMs: nowMs,
1963
+ };
1964
+ }
1965
+ export function generateCodexAppControlChallenge() {
1966
+ return randomBytes(32).toString('base64url');
1967
+ }
1968
+ function canonicalJson(value) {
1969
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
1970
+ return JSON.stringify(value);
1971
+ if (typeof value === 'number') {
1972
+ if (!Number.isFinite(value))
1973
+ throw new Error('non-finite control payload number');
1974
+ return JSON.stringify(value);
1975
+ }
1976
+ if (Array.isArray(value))
1977
+ return `[${value.map(canonicalJson).join(',')}]`;
1978
+ if (value && typeof value === 'object') {
1979
+ const record = value;
1980
+ return `{${Object.keys(record).sort().map(key => (`${JSON.stringify(key)}:${canonicalJson(record[key])}`)).join(',')}}`;
1981
+ }
1982
+ throw new Error('unsupported control payload value');
1983
+ }
1984
+ function signingBytes(domain, value) {
1985
+ return Buffer.from(`botmux/codex-app/control/${domain}/v2\0${canonicalJson(value)}`, 'utf8');
1986
+ }
1987
+ function signature(privateKey, domain, value) {
1988
+ if (privateKey.type !== 'private' || privateKey.asymmetricKeyType !== 'ed25519') {
1989
+ throw new Error('Codex App control signer must be an Ed25519 private key');
1990
+ }
1991
+ return sign(null, signingBytes(domain, value), privateKey).toString('base64url');
1992
+ }
1993
+ function signatureValid(publicKey, signatureValue, domain, value) {
1994
+ if (!KEY_RE.test(publicKey) || !SIGNATURE_RE.test(signatureValue))
1995
+ return false;
1996
+ try {
1997
+ return verify(null, signingBytes(domain, value), importPublicKey(publicKey), Buffer.from(signatureValue, 'base64url'));
1998
+ }
1999
+ catch {
2000
+ return false;
2001
+ }
2002
+ }
2003
+ export function encodeCodexAppControlChallenge(sessionId, challenge) {
2004
+ if (!sessionId || !isValidChallenge(challenge))
2005
+ throw new Error('Codex App control challenge is invalid');
2006
+ return JSON.stringify({ version: CONTROL_WIRE_VERSION, type: 'challenge', sessionId, challenge });
2007
+ }
2008
+ export function encodeCodexAppControlAccepted(sessionId, generation, challenge, endpointEpoch) {
2009
+ if (!sessionId || !isValidGeneration(generation) || !isValidChallenge(challenge)) {
2010
+ throw new Error('Codex App control acceptance metadata is invalid');
2011
+ }
2012
+ if (endpointEpoch !== undefined && !isValidGeneration(endpointEpoch)) {
2013
+ throw new Error('Codex App control endpoint epoch is invalid');
2014
+ }
2015
+ return JSON.stringify({
2016
+ version: CONTROL_WIRE_VERSION,
2017
+ type: 'accepted',
2018
+ sessionId,
2019
+ generation,
2020
+ challenge,
2021
+ ...(endpointEpoch ? { endpointEpoch } : {}),
2022
+ });
2023
+ }
2024
+ export function encodeCodexAppControlAck(sessionId, generation, challenge, seq) {
2025
+ if (!sessionId || !isValidGeneration(generation) || !isValidChallenge(challenge)
2026
+ || !Number.isSafeInteger(seq) || seq <= 0) {
2027
+ throw new Error('Codex App control acknowledgement metadata is invalid');
2028
+ }
2029
+ return JSON.stringify({
2030
+ version: CONTROL_WIRE_VERSION,
2031
+ type: 'ack',
2032
+ sessionId,
2033
+ generation,
2034
+ challenge,
2035
+ seq,
2036
+ });
2037
+ }
2038
+ function authUnsigned(sessionId, generation, challenge) {
2039
+ return { sessionId, generation, challenge };
2040
+ }
2041
+ export function encodeCodexAppControlAuth(privateKey, sessionId, generation, challenge) {
2042
+ if (!sessionId || !isValidGeneration(generation) || !isValidChallenge(challenge)) {
2043
+ throw new Error('Codex App control authentication metadata is invalid');
2044
+ }
2045
+ const unsigned = authUnsigned(sessionId, generation, challenge);
2046
+ return JSON.stringify({
2047
+ version: CONTROL_WIRE_VERSION,
2048
+ type: 'auth',
2049
+ ...unsigned,
2050
+ signature: signature(privateKey, 'auth', unsigned),
2051
+ });
2052
+ }
2053
+ function markerUnsigned(sessionId, generation, challenge, seq, kind, payload) {
2054
+ return { sessionId, generation, challenge, seq, kind, payload };
2055
+ }
2056
+ export function encodeCodexAppSignedControlMarker(privateKey, sessionId, generation, challenge, seq, kind, payload) {
2057
+ if (!sessionId || !isValidGeneration(generation) || !isValidChallenge(challenge)
2058
+ || !Number.isSafeInteger(seq) || seq <= 0 || !kind) {
2059
+ throw new Error('Codex App control marker metadata is invalid');
2060
+ }
2061
+ const unsigned = markerUnsigned(sessionId, generation, challenge, seq, kind, payload);
2062
+ return JSON.stringify({
2063
+ version: CONTROL_WIRE_VERSION,
2064
+ type: 'marker',
2065
+ ...unsigned,
2066
+ signature: signature(privateKey, 'marker', unsigned),
2067
+ });
2068
+ }
2069
+ export function parseCodexAppControlWireRecord(line) {
2070
+ try {
2071
+ const value = JSON.parse(line);
2072
+ if (value.version !== CONTROL_WIRE_VERSION || typeof value.sessionId !== 'string' || !value.sessionId)
2073
+ return undefined;
2074
+ if (value.type === 'challenge' && isValidChallenge(value.challenge)) {
2075
+ return value;
2076
+ }
2077
+ if (value.type === 'auth' && isValidGeneration(value.generation)
2078
+ && isValidChallenge(value.challenge)
2079
+ && typeof value.signature === 'string' && SIGNATURE_RE.test(value.signature)) {
2080
+ return value;
2081
+ }
2082
+ if (value.type === 'accepted' && isValidGeneration(value.generation)
2083
+ && isValidChallenge(value.challenge)
2084
+ && (value.endpointEpoch === undefined || isValidGeneration(value.endpointEpoch))) {
2085
+ return value;
2086
+ }
2087
+ if (value.type === 'ack' && isValidGeneration(value.generation)
2088
+ && isValidChallenge(value.challenge)
2089
+ && Number.isSafeInteger(value.seq) && Number(value.seq) > 0) {
2090
+ return value;
2091
+ }
2092
+ if (value.type === 'marker' && isValidGeneration(value.generation)
2093
+ && isValidChallenge(value.challenge)
2094
+ && Number.isSafeInteger(value.seq) && Number(value.seq) > 0
2095
+ && typeof value.kind === 'string' && value.kind.length > 0
2096
+ && value.payload && typeof value.payload === 'object' && !Array.isArray(value.payload)
2097
+ && typeof value.signature === 'string' && SIGNATURE_RE.test(value.signature)) {
2098
+ return value;
2099
+ }
2100
+ return undefined;
2101
+ }
2102
+ catch {
2103
+ return undefined;
2104
+ }
2105
+ }
2106
+ /**
2107
+ * Pure runner-side handshake state machine. Keeping the phase checks here
2108
+ * makes repeated challenges, wrong locator epochs, and out-of-order ACKs
2109
+ * executable in unit tests instead of relying on source-string assertions.
2110
+ */
2111
+ export class CodexAppControlRunnerHandshake {
2112
+ expectedSessionId;
2113
+ expectedGeneration;
2114
+ expectedEndpointEpoch;
2115
+ phase = 'challenge';
2116
+ challengeValue;
2117
+ constructor(expectedSessionId, expectedGeneration, expectedEndpointEpoch) {
2118
+ this.expectedSessionId = expectedSessionId;
2119
+ this.expectedGeneration = expectedGeneration;
2120
+ this.expectedEndpointEpoch = expectedEndpointEpoch;
2121
+ }
2122
+ handle(record, sentThrough) {
2123
+ if (!record || record.sessionId !== this.expectedSessionId)
2124
+ return { type: 'reject' };
2125
+ if (this.phase === 'challenge' && record.type === 'challenge') {
2126
+ this.challengeValue = record.challenge;
2127
+ this.phase = 'accepted';
2128
+ return { type: 'authenticate', challenge: record.challenge };
2129
+ }
2130
+ if (this.phase === 'accepted'
2131
+ && record.type === 'accepted'
2132
+ && record.generation === this.expectedGeneration
2133
+ && record.challenge === this.challengeValue
2134
+ && (this.expectedEndpointEpoch === undefined
2135
+ || record.endpointEpoch === this.expectedEndpointEpoch)) {
2136
+ this.phase = 'active';
2137
+ return { type: 'accepted', challenge: record.challenge };
2138
+ }
2139
+ if (this.phase === 'active'
2140
+ && record.type === 'ack'
2141
+ && record.generation === this.expectedGeneration
2142
+ && record.challenge === this.challengeValue
2143
+ && record.seq > 0
2144
+ && record.seq <= sentThrough) {
2145
+ return { type: 'ack', seq: record.seq };
2146
+ }
2147
+ return { type: 'reject' };
2148
+ }
2149
+ get active() {
2150
+ return this.phase === 'active';
2151
+ }
2152
+ }
2153
+ export function verifyCodexAppControlAuth(auth, publicKey) {
2154
+ return signatureValid(publicKey, auth.signature, 'auth', authUnsigned(auth.sessionId, auth.generation, auth.challenge));
2155
+ }
2156
+ export function authenticateCodexAppControlCandidate(input) {
2157
+ if (!input.state
2158
+ || input.auth.sessionId !== input.sessionId
2159
+ || input.auth.challenge !== input.challenge)
2160
+ return undefined;
2161
+ const identity = input.state.identities.find(candidate => candidate.generation === input.auth.generation);
2162
+ return identity && verifyCodexAppControlAuth(input.auth, identity.publicKey)
2163
+ ? identity
2164
+ : undefined;
2165
+ }
2166
+ export function verifyCodexAppSignedControlMarker(marker, publicKey) {
2167
+ return signatureValid(publicKey, marker.signature, 'marker', markerUnsigned(marker.sessionId, marker.generation, marker.challenge, marker.seq, marker.kind, marker.payload));
2168
+ }
2169
+ /**
2170
+ * Marker sequences may begin above one after a worker replacement, but every
2171
+ * record on one authenticated connection must then be contiguous. This keeps
2172
+ * a skipped final fragment from being hidden by a later cumulative ACK.
2173
+ */
2174
+ export class CodexAppControlSequenceFence {
2175
+ previous;
2176
+ accept(seq) {
2177
+ if (!Number.isSafeInteger(seq) || seq <= 0)
2178
+ return false;
2179
+ if (this.previous !== undefined && seq !== this.previous + 1)
2180
+ return false;
2181
+ this.previous = seq;
2182
+ return true;
2183
+ }
2184
+ }
2185
+ const FINAL_ID_MAX_BYTES = 512;
2186
+ const STRICT_BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
2187
+ /**
2188
+ * Per-connection final transaction assembler. Start and chunks deliberately
2189
+ * remain unacknowledged; only a valid, complete end record is eligible for the
2190
+ * cumulative ACK. Any gap or interleaving rejects the connection, so the
2191
+ * runner must re-sign and replay the complete transaction under a new
2192
+ * challenge.
2193
+ */
2194
+ export class CodexAppControlFinalAssembler {
2195
+ active;
2196
+ accept(kind, payload) {
2197
+ const isTransactionKind = kind === 'final-start' || kind === 'final-chunk' || kind === 'final-end';
2198
+ if (!isTransactionKind) {
2199
+ if (kind === 'final' || kind.startsWith('final-')) {
2200
+ return this.reject(`unsupported final record ${kind}`);
2201
+ }
2202
+ return this.active
2203
+ ? this.reject(`interleaved ${kind} record inside final transaction`)
2204
+ : { status: 'not-final' };
2205
+ }
2206
+ const id = typeof payload.id === 'string' ? payload.id : '';
2207
+ if (!id || Buffer.byteLength(id, 'utf8') > FINAL_ID_MAX_BYTES) {
2208
+ return this.reject('invalid final transaction id');
2209
+ }
2210
+ if (kind === 'final-start') {
2211
+ const total = typeof payload.total === 'number' && Number.isSafeInteger(payload.total)
2212
+ ? payload.total
2213
+ : -1;
2214
+ if (this.active || total < 0 || total > 2_048) {
2215
+ return this.reject('invalid or overlapping final-start');
2216
+ }
2217
+ const { id: _id, total: _total, ...metadata } = payload;
2218
+ this.active = { id, total, metadata, chunks: [], bytes: 0 };
2219
+ return { status: 'accepted' };
2220
+ }
2221
+ const assembly = this.active;
2222
+ if (!assembly || id !== assembly.id) {
2223
+ return this.reject('final fragment has no matching start');
2224
+ }
2225
+ if (kind === 'final-chunk') {
2226
+ const index = typeof payload.index === 'number' && Number.isSafeInteger(payload.index)
2227
+ ? payload.index
2228
+ : -1;
2229
+ if (index !== assembly.chunks.length || index >= assembly.total
2230
+ || typeof payload.data !== 'string' || !STRICT_BASE64_RE.test(payload.data)) {
2231
+ return this.reject('invalid, duplicate, or out-of-order final chunk');
2232
+ }
2233
+ const chunk = Buffer.from(payload.data, 'base64');
2234
+ if (chunk.length === 0 || chunk.length > CODEX_APP_CONTROL_FINAL_CHUNK_BYTES) {
2235
+ return this.reject('final chunk size is invalid');
2236
+ }
2237
+ assembly.bytes += chunk.length;
2238
+ if (assembly.bytes > CODEX_APP_CONTROL_FINAL_MAX_BYTES) {
2239
+ return this.reject('final transaction exceeds the byte limit');
2240
+ }
2241
+ assembly.chunks.push(chunk);
2242
+ return { status: 'accepted' };
2243
+ }
2244
+ const endTotal = typeof payload.total === 'number' && Number.isSafeInteger(payload.total)
2245
+ ? payload.total
2246
+ : -1;
2247
+ if (endTotal !== assembly.total || assembly.chunks.length !== assembly.total) {
2248
+ return this.reject('incomplete or inconsistent final-end');
2249
+ }
2250
+ this.active = undefined;
2251
+ return {
2252
+ status: 'complete',
2253
+ payload: {
2254
+ ...assembly.metadata,
2255
+ content: Buffer.concat(assembly.chunks, assembly.bytes).toString('utf8'),
2256
+ },
2257
+ };
2258
+ }
2259
+ clear() {
2260
+ this.active = undefined;
2261
+ }
2262
+ reject(reason) {
2263
+ this.active = undefined;
2264
+ return { status: 'reject', reason };
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Per-worker replay window for authenticated runner generations. A runner may
2269
+ * reconnect after losing an ACK and legitimately re-sign the same sequence
2270
+ * under the new connection challenge; the worker ACKs that retry without
2271
+ * applying its lifecycle/final side effects twice.
2272
+ */
2273
+ export class CodexAppControlReplayWindow {
2274
+ highWaterByGeneration = new Map();
2275
+ highWater(generation) {
2276
+ return this.highWaterByGeneration.get(generation) ?? 0;
2277
+ }
2278
+ hasSeen(generation, seq) {
2279
+ return seq <= this.highWater(generation);
2280
+ }
2281
+ commit(generation, seq) {
2282
+ if (!isValidGeneration(generation) || !Number.isSafeInteger(seq) || seq <= 0) {
2283
+ throw new Error('Codex App control replay sequence is invalid');
2284
+ }
2285
+ if (seq > this.highWater(generation))
2286
+ this.highWaterByGeneration.set(generation, seq);
2287
+ }
2288
+ retainOnly(generation) {
2289
+ const retained = this.highWaterByGeneration.get(generation);
2290
+ this.highWaterByGeneration.clear();
2291
+ if (retained !== undefined)
2292
+ this.highWaterByGeneration.set(generation, retained);
2293
+ }
2294
+ }
2295
+ /** Bounded newline decoder; oversized attacker input is discarded until resync. */
2296
+ export class CodexAppControlLineDecoder {
2297
+ pending = Buffer.alloc(0);
2298
+ droppingOversized = false;
2299
+ push(chunk) {
2300
+ const lines = [];
2301
+ let droppedMalformed = false;
2302
+ let cursor = 0;
2303
+ while (cursor < chunk.length) {
2304
+ const nl = chunk.indexOf(0x0a, cursor);
2305
+ const end = nl >= 0 ? nl : chunk.length;
2306
+ const piece = chunk.subarray(cursor, end);
2307
+ cursor = nl >= 0 ? nl + 1 : chunk.length;
2308
+ if (this.droppingOversized) {
2309
+ droppedMalformed = true;
2310
+ if (nl >= 0)
2311
+ this.droppingOversized = false;
2312
+ continue;
2313
+ }
2314
+ if (this.pending.length + piece.length > CODEX_APP_CONTROL_LINE_MAX_BYTES) {
2315
+ this.pending = Buffer.alloc(0);
2316
+ droppedMalformed = true;
2317
+ if (nl < 0)
2318
+ this.droppingOversized = true;
2319
+ continue;
2320
+ }
2321
+ this.pending = Buffer.concat([this.pending, piece]);
2322
+ if (nl >= 0) {
2323
+ const line = this.pending.toString('utf8').trim();
2324
+ this.pending = Buffer.alloc(0);
2325
+ if (line)
2326
+ lines.push(line);
2327
+ }
2328
+ }
2329
+ return { lines, droppedMalformed };
2330
+ }
2331
+ clear() {
2332
+ this.pending = Buffer.alloc(0);
2333
+ this.droppingOversized = false;
2334
+ }
2335
+ }
2336
+ //# sourceMappingURL=codex-app-control.js.map