@coreplane/switchboard 1.204.0 → 1.206.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 (389) hide show
  1. package/dist/assets/Dockerfile +6 -3
  2. package/dist/assets/config/config.example.yaml +56 -4
  3. package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
  4. package/dist/assets/deploy/cloudflare/shared.ts +9 -0
  5. package/dist/assets/deploy/cloudflare/worker.ts +142 -6
  6. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
  7. package/dist/assets/deploy/cloudflare-memory/worker.ts +381 -7
  8. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +22 -2
  9. package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
  11. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +4 -1
  12. package/dist/assets/deploy/secrets.manifest.json +1 -1
  13. package/dist/assets/package-lock.json +291 -97
  14. package/dist/assets/package.json +1 -1
  15. package/dist/assets/source.json +3 -3
  16. package/dist/assets/src/agents/registry.ts +517 -0
  17. package/dist/assets/src/config/profile.ts +229 -0
  18. package/dist/assets/src/core/authz/policy.ts +15 -0
  19. package/dist/assets/src/core/coordinator/contract.ts +239 -0
  20. package/dist/assets/src/core/coordinator/driver.ts +501 -0
  21. package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
  22. package/dist/assets/src/core/delivery.ts +829 -0
  23. package/dist/assets/src/core/deliverySnapshotStore.ts +260 -0
  24. package/dist/assets/src/core/reviewVerdict.ts +296 -0
  25. package/dist/assets/src/core/reviewedHead.ts +77 -0
  26. package/dist/assets/src/core/runEvents.ts +4 -2
  27. package/dist/assets/src/core/runLedger/decisions.ts +2 -1
  28. package/dist/assets/src/core/runLedger/types.ts +23 -1
  29. package/dist/assets/src/core/runRecord.ts +100 -0
  30. package/dist/assets/src/core/ship/contract.ts +502 -0
  31. package/dist/assets/src/core/ship/coordinator.ts +1039 -0
  32. package/dist/assets/src/core/workerError.ts +12 -0
  33. package/dist/assets/web/dist/.vite/manifest.json +2770 -70
  34. package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +1 -0
  35. package/dist/assets/web/dist/assets/{CostsPage-XQ-fQzdz.js → CostsPage-DGSE48l1.js} +1 -1
  36. package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
  37. package/dist/assets/web/dist/assets/NotFoundPage-BzWS4Ca1.js +1 -0
  38. package/dist/assets/web/dist/assets/ResidentDetailPage-D3P21yeI.js +1 -0
  39. package/dist/assets/web/dist/assets/ResidentsIndexPage-DOYqnZ1q.js +1 -0
  40. package/dist/assets/web/dist/assets/RunRoutePage-OmvrvPXY.js +12 -0
  41. package/dist/assets/web/dist/assets/RunRoutePage-f6TSNPeW.css +1 -0
  42. package/dist/assets/web/dist/assets/RunsIndexPage-DWbSQtL4.js +1 -0
  43. package/dist/assets/web/dist/assets/RunsTabs-DEICQ4FY.js +1 -0
  44. package/dist/assets/web/dist/assets/ScheduledPage-CPKfJ4mR.js +1 -0
  45. package/dist/assets/web/dist/assets/StatusDot-COr8jTyM.js +1 -0
  46. package/dist/assets/web/dist/assets/Tooltip-fOqTZkNT.js +1 -0
  47. package/dist/assets/web/dist/assets/abap-CLvhMVsD.js +1 -0
  48. package/dist/assets/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  49. package/dist/assets/web/dist/assets/ada-C5qYipkI.js +1 -0
  50. package/dist/assets/web/dist/assets/ahk-CA9gdOdF.js +1 -0
  51. package/dist/assets/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  52. package/dist/assets/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  53. package/dist/assets/web/dist/assets/angular-html-Cw130Zyi.js +1 -0
  54. package/dist/assets/web/dist/assets/angular-ts-DY1m4C2s.js +1 -0
  55. package/dist/assets/web/dist/assets/apache-U0d_L8uA.js +1 -0
  56. package/dist/assets/web/dist/assets/apex-KFGFyKwG.js +1 -0
  57. package/dist/assets/web/dist/assets/apl-W6Ty7v05.js +1 -0
  58. package/dist/assets/web/dist/assets/applescript-CCn79oCD.js +1 -0
  59. package/dist/assets/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  60. package/dist/assets/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  61. package/dist/assets/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  62. package/dist/assets/web/dist/assets/astro-DlLVHwVk.js +1 -0
  63. package/dist/assets/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  64. package/dist/assets/web/dist/assets/awk-BWXHIvNe.js +1 -0
  65. package/dist/assets/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  66. package/dist/assets/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  67. package/dist/assets/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  68. package/dist/assets/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  69. package/dist/assets/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  70. package/dist/assets/web/dist/assets/beancount-D-usSTwE.js +1 -0
  71. package/dist/assets/web/dist/assets/berry-DKpUyyne.js +1 -0
  72. package/dist/assets/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  73. package/dist/assets/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  74. package/dist/assets/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  75. package/dist/assets/web/dist/assets/blade-Bco3tDh4.js +1 -0
  76. package/dist/assets/web/dist/assets/bsl-_VjlB9mv.js +1 -0
  77. package/dist/assets/web/dist/assets/c-1LaFY_cj.js +1 -0
  78. package/dist/assets/web/dist/assets/c3-D0apG41V.js +1 -0
  79. package/dist/assets/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  80. package/dist/assets/web/dist/assets/cairo-DLTphjLi.js +1 -0
  81. package/dist/assets/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  82. package/dist/assets/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  83. package/dist/assets/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  84. package/dist/assets/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  85. package/dist/assets/web/dist/assets/chapel-B-OvrOL5.js +1 -0
  86. package/dist/assets/web/dist/assets/clarity-CtfBBspg.js +1 -0
  87. package/dist/assets/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  88. package/dist/assets/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  89. package/dist/assets/web/dist/assets/cobol-Ch3EPlHu.js +1 -0
  90. package/dist/assets/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  91. package/dist/assets/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  92. package/dist/assets/web/dist/assets/coffee-hZuITnto.js +1 -0
  93. package/dist/assets/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  94. package/dist/assets/web/dist/assets/coq-DuBRPxYg.js +1 -0
  95. package/dist/assets/web/dist/assets/cpp-B8iRVo8m.js +1 -0
  96. package/dist/assets/web/dist/assets/crystal-DKveI7lr.js +1 -0
  97. package/dist/assets/web/dist/assets/csharp-oqKa8noW.js +1 -0
  98. package/dist/assets/web/dist/assets/css-_2_CD-rr.js +1 -0
  99. package/dist/assets/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  100. package/dist/assets/web/dist/assets/cue-CE9AQfxI.js +1 -0
  101. package/dist/assets/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  102. package/dist/assets/web/dist/assets/d-qD-0Kul2.js +1 -0
  103. package/dist/assets/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  104. package/dist/assets/web/dist/assets/dart-CnvKMtbv.js +1 -0
  105. package/dist/assets/web/dist/assets/dax-BkyTk9wS.js +1 -0
  106. package/dist/assets/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  107. package/dist/assets/web/dist/assets/diff-woXpYk--.js +1 -0
  108. package/dist/assets/web/dist/assets/dist-BVjAWgkb.js +1547 -0
  109. package/dist/assets/web/dist/assets/docker-IyjqRm3v.js +1 -0
  110. package/dist/assets/web/dist/assets/dotenv-_5a1GRtc.js +1 -0
  111. package/dist/assets/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  112. package/dist/assets/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  113. package/dist/assets/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  114. package/dist/assets/web/dist/assets/edge-BenMDPct.js +1 -0
  115. package/dist/assets/web/dist/assets/elixir-CBbdEqG6.js +1 -0
  116. package/dist/assets/web/dist/assets/elm-DMub_2lY.js +1 -0
  117. package/dist/assets/web/dist/assets/emacs-lisp-c_oH4hRZ.js +1 -0
  118. package/dist/assets/web/dist/assets/erb-CI9He_Up.js +1 -0
  119. package/dist/assets/web/dist/assets/erlang-Cphh6RMH.js +1 -0
  120. package/dist/assets/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  121. package/dist/assets/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  122. package/dist/assets/web/dist/assets/{favicon-CFfbl2AI.js → favicon-CWPcvWvp.js} +1 -1
  123. package/dist/assets/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  124. package/dist/assets/web/dist/assets/fish-BJitypiv.js +1 -0
  125. package/dist/assets/web/dist/assets/fluent-C03EYrpw.js +1 -0
  126. package/dist/assets/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  127. package/dist/assets/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  128. package/dist/assets/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
  129. package/dist/assets/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
  130. package/dist/assets/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  131. package/dist/assets/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  132. package/dist/assets/web/dist/assets/genie-CV2tkWYe.js +1 -0
  133. package/dist/assets/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  134. package/dist/assets/web/dist/assets/git-commit-BSykSTBG.js +1 -0
  135. package/dist/assets/web/dist/assets/git-rebase-BBfhnyF8.js +1 -0
  136. package/dist/assets/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  137. package/dist/assets/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  138. package/dist/assets/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  139. package/dist/assets/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  140. package/dist/assets/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  141. package/dist/assets/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  142. package/dist/assets/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  143. package/dist/assets/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  144. package/dist/assets/web/dist/assets/glimmer-js-CR9Fxrau.js +1 -0
  145. package/dist/assets/web/dist/assets/glimmer-ts-CwFwX_XS.js +1 -0
  146. package/dist/assets/web/dist/assets/glsl-Daiodp1r.js +1 -0
  147. package/dist/assets/web/dist/assets/gn-ilITqXS6.js +1 -0
  148. package/dist/assets/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  149. package/dist/assets/web/dist/assets/go-rLFTqkRN.js +1 -0
  150. package/dist/assets/web/dist/assets/graphql-DZvrsEkB.js +1 -0
  151. package/dist/assets/web/dist/assets/groovy-CacY0gHj.js +1 -0
  152. package/dist/assets/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  153. package/dist/assets/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  154. package/dist/assets/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  155. package/dist/assets/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  156. package/dist/assets/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  157. package/dist/assets/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  158. package/dist/assets/web/dist/assets/hack-BaXOzdg7.js +1 -0
  159. package/dist/assets/web/dist/assets/haml-CMskq_VA.js +1 -0
  160. package/dist/assets/web/dist/assets/handlebars-Dhw_f7jD.js +1 -0
  161. package/dist/assets/web/dist/assets/haskell-D8IpX4py.js +1 -0
  162. package/dist/assets/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  163. package/dist/assets/web/dist/assets/hcl-Dh228itO.js +1 -0
  164. package/dist/assets/web/dist/assets/hjson-CxZEssPk.js +1 -0
  165. package/dist/assets/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  166. package/dist/assets/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  167. package/dist/assets/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  168. package/dist/assets/web/dist/assets/houston-CsvMBhTu.js +1 -0
  169. package/dist/assets/web/dist/assets/html-C9k99z0z.js +1 -0
  170. package/dist/assets/web/dist/assets/html-derivative-VckPItXN.js +1 -0
  171. package/dist/assets/web/dist/assets/http-DXU_3h9v.js +1 -0
  172. package/dist/assets/web/dist/assets/hurl-KU24JNdp.js +1 -0
  173. package/dist/assets/web/dist/assets/hxml-Dcm88srW.js +1 -0
  174. package/dist/assets/web/dist/assets/hy-CZbG8q4J.js +1 -0
  175. package/dist/assets/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  176. package/dist/assets/web/dist/assets/indexFormat-B-pd4r7Y.js +1 -0
  177. package/dist/assets/web/dist/assets/ini-B5eOa1yu.js +1 -0
  178. package/dist/assets/web/dist/assets/java-BbZjTNgf.js +1 -0
  179. package/dist/assets/web/dist/assets/javascript-DXCdqcZZ.js +1 -0
  180. package/dist/assets/web/dist/assets/jinja-pDlsjTVp.js +1 -0
  181. package/dist/assets/web/dist/assets/jison-DsXDk66e.js +1 -0
  182. package/dist/assets/web/dist/assets/json-tCxXfRgG.js +1 -0
  183. package/dist/assets/web/dist/assets/json5-BR5RXkoi.js +1 -0
  184. package/dist/assets/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  185. package/dist/assets/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  186. package/dist/assets/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  187. package/dist/assets/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  188. package/dist/assets/web/dist/assets/jsx-B0ZYkCmi.js +1 -0
  189. package/dist/assets/web/dist/assets/julia-BLzzeI6x.js +1 -0
  190. package/dist/assets/web/dist/assets/just-DvD2_60c.js +1 -0
  191. package/dist/assets/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  192. package/dist/assets/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  193. package/dist/assets/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  194. package/dist/assets/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  195. package/dist/assets/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  196. package/dist/assets/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  197. package/dist/assets/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  198. package/dist/assets/web/dist/assets/latex-511zn3h7.js +1 -0
  199. package/dist/assets/web/dist/assets/lean-CewbzKMR.js +1 -0
  200. package/dist/assets/web/dist/assets/less-DVTAwKKz.js +1 -0
  201. package/dist/assets/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  202. package/dist/assets/web/dist/assets/liquid-BD0BkMox.js +1 -0
  203. package/dist/assets/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  204. package/dist/assets/web/dist/assets/log-BNLmms1o.js +1 -0
  205. package/dist/assets/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  206. package/dist/assets/web/dist/assets/lua-NFicKm6N.js +1 -0
  207. package/dist/assets/web/dist/assets/luau-C2pBCSgt.js +1 -0
  208. package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
  209. package/dist/assets/web/dist/assets/main-DZbJaqUb.js +28 -0
  210. package/dist/assets/web/dist/assets/make-Dixweg8N.js +1 -0
  211. package/dist/assets/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  212. package/dist/assets/web/dist/assets/marko-1-G_nEXK.js +1 -0
  213. package/dist/assets/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  214. package/dist/assets/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  215. package/dist/assets/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  216. package/dist/assets/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  217. package/dist/assets/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  218. package/dist/assets/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  219. package/dist/assets/web/dist/assets/mdc-DUe3AQLp.js +1 -0
  220. package/dist/assets/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  221. package/dist/assets/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  222. package/dist/assets/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  223. package/dist/assets/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  224. package/dist/assets/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  225. package/dist/assets/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  226. package/dist/assets/web/dist/assets/monokai-B_bV9PQ9.js +1 -0
  227. package/dist/assets/web/dist/assets/moonbit-DgjMoLWr.js +1 -0
  228. package/dist/assets/web/dist/assets/move-BYnGhZoM.js +1 -0
  229. package/dist/assets/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  230. package/dist/assets/web/dist/assets/nextflow-BJtWHP5T.js +1 -0
  231. package/dist/assets/web/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
  232. package/dist/assets/web/dist/assets/nginx-Dg379_bA.js +1 -0
  233. package/dist/assets/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  234. package/dist/assets/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  235. package/dist/assets/web/dist/assets/nim-BnFq97ZO.js +1 -0
  236. package/dist/assets/web/dist/assets/nix-IvuFDN5E.js +1 -0
  237. package/dist/assets/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  238. package/dist/assets/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  239. package/dist/assets/web/dist/assets/nushell-DHYfO66s.js +1 -0
  240. package/dist/assets/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  241. package/dist/assets/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  242. package/dist/assets/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  243. package/dist/assets/web/dist/assets/odin-B1RWQWA5.js +1 -0
  244. package/dist/assets/web/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
  245. package/dist/assets/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  246. package/dist/assets/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  247. package/dist/assets/web/dist/assets/org-F0uiwGvq.js +1 -0
  248. package/dist/assets/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  249. package/dist/assets/web/dist/assets/perl-B0euczkl.js +1 -0
  250. package/dist/assets/web/dist/assets/php-DkL3k_n7.js +1 -0
  251. package/dist/assets/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
  252. package/dist/assets/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  253. package/dist/assets/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  254. package/dist/assets/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  255. package/dist/assets/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  256. package/dist/assets/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
  257. package/dist/assets/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  258. package/dist/assets/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  259. package/dist/assets/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  260. package/dist/assets/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  261. package/dist/assets/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  262. package/dist/assets/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  263. package/dist/assets/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  264. package/dist/assets/web/dist/assets/po-BiJDBrnU.js +1 -0
  265. package/dist/assets/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  266. package/dist/assets/web/dist/assets/polar-C7UOKdEL.js +1 -0
  267. package/dist/assets/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  268. package/dist/assets/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  269. package/dist/assets/web/dist/assets/powershell-CCSX75yM.js +1 -0
  270. package/dist/assets/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  271. package/dist/assets/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  272. package/dist/assets/web/dist/assets/proto-DB4EqR-F.js +1 -0
  273. package/dist/assets/web/dist/assets/pug-L_OjjZP4.js +1 -0
  274. package/dist/assets/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  275. package/dist/assets/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  276. package/dist/assets/web/dist/assets/python-gzcpVVnB.js +1 -0
  277. package/dist/assets/web/dist/assets/qml-BMjB00Zz.js +1 -0
  278. package/dist/assets/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  279. package/dist/assets/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  280. package/dist/assets/web/dist/assets/r-DoeLdnqR.js +1 -0
  281. package/dist/assets/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  282. package/dist/assets/web/dist/assets/raku-B3gFvitq.js +1 -0
  283. package/dist/assets/web/dist/assets/razor-BzYNkAWy.js +1 -0
  284. package/dist/assets/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  285. package/dist/assets/web/dist/assets/red-DX1BQOs8.js +1 -0
  286. package/dist/assets/web/dist/assets/reg-CRGYupPL.js +1 -0
  287. package/dist/assets/web/dist/assets/regexp-CPmElMk3.js +1 -0
  288. package/dist/assets/web/dist/assets/rel-BtDbiS_P.js +1 -0
  289. package/dist/assets/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  290. package/dist/assets/web/dist/assets/ron-VUp2lXgN.js +1 -0
  291. package/dist/assets/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  292. package/dist/assets/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  293. package/dist/assets/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  294. package/dist/assets/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  295. package/dist/assets/web/dist/assets/rst-DrsjgfI7.js +1 -0
  296. package/dist/assets/web/dist/assets/ruby-cB42ppvx.js +1 -0
  297. package/dist/assets/web/dist/assets/rust-Cfkwpbl8.js +1 -0
  298. package/dist/assets/web/dist/assets/sas-R5Y3NM_I.js +1 -0
  299. package/dist/assets/web/dist/assets/sass-DXrisJhu.js +1 -0
  300. package/dist/assets/web/dist/assets/scala-BiFt_7mu.js +1 -0
  301. package/dist/assets/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  302. package/dist/assets/web/dist/assets/scss-C-zWxV9s.js +1 -0
  303. package/dist/assets/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  304. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +6 -0
  305. package/dist/assets/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
  306. package/dist/assets/web/dist/assets/shellscript-oJF96aAU.js +1 -0
  307. package/dist/assets/web/dist/assets/shellsession-CznECKyi.js +1 -0
  308. package/dist/assets/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  309. package/dist/assets/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  310. package/dist/assets/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  311. package/dist/assets/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  312. package/dist/assets/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  313. package/dist/assets/web/dist/assets/solarized-dark-UdsmMDCL.js +1 -0
  314. package/dist/assets/web/dist/assets/solarized-light-C1IUL_tW.js +1 -0
  315. package/dist/assets/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  316. package/dist/assets/web/dist/assets/soy-wjHLSRag.js +1 -0
  317. package/dist/assets/web/dist/assets/sparql-D_iOobhT.js +1 -0
  318. package/dist/assets/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  319. package/dist/assets/web/dist/assets/sql-C_BM8IOW.js +1 -0
  320. package/dist/assets/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  321. package/dist/assets/web/dist/assets/stata-D31HO8aQ.js +1 -0
  322. package/dist/assets/web/dist/assets/stylus-B6D30XZt.js +1 -0
  323. package/dist/assets/web/dist/assets/surrealql-CKCLyplA.js +1 -0
  324. package/dist/assets/web/dist/assets/svelte-3geWk2iu.js +1 -0
  325. package/dist/assets/web/dist/assets/swift-CyEgAFGc.js +1 -0
  326. package/dist/assets/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  327. package/dist/assets/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  328. package/dist/assets/web/dist/assets/systemd-BxMlprV5.js +1 -0
  329. package/dist/assets/web/dist/assets/talonscript-CohzipZa.js +1 -0
  330. package/dist/assets/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  331. package/dist/assets/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  332. package/dist/assets/web/dist/assets/templ-DiJr_hTD.js +1 -0
  333. package/dist/assets/web/dist/assets/terraform-DswuEJGm.js +1 -0
  334. package/dist/assets/web/dist/assets/tex-BEJGWyeF.js +1 -0
  335. package/dist/assets/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  336. package/dist/assets/web/dist/assets/toml-CcmNWLt0.js +1 -0
  337. package/dist/assets/web/dist/assets/ts-tags-C8Mzdpxx.js +1 -0
  338. package/dist/assets/web/dist/assets/tsv-sltzmVWM.js +1 -0
  339. package/dist/assets/web/dist/assets/tsx-UQiSm_p3.js +1 -0
  340. package/dist/assets/web/dist/assets/turtle-ByJddavk.js +1 -0
  341. package/dist/assets/web/dist/assets/twig-_Pvzkeo9.js +1 -0
  342. package/dist/assets/web/dist/assets/typescript-BWMkINKK.js +1 -0
  343. package/dist/assets/web/dist/assets/typespec-BRdr0IET.js +1 -0
  344. package/dist/assets/web/dist/assets/typst-Bdg9m-e7.js +1 -0
  345. package/dist/assets/web/dist/assets/v-DBj4K9d_.js +1 -0
  346. package/dist/assets/web/dist/assets/vala-zf12oZj6.js +1 -0
  347. package/dist/assets/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  348. package/dist/assets/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  349. package/dist/assets/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  350. package/dist/assets/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  351. package/dist/assets/web/dist/assets/viml-DvXPmvsu.js +1 -0
  352. package/dist/assets/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  353. package/dist/assets/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  354. package/dist/assets/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  355. package/dist/assets/web/dist/assets/vue-CDZrrAZ8.js +1 -0
  356. package/dist/assets/web/dist/assets/vue-html-tAlpNhfN.js +1 -0
  357. package/dist/assets/web/dist/assets/vue-vine-C27UF_rh.js +1 -0
  358. package/dist/assets/web/dist/assets/vyper-CgoNMtux.js +1 -0
  359. package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +1 -0
  360. package/dist/assets/web/dist/assets/wasm-BnjxR4X6.js +1 -0
  361. package/dist/assets/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  362. package/dist/assets/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  363. package/dist/assets/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  364. package/dist/assets/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  365. package/dist/assets/web/dist/assets/wit-DdvCle-K.js +1 -0
  366. package/dist/assets/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  367. package/dist/assets/web/dist/assets/xml-CxeDr9Zh.js +1 -0
  368. package/dist/assets/web/dist/assets/xsl-Bpi0uUnM.js +1 -0
  369. package/dist/assets/web/dist/assets/yaml-BtUgZ1GN.js +1 -0
  370. package/dist/assets/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  371. package/dist/assets/web/dist/assets/zig-CMLA9XwU.js +1 -0
  372. package/dist/cli.js +6936 -3347
  373. package/package.json +1 -1
  374. package/dist/assets/web/dist/assets/AppShell-Bw3-_TTE.js +0 -1
  375. package/dist/assets/web/dist/assets/DeliveryPage-BRwcQyr7.js +0 -1
  376. package/dist/assets/web/dist/assets/NotFoundPage-RjH-9dPy.js +0 -1
  377. package/dist/assets/web/dist/assets/ResidentDetailPage-BRy5wkv9.js +0 -1
  378. package/dist/assets/web/dist/assets/ResidentsIndexPage-DAEVLN8j.js +0 -1
  379. package/dist/assets/web/dist/assets/RunRoutePage-B1KHmkZ9.js +0 -126
  380. package/dist/assets/web/dist/assets/RunRoutePage-CdbrbqU2.css +0 -1
  381. package/dist/assets/web/dist/assets/RunsIndexPage-3hUWFpFV.js +0 -1
  382. package/dist/assets/web/dist/assets/RunsTabs-Qn_5TVJU.js +0 -1
  383. package/dist/assets/web/dist/assets/ScheduledPage-DysVvVm1.js +0 -1
  384. package/dist/assets/web/dist/assets/StatusDot-Bug2a6T6.js +0 -1
  385. package/dist/assets/web/dist/assets/Tooltip-C2eEUbwn.js +0 -1
  386. package/dist/assets/web/dist/assets/main-0uhp-taL.js +0 -28
  387. package/dist/assets/web/dist/assets/main-Xickfv8V.css +0 -1
  388. package/dist/assets/web/dist/assets/seed-B8mQEhhw.js +0 -6
  389. package/dist/assets/web/dist/assets/wallClock-DI4HIEN5.js +0 -1
@@ -10,6 +10,13 @@ import {
10
10
  } from "../../src/core/memory/engine.ts";
11
11
  import { tokenize } from "../../src/core/memory/scorer.ts";
12
12
  import { FIRING_DETAIL_MAX, isScheduleFiring, type ScheduleFiring } from "../../src/core/schedules.ts";
13
+ import { REPO_SLUG } from "../../src/core/delivery.ts";
14
+ import {
15
+ isDeliverySnapshot,
16
+ isDeliverySnapshotPatch,
17
+ type DeliverySnapshot,
18
+ type DeliverySnapshotPatch,
19
+ } from "../../src/core/deliverySnapshotStore.ts";
13
20
  import {
14
21
  applyRetention,
15
22
  clampRetentionPolicy,
@@ -41,6 +48,16 @@ import {
41
48
  reclaimPhase,
42
49
  selectReclaim,
43
50
  } from "../../src/core/runLedger/decisions.ts";
51
+ import {
52
+ IDEMPOTENCY_KEY_PATTERN,
53
+ INSTANCE_ID_PATTERN,
54
+ isCoordinatorInstance,
55
+ isCoordinatorUnit,
56
+ sendRunFinished,
57
+ type CoordinatorInstance,
58
+ type CoordinatorUnit,
59
+ type RunFinishedSend,
60
+ } from "../../src/core/coordinator/contract.ts";
44
61
  import {
45
62
  GEN_PATTERN,
46
63
  type ClaimRequest,
@@ -127,9 +144,20 @@ export interface Env {
127
144
  CONFIG: DurableObjectNamespace<ConfigDO>;
128
145
  /** Live-run transcripts (run-history item 32): one RunTranscriptDO per live run, named by run id. */
129
146
  RUN_TRANSCRIPTS: DurableObjectNamespace<RunTranscriptDO>;
147
+ /** Delivery snapshots (delivery item 10): ONE DeliveryDO (named "delivery"), one snapshot per repository. */
148
+ DELIVERY: DurableObjectNamespace<DeliveryDO>;
149
+ /** The ship coordinator Workflow in the bot's shim Worker (run-history item
150
+ * 47): where `RunHistoryDO.finish` sends `run finished:<runId>` for a record
151
+ * carrying `parentInstanceId`. Optional: this Worker deploys without it (the
152
+ * binding is a cross-script one, and the class must exist on the bot before
153
+ * the state Worker may name it), and a finish then commits with no event. */
154
+ SHIP_COORDINATOR?: Workflow;
130
155
  MEMORY_TOKEN?: string;
131
156
  }
132
157
 
158
+ /** The single DeliveryDO's name — every repository's snapshot lives in one object. */
159
+ const DELIVERY_OBJECT = "delivery";
160
+
133
161
  /** The single ConfigDO's name. */
134
162
  const CONFIG_OBJECT = "config";
135
163
  /** A config document key: short, lowercase, like `overrides`. */
@@ -142,6 +170,8 @@ const SCHEDULES_OBJECT = "schedules";
142
170
  const MAX_LIMIT = 50;
143
171
  /** Candidates per write batch (the reflection pass emits ≤6). */
144
172
  const MAX_BATCH = 50;
173
+ /** Unit rows one put may carry: a plan has tens of units, never hundreds. */
174
+ const MAX_UNITS_PER_PUT = 200;
145
175
  const MAX_TEXT_CHARS = 4000;
146
176
  const MAX_KEYWORDS = 20;
147
177
  const MAX_KEYWORD_CHARS = 64;
@@ -700,6 +730,172 @@ function parseStored<T>(text: string, guard: (v: unknown) => v is T): T | null {
700
730
  }
701
731
  }
702
732
 
733
+ // ---------------------------------------------------------------------------
734
+ // Durable Object: delivery snapshots, one per repository
735
+ // ---------------------------------------------------------------------------
736
+
737
+ // The delivery page's snapshot (docs/reference/specs/delivery.md item 10): the
738
+ // merged pull requests' facts over the snapshot window, as GitHub gave them,
739
+ // and when they were read. A busy repository's window is many MB — the review
740
+ // bodies and every workflow run of every branch — so a snapshot is stored as
741
+ // one row per pull request under a meta row, never as one JSON value (the
742
+ // per-row limit is 2 MB). A put replaces the repository's snapshot whole, in
743
+ // one transaction — the first read; a merge applies a refresh — the rows it
744
+ // re-read replace theirs by number, the rows that aged out go, the meta is
745
+ // replaced — so the hourly write is the change, not the window; a get
746
+ // reassembles the snapshot in pull request number order.
747
+
748
+ /** One pull request's facts may not exceed a fraction of the row limit; the fence names the pull request. */
749
+ const MAX_PULL_REQUEST_FACTS_BYTES = 1024 * 1024;
750
+
751
+ export class DeliveryDO extends DurableObject<Env> {
752
+ private readonly sql: SqlStorage;
753
+
754
+ constructor(ctx: DurableObjectState, env: Env) {
755
+ super(ctx, env);
756
+ this.sql = ctx.storage.sql;
757
+ this.sql.exec(`
758
+ CREATE TABLE IF NOT EXISTS snapshots (
759
+ repo TEXT PRIMARY KEY,
760
+ snapshot_at TEXT NOT NULL,
761
+ meta TEXT NOT NULL
762
+ );
763
+ CREATE TABLE IF NOT EXISTS pull_requests (
764
+ repo TEXT NOT NULL,
765
+ number INTEGER NOT NULL,
766
+ facts TEXT NOT NULL,
767
+ PRIMARY KEY (repo, number)
768
+ );
769
+ `);
770
+ }
771
+
772
+ /** Replace the repository's snapshot: the meta row and one row per pull request, in one transaction. */
773
+ async put(snapshot: DeliverySnapshot): Promise<number> {
774
+ const { prs, ...meta } = snapshot;
775
+ this.ctx.storage.transactionSync(() => {
776
+ this.sql.exec(`DELETE FROM pull_requests WHERE repo = ?`, snapshot.repo);
777
+ for (const pr of prs) {
778
+ this.sql.exec(
779
+ `INSERT OR REPLACE INTO pull_requests (repo, number, facts) VALUES (?, ?, ?)`,
780
+ snapshot.repo,
781
+ pr.number,
782
+ JSON.stringify(pr),
783
+ );
784
+ }
785
+ this.sql.exec(
786
+ `INSERT OR REPLACE INTO snapshots (repo, snapshot_at, meta) VALUES (?, ?, ?)`,
787
+ snapshot.repo,
788
+ snapshot.snapshotAt,
789
+ JSON.stringify(meta),
790
+ );
791
+ });
792
+ return prs.length;
793
+ }
794
+
795
+ /** Apply a refresh to the repository's snapshot in one transaction; the rows now stored, or null
796
+ * when the repository has no snapshot to merge into (a partial snapshot would claim a
797
+ * completeness it lacks — the caller writes whole instead). */
798
+ async merge(patch: DeliverySnapshotPatch): Promise<number | null> {
799
+ const { upsert, drop, ...meta } = patch;
800
+ return this.ctx.storage.transactionSync(() => {
801
+ const stored = this.sql.exec(`SELECT 1 FROM snapshots WHERE repo = ?`, patch.repo).toArray().length > 0;
802
+ if (!stored) return null;
803
+ for (const pr of upsert) {
804
+ this.sql.exec(
805
+ `INSERT OR REPLACE INTO pull_requests (repo, number, facts) VALUES (?, ?, ?)`,
806
+ patch.repo,
807
+ pr.number,
808
+ JSON.stringify(pr),
809
+ );
810
+ }
811
+ for (const number of drop) {
812
+ this.sql.exec(`DELETE FROM pull_requests WHERE repo = ? AND number = ?`, patch.repo, number);
813
+ }
814
+ this.sql.exec(
815
+ `INSERT OR REPLACE INTO snapshots (repo, snapshot_at, meta) VALUES (?, ?, ?)`,
816
+ patch.repo,
817
+ patch.snapshotAt,
818
+ JSON.stringify(meta),
819
+ );
820
+ const count = this.sql
821
+ .exec<{ n: number }>(`SELECT COUNT(*) AS n FROM pull_requests WHERE repo = ?`, patch.repo)
822
+ .toArray()[0];
823
+ return count?.n ?? 0;
824
+ });
825
+ }
826
+
827
+ /** The repository's snapshot, pull requests in number order; null when none was stored. */
828
+ async get(repo: string): Promise<DeliverySnapshot | null> {
829
+ const row = this.sql.exec<{ meta: string }>(`SELECT meta FROM snapshots WHERE repo = ?`, repo).toArray()[0];
830
+ if (!row) return null;
831
+ const prs = this.sql
832
+ .exec<{ facts: string }>(`SELECT facts FROM pull_requests WHERE repo = ? ORDER BY number`, repo)
833
+ .toArray()
834
+ .map((r) => JSON.parse(r.facts) as DeliverySnapshot["prs"][number]);
835
+ return { ...(JSON.parse(row.meta) as Omit<DeliverySnapshot, "prs">), prs };
836
+ }
837
+ }
838
+
839
+ const DELIVERY_ROUTES = new Set(["/delivery/get", "/delivery/put", "/delivery/merge"]);
840
+
841
+ /** The pull request whose facts exceed the row fence, if any — checked before the transaction. */
842
+ function oversizedFacts(prs: readonly DeliverySnapshot["prs"][number][]): number | undefined {
843
+ const encoder = new TextEncoder();
844
+ return prs.find((pr) => encoder.encode(JSON.stringify(pr)).byteLength > MAX_PULL_REQUEST_FACTS_BYTES)?.number;
845
+ }
846
+
847
+ async function handleDelivery(pathname: string, body: unknown, env: Env): Promise<Response> {
848
+ const b = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
849
+ const dO = env.DELIVERY.get(env.DELIVERY.idFromName(DELIVERY_OBJECT));
850
+ if (pathname === "/delivery/get") {
851
+ if (typeof b.repo !== "string" || !REPO_SLUG.test(b.repo)) return json({ error: "repo must be owner/name" }, 400);
852
+ const snapshot = await dO.get(b.repo);
853
+ console.log(
854
+ `[delivery/get] ${b.repo} -> ${snapshot ? `${snapshot.prs.length} pull requests as of ${snapshot.snapshotAt}` : "none"}`,
855
+ );
856
+ return json({ snapshot });
857
+ }
858
+ if (pathname === "/delivery/put") {
859
+ if (!isDeliverySnapshot(b.snapshot))
860
+ return json(
861
+ { error: "snapshot must be a DeliverySnapshot (repo, snapshotAt, range, prs[], truncated, completeFrom)" },
862
+ 400,
863
+ );
864
+ const oversized = oversizedFacts(b.snapshot.prs);
865
+ if (oversized !== undefined)
866
+ return json(
867
+ { error: `pull request ${oversized}'s facts must be at most ${MAX_PULL_REQUEST_FACTS_BYTES} bytes` },
868
+ 413,
869
+ );
870
+ const prs = await dO.put(b.snapshot);
871
+ console.log(`[delivery/put] ${b.snapshot.repo} <- ${prs} pull requests as of ${b.snapshot.snapshotAt}`);
872
+ return json({ ok: true, prs });
873
+ }
874
+ if (pathname === "/delivery/merge") {
875
+ if (!isDeliverySnapshotPatch(b.patch))
876
+ return json(
877
+ {
878
+ error:
879
+ "patch must be a DeliverySnapshotPatch (repo, snapshotAt, range, truncated, completeFrom, upsert[], drop[])",
880
+ },
881
+ 400,
882
+ );
883
+ const oversized = oversizedFacts(b.patch.upsert);
884
+ if (oversized !== undefined)
885
+ return json(
886
+ { error: `pull request ${oversized}'s facts must be at most ${MAX_PULL_REQUEST_FACTS_BYTES} bytes` },
887
+ 413,
888
+ );
889
+ const prs = await dO.merge(b.patch);
890
+ if (prs === null) return json({ error: `no snapshot for ${b.patch.repo} to merge into` }, 404);
891
+ console.log(
892
+ `[delivery/merge] ${b.patch.repo} <- ${b.patch.upsert.length} pull requests re-read, ${b.patch.drop.length} dropped, ${prs} stored as of ${b.patch.snapshotAt}`,
893
+ );
894
+ return json({ ok: true, prs });
895
+ }
896
+ return json({ error: "not found" }, 404);
897
+ }
898
+
703
899
  /** Documents are small; a body over this is refused before storage. */
704
900
  const MAX_CONFIG_DOCUMENT_BYTES = 256 * 1024;
705
901
 
@@ -977,6 +1173,102 @@ export class RunHistoryDO extends DurableObject<Env> {
977
1173
  PRIMARY KEY (run_id, kind)
978
1174
  );
979
1175
  `);
1176
+ // The coordinator's parent records (run-history item 49): one row per
1177
+ // instance, written by the bot at the instance's creation and read by the
1178
+ // spawn route for the requester, channel and thread every child acts as.
1179
+ this.sql.exec(`
1180
+ CREATE TABLE IF NOT EXISTS coordinator_instances (
1181
+ instance_id TEXT PRIMARY KEY,
1182
+ json TEXT NOT NULL,
1183
+ created_at INTEGER NOT NULL
1184
+ );
1185
+ `);
1186
+ // The units of the plan an instance runs (run-history item 50): one row per
1187
+ // (instance, unit), replaced whole as the runner reaches the unit; the
1188
+ // rowid keeps the order the rows were first written — the plan's.
1189
+ this.sql.exec(`
1190
+ CREATE TABLE IF NOT EXISTS coordinator_units (
1191
+ instance_id TEXT NOT NULL,
1192
+ unit TEXT NOT NULL,
1193
+ json TEXT NOT NULL,
1194
+ updated_at INTEGER NOT NULL,
1195
+ PRIMARY KEY (instance_id, unit)
1196
+ );
1197
+ `);
1198
+ }
1199
+
1200
+ // ---- the coordinator's parent records (run-history item 49) -----------------
1201
+
1202
+ /** Idempotent for the same record; a different record under a taken id is refused. */
1203
+ async putInstance(instance: CoordinatorInstance): Promise<{ ok: true } | { ok: false; reason: "exists" }> {
1204
+ let out: { ok: true } | { ok: false; reason: "exists" } = { ok: true };
1205
+ this.ctx.storage.transactionSync(() => {
1206
+ const text = JSON.stringify(instance);
1207
+ const existing = this.sql
1208
+ .exec<{ json: string }>(`SELECT json FROM coordinator_instances WHERE instance_id = ?`, instance.id)
1209
+ .toArray()[0];
1210
+ if (existing) {
1211
+ if (existing.json !== text) out = { ok: false, reason: "exists" };
1212
+ return;
1213
+ }
1214
+ this.sql.exec(
1215
+ `INSERT INTO coordinator_instances (instance_id, json, created_at) VALUES (?, ?, ?)`,
1216
+ instance.id,
1217
+ text,
1218
+ instance.createdAt,
1219
+ );
1220
+ });
1221
+ return out;
1222
+ }
1223
+
1224
+ /** The record written over whatever the id holds and the id's unit rows
1225
+ * dropped, in one transaction — an attempt starting over: the leftover of one
1226
+ * whose Workflow instance was never created, once the shim said so. */
1227
+ async replaceInstance(instance: CoordinatorInstance): Promise<{ ok: true }> {
1228
+ this.ctx.storage.transactionSync(() => {
1229
+ this.sql.exec(
1230
+ `INSERT INTO coordinator_instances (instance_id, json, created_at) VALUES (?, ?, ?)
1231
+ ON CONFLICT(instance_id) DO UPDATE SET json = excluded.json, created_at = excluded.created_at`,
1232
+ instance.id,
1233
+ JSON.stringify(instance),
1234
+ instance.createdAt,
1235
+ );
1236
+ this.sql.exec(`DELETE FROM coordinator_units WHERE instance_id = ?`, instance.id);
1237
+ });
1238
+ return { ok: true };
1239
+ }
1240
+
1241
+ async getInstance(id: string): Promise<CoordinatorInstance | null> {
1242
+ const row = this.sql
1243
+ .exec<{ json: string }>(`SELECT json FROM coordinator_instances WHERE instance_id = ?`, id)
1244
+ .toArray()[0];
1245
+ return row ? (JSON.parse(row.json) as CoordinatorInstance) : null;
1246
+ }
1247
+
1248
+ // ---- the units of the plan an instance runs (run-history item 50) -----------
1249
+
1250
+ /** Each row replaced whole under its (instance, unit); a replace keeps the row's place. */
1251
+ async putUnits(units: CoordinatorUnit[], now: number): Promise<{ ok: true }> {
1252
+ this.ctx.storage.transactionSync(() => {
1253
+ for (const u of units) {
1254
+ this.sql.exec(
1255
+ `INSERT INTO coordinator_units (instance_id, unit, json, updated_at) VALUES (?, ?, ?, ?)
1256
+ ON CONFLICT(instance_id, unit) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at`,
1257
+ u.instanceId,
1258
+ u.unit,
1259
+ JSON.stringify(u),
1260
+ now,
1261
+ );
1262
+ }
1263
+ });
1264
+ return { ok: true };
1265
+ }
1266
+
1267
+ async listUnits(instanceId: string): Promise<CoordinatorUnit[]> {
1268
+ return this.sql
1269
+ .exec<{ json: string }>(`SELECT json FROM coordinator_units WHERE instance_id = ? ORDER BY rowid`, instanceId)
1270
+ .toArray()
1271
+ .map((r) => JSON.parse(r.json) as CoordinatorUnit);
980
1272
  }
981
1273
 
982
1274
  // ---- the live-run ledger (run-history items 28–34) --------------------------
@@ -1004,6 +1296,7 @@ export class RunHistoryDO extends DurableObject<Env> {
1004
1296
  agent: existing.meta.agent,
1005
1297
  startedAt: existing.startedAt,
1006
1298
  ownerGen: existing.ownerGen,
1299
+ idempotencyKey: existing.meta.idempotencyKey,
1007
1300
  }
1008
1301
  : undefined,
1009
1302
  req,
@@ -1185,13 +1478,17 @@ export class RunHistoryDO extends DurableObject<Env> {
1185
1478
  return out;
1186
1479
  }
1187
1480
 
1188
- /** The finished record replaces the live rows in ONE transaction (item 33). Fenced. */
1481
+ /** The finished record replaces the live rows in ONE transaction (item 33).
1482
+ * Fenced. Then, for a record carrying `parentInstanceId`, ONE `run
1483
+ * finished:<runId>` to that coordinator instance (item 47) — after the
1484
+ * commit, never inside it, and never able to undo it: a refused send (the
1485
+ * instance ended, no binding) is the answer's `event`, not an error. */
1189
1486
  async finish(
1190
1487
  runId: string,
1191
1488
  gen: string,
1192
1489
  record: RunRecord,
1193
1490
  proposal?: RunPolicyProposal,
1194
- ): Promise<FenceResult & { stored?: boolean }> {
1491
+ ): Promise<FenceResult & { stored?: boolean; event?: RunFinishedSend["kind"] }> {
1195
1492
  let out: FenceResult & { stored?: boolean } = { ok: true };
1196
1493
  this.ctx.storage.transactionSync(() => {
1197
1494
  const fence = checkFence(this.liveRow(runId), gen);
@@ -1203,9 +1500,13 @@ export class RunHistoryDO extends DurableObject<Env> {
1203
1500
  this.deleteLiveRows([runId]);
1204
1501
  out = { ok: true, stored: put.stored };
1205
1502
  });
1503
+ if (!out.ok) return out;
1206
1504
  if ((await this.ctx.storage.getAlarm()) === null)
1207
1505
  await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
1208
- return out;
1506
+ const event = await sendRunFinished(this.env.SHIP_COORDINATOR, record);
1507
+ if (event.kind === "failed")
1508
+ console.warn(`[runs/finish] ${runId} → run finished not delivered to ${event.instance}: ${event.reason}`);
1509
+ return { ...out, event: event.kind };
1209
1510
  }
1210
1511
 
1211
1512
  /** The live rows go with no record (item 42): a reserved run that never
@@ -2157,6 +2458,11 @@ export class RunTranscriptDO extends DurableObject<Env> {
2157
2458
  }
2158
2459
 
2159
2460
  const LEDGER_ROUTES = new Set([
2461
+ "/runs/coordinator/put",
2462
+ "/runs/coordinator/replace",
2463
+ "/runs/coordinator/get",
2464
+ "/runs/coordinator/units/put",
2465
+ "/runs/coordinator/units/list",
2160
2466
  "/runs/claim",
2161
2467
  "/runs/heartbeat",
2162
2468
  "/runs/append",
@@ -2180,6 +2486,17 @@ const LEDGER_ROUTES = new Set([
2180
2486
 
2181
2487
  /** Routes whose bodies may carry a record, a transcript chunk, or an event batch. */
2182
2488
  const WIDE_BODY_ROUTES = new Set(["/runs/put", "/runs/finish", "/runs/append", "/runs/transcript/write"]);
2489
+ /** A delivery snapshot written whole, or a refresh's patch: every merged pull request's reviews and
2490
+ * its branch's workflow runs — about 7 KB a pull request (measured: 291 pull requests, 2.1 MB), so
2491
+ * a first read at the listing cap is under 6 MB and a busy repository's whole window many MB. */
2492
+ const MAX_SNAPSHOT_BODY_BYTES = 16 * 1024 * 1024;
2493
+
2494
+ /** The request body ceiling per route, decided after routing and before the parse. */
2495
+ function bodyFenceFor(pathname: string): number {
2496
+ if (WIDE_BODY_ROUTES.has(pathname)) return MAX_RUN_PUT_BODY_BYTES;
2497
+ if (pathname === "/delivery/put" || pathname === "/delivery/merge") return MAX_SNAPSHOT_BODY_BYTES;
2498
+ return MAX_BODY_BYTES;
2499
+ }
2183
2500
 
2184
2501
  const gen = (v: unknown): Validated<string> =>
2185
2502
  typeof v === "string" && GEN_PATTERN.test(v)
@@ -2209,6 +2526,20 @@ function parseClaim(b: Record<string, unknown>): Validated<ClaimRequest> {
2209
2526
  if (typeof r.startedAt !== "number" || !Number.isFinite(r.startedAt))
2210
2527
  return invalid("run.startedAt must be a number");
2211
2528
  if (typeof r.meta !== "object" || r.meta === null) return invalid("run.meta must be an object");
2529
+ // A coordinator's tag (run-history item 48) is stored at the claim and read
2530
+ // by the finish's send and the refusal a second claim meets: shaped or
2531
+ // refused, and both fields or neither — one alone is no tag.
2532
+ const meta = r.meta as Record<string, unknown>;
2533
+ if ((meta.parentInstanceId === undefined) !== (meta.idempotencyKey === undefined))
2534
+ return invalid("run.meta.parentInstanceId and run.meta.idempotencyKey come together or not at all");
2535
+ if (meta.parentInstanceId !== undefined) {
2536
+ if (typeof meta.parentInstanceId !== "string" || !INSTANCE_ID_PATTERN.test(meta.parentInstanceId))
2537
+ return invalid("run.meta.parentInstanceId must be a Workflow instance id");
2538
+ }
2539
+ if (meta.idempotencyKey !== undefined) {
2540
+ if (typeof meta.idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(meta.idempotencyKey))
2541
+ return invalid("run.meta.idempotencyKey must be <parentInstanceId>:<step>");
2542
+ }
2212
2543
  if (typeof r.system !== "string") return invalid("run.system must be a string");
2213
2544
  if (!Array.isArray(r.tools)) return invalid("run.tools must be an array");
2214
2545
  if (r.card !== undefined && r.card !== null) {
@@ -2359,6 +2690,44 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
2359
2690
  return json(r);
2360
2691
  }
2361
2692
 
2693
+ // The coordinator's parent records (run-history item 49): the record whole,
2694
+ // validated by the shared contract; a read by instance id.
2695
+ if (pathname === "/runs/coordinator/put") {
2696
+ if (!isCoordinatorInstance(b.instance))
2697
+ return json({ error: "instance must be a coordinator instance record" }, 400);
2698
+ const r = await stub.putInstance(b.instance);
2699
+ console.log(`[runs/coordinator/put] ${key.value} ${b.instance.id} → ${r.ok ? "stored" : r.reason}`);
2700
+ return r.ok ? json(r) : json(r, 409);
2701
+ }
2702
+ if (pathname === "/runs/coordinator/replace") {
2703
+ if (!isCoordinatorInstance(b.instance))
2704
+ return json({ error: "instance must be a coordinator instance record" }, 400);
2705
+ const r = await stub.replaceInstance(b.instance);
2706
+ console.log(`[runs/coordinator/replace] ${key.value} ${b.instance.id} → replaced`);
2707
+ return json(r);
2708
+ }
2709
+ if (pathname === "/runs/coordinator/get") {
2710
+ if (typeof b.id !== "string" || !INSTANCE_ID_PATTERN.test(b.id))
2711
+ return json({ error: "id must be a Workflow instance id" }, 400);
2712
+ return json({ instance: await stub.getInstance(b.id) });
2713
+ }
2714
+ // The units of the plan an instance runs (run-history item 50): rows
2715
+ // validated by the shared contract, each replaced whole; a list by instance.
2716
+ if (pathname === "/runs/coordinator/units/put") {
2717
+ if (!Array.isArray(b.units) || b.units.length === 0 || b.units.length > MAX_UNITS_PER_PUT)
2718
+ return json({ error: `units must be a non-empty array of at most ${MAX_UNITS_PER_PUT} unit rows` }, 400);
2719
+ if (!b.units.every(isCoordinatorUnit)) return json({ error: "every unit must be a coordinator unit row" }, 400);
2720
+ const units = b.units as CoordinatorUnit[];
2721
+ const r = await stub.putUnits(units, now);
2722
+ console.log(`[runs/coordinator/units/put] ${key.value} ${units[0]!.instanceId} ${units.length} row(s)`);
2723
+ return json(r);
2724
+ }
2725
+ if (pathname === "/runs/coordinator/units/list") {
2726
+ if (typeof b.instanceId !== "string" || !INSTANCE_ID_PATTERN.test(b.instanceId))
2727
+ return json({ error: "instanceId must be a Workflow instance id" }, 400);
2728
+ return json({ units: await stub.listUnits(b.instanceId) });
2729
+ }
2730
+
2362
2731
  const runId = parseRunId(b.runId);
2363
2732
  if (!runId.ok) return json({ error: runId.error }, 400);
2364
2733
  if (pathname === "/runs/inbox") {
@@ -2422,7 +2791,9 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
2422
2791
  if (!parsed.ok) return json({ error: parsed.error }, 400);
2423
2792
  if (parsed.value.record.id !== runId.value) return json({ error: "record.id must equal runId" }, 400);
2424
2793
  const r = await stub.finish(runId.value, g.value, parsed.value.record, parsed.value.proposal);
2425
- console.log(`[runs/finish] ${key.value} ${runId.value} ok=${r.ok}${r.ok ? ` stored=${r.stored}` : ` ${r.reason}`}`);
2794
+ console.log(
2795
+ `[runs/finish] ${key.value} ${runId.value} ok=${r.ok}${r.ok ? ` stored=${r.stored} event=${r.event}` : ` ${r.reason}`}`,
2796
+ );
2426
2797
  return r.ok ? json(r) : json(r, 409);
2427
2798
  }
2428
2799
  return json({ error: "not found" }, 404);
@@ -2486,6 +2857,7 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
2486
2857
 
2487
2858
  const ROUTES = new Set([
2488
2859
  ...CONFIG_ROUTES,
2860
+ ...DELIVERY_ROUTES,
2489
2861
  "/retrieve",
2490
2862
  "/write",
2491
2863
  "/list",
@@ -2513,7 +2885,7 @@ interface Admission {
2513
2885
  async function handleRequest(request: Request, env: Env, admission: Admission): Promise<Response> {
2514
2886
  const url = new URL(request.url);
2515
2887
  if (url.pathname === "/healthz" && request.method === "GET")
2516
- return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config"] });
2888
+ return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery"] });
2517
2889
  if (!admission.known) return json({ error: "not found" }, 404);
2518
2890
  if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
2519
2891
  if (!admission.authorized) return json({ error: "unauthorized" }, 401);
@@ -2533,8 +2905,9 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
2533
2905
  return json({ error: "body must declare a numeric Content-Length" }, 411);
2534
2906
  }
2535
2907
  // The ledger's bulk routes carry a finished record, a transcript chunk, or
2536
- // an event batch (32 × 64 KiB) and share /runs/put's fence.
2537
- const maxBodyBytes = WIDE_BODY_ROUTES.has(url.pathname) ? MAX_RUN_PUT_BODY_BYTES : MAX_BODY_BYTES;
2908
+ // an event batch (32 × 64 KiB) and share /runs/put's fence; a delivery
2909
+ // snapshot carries a whole window of pull request facts and has its own.
2910
+ const maxBodyBytes = bodyFenceFor(url.pathname);
2538
2911
  if (Number(header) > maxBodyBytes) {
2539
2912
  return json({ error: `body must be at most ${maxBodyBytes} bytes` }, 413);
2540
2913
  }
@@ -2563,6 +2936,7 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
2563
2936
  }
2564
2937
  if (url.pathname.startsWith("/runs/")) return handleRuns(url.pathname, body, env);
2565
2938
  if (url.pathname.startsWith("/config/")) return handleConfig(url.pathname, body, env);
2939
+ if (url.pathname.startsWith("/delivery/")) return handleDelivery(url.pathname, body, env);
2566
2940
 
2567
2941
  if (url.pathname === "/retrieve") {
2568
2942
  const parsed = parseRetrieve(body);
@@ -31,9 +31,28 @@
31
31
  // transcript a resumed run continues from (docs/reference/specs/run-history.md item
32
32
  // 32). Kept apart from the history object so transcript bytes never queue
33
33
  // behind an admission decision; cleared at finish.
34
- { "name": "RUN_TRANSCRIPTS", "class_name": "RunTranscriptDO" }
34
+ { "name": "RUN_TRANSCRIPTS", "class_name": "RunTranscriptDO" },
35
+ // ONE DeliveryDO (named "delivery"): the delivery page's snapshot per
36
+ // repository — the merged pull requests' facts over the window, one row
37
+ // each, and when they were read (docs/reference/specs/delivery.md item 10).
38
+ { "name": "DELIVERY", "class_name": "DeliveryDO" }
35
39
  ]
36
40
  },
41
+ // The bot shim's ship coordinator Workflow, bound across scripts by the bot's
42
+ // script name (docs/reference/specs/run-history.md item 47): `RunHistoryDO.finish`
43
+ // sends it `run finished:<runId>` for a record carrying `parentInstanceId`.
44
+ // The name is the bot's own Workflow name (`<bot script>-ship-coordinator`,
45
+ // deploy/cloudflare/wrangler.template.jsonc). The class is deployed with the
46
+ // bot, which deploys after this Worker — so this binding lands one release
47
+ // after the class did, never in the same one.
48
+ "workflows": [
49
+ {
50
+ "name": "{{bot.script}}-ship-coordinator",
51
+ "binding": "SHIP_COORDINATOR",
52
+ "class_name": "ShipCoordinator",
53
+ "script_name": "{{bot.script}}"
54
+ }
55
+ ],
37
56
  // SQLite-backed DOs (durable across restarts — AGENTS.md invariant 6).
38
57
  "migrations": [
39
58
  { "tag": "v1", "new_sqlite_classes": ["MemoryDO"] },
@@ -45,6 +64,7 @@
45
64
  // The friction ledger is read from run history (RunHistoryDO carries every
46
65
  // run's diagnosis); the FrictionDO that once mirrored those diagnoses is
47
66
  // retired with its rows — docs/reference/specs/self-improvement.md item 1.
48
- { "tag": "v7", "deleted_classes": ["FrictionDO"] }
67
+ { "tag": "v7", "deleted_classes": ["FrictionDO"] },
68
+ { "tag": "v8", "new_sqlite_classes": ["DeliveryDO"] }
49
69
  ]
50
70
  }
@@ -1,6 +1,16 @@
1
1
  # Resident container image: the @cloudflare/sandbox base (Ubuntu 22.04; already
2
- # ships git, curl, jq, Node 24, npm, bun, cloudflared) plus GitHub tooling and
3
- # a pool of unprivileged users.
2
+ # ships git, curl, jq, npm, bun, cloudflared and a Node 22 this image replaces
3
+ # with Node 24 below) plus GitHub tooling and a pool of unprivileged users.
4
+
5
+ # The Node this image ships (copied in below), at the exact tag every image in
6
+ # this repository shares — its major is .nvmrc's, which is what CI runs and
7
+ # the install script requires. `--platform`: the cloudflare/sandbox base below
8
+ # publishes linux/amd64 only, so the stage the binary is copied from must be
9
+ # amd64 too — a bare FROM on an arm64 host resolves it to arm64 and the copied
10
+ # binary cannot run in the base (check:image on Apple silicon failed exactly
11
+ # so). src/deploy/imageNode.test.ts holds the tag, its major and the platform.
12
+ FROM --platform=linux/amd64 docker.io/library/node:24.21.0-slim AS node
13
+
4
14
  # The tag MUST match the @cloudflare/sandbox version in package.json exactly
5
15
  # (no 'latest' tag exists on the @next line) — bump both together.
6
16
  FROM docker.io/cloudflare/sandbox:0.13.0-next.751.1
@@ -32,7 +42,29 @@ ENV CI=1 \
32
42
  UV_THREADPOOL_SIZE=4 \
33
43
  NODE_OPTIONS=--max-old-space-size=1536
34
44
 
35
- # pnpm + yarn: the base ships Node 24 + npm + bun but neither pnpm nor yarn, so a
45
+ # Node 24, not the base's. The 0.13.0-next base ships Node 22.23.2 like the
46
+ # 0.12.9 one (copied from node:22-slim: the binary at /usr/local/bin/node, npm
47
+ # and corepack under /usr/local/lib/node_modules, npm/npx symlinked into it),
48
+ # while this repository builds and tests on Node 24 (.nvmrc, CI) and its
49
+ # published CLI requires `>=24` — so a repository validated here read a
50
+ # different `verify` than CI did. Node comes from the official image at an
51
+ # exact tag, in the `node` stage above, the same way the sandbox image does it
52
+ # (deploy/cloudflare-sandbox/Dockerfile): the base's npm tree removed first,
53
+ # then the binary, npm's tree and the headers at the base's own paths, so its
54
+ # npm/npx symlinks keep resolving; the container server is a bun-compiled
55
+ # binary and does not run on this Node. The grep fails the BUILD if a bump
56
+ # lands another version; src/deploy/imageNode.test.ts holds it equal to the
57
+ # stage's tag. This layer sits before the package managers below so they are
58
+ # installed by this Node's npm, into this tree.
59
+ RUN rm -rf /usr/local/lib/node_modules /usr/local/include/node
60
+ COPY --from=node /usr/local/bin/node /usr/local/bin/node
61
+ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
62
+ COPY --from=node /usr/local/include/node /usr/local/include/node
63
+ RUN node --version | grep -qx 'v24.21.0' \
64
+ && npm --version >/dev/null \
65
+ && npx --version >/dev/null
66
+
67
+ # pnpm + yarn: the base ships npm + bun but neither pnpm nor yarn, so a
36
68
  # repo whose onboard table is detected as pnpm or yarn (resident-repos item 52 —
37
69
  # every manager `detectCommands` can emit must exist here, or the table fails
38
70
  # deterministically at provision) couldn't be provisioned (install → build).
@@ -74,6 +106,53 @@ RUN apt-get update \
74
106
  && rm -rf /var/lib/apt/lists/* \
75
107
  && command -v unsquashfs >/dev/null
76
108
 
109
+ # The toolchain for what a run BUILDS and LOOKS AT — the same layer as the
110
+ # sandbox image's (deploy/cloudflare-sandbox/Dockerfile);
111
+ # src/deploy/imageToolchain.test.ts holds the two to one shape.
112
+ # - python3 + make + g++: node-gyp's needs. A cold `npm install` in a repo
113
+ # with a native module (node-pty) rebuilds it from source, and died on
114
+ # "no Python". Just the three — no build-essential, no recommends.
115
+ # - ffmpeg (Ubuntu's, with libx264): an agent pulls frames out of a video
116
+ # to look at it (`ffmpeg -i in.mp4 -vf fps=1 frame_%03d.png`) and encodes
117
+ # video from frames or a screen recording.
118
+ # - a headless Chromium through Playwright at an EXACT pin
119
+ # (src/deploy/imagePins.test.ts): screenshots, PDFs and `recordVideo`.
120
+ # Ubuntu 22.04's apt `chromium` is a snap stub that does not run in a
121
+ # container, so the browser is Playwright's own build — the headless shell
122
+ # only (`--only-shell`: there is no display), with the system libraries it
123
+ # needs (`--with-deps`) and the fonts pages render text with.
124
+ # PLAYWRIGHT_BROWSERS_PATH is set BEFORE the install so the browser lands
125
+ # under /opt, not root's home, and the tree is opened to every user
126
+ # (a+rX): every agent command here runs as an unprivileged workerN (the
127
+ # pool below), through `su` without `-`, which keeps this environment.
128
+ # NODE_PATH makes `require('playwright')` resolve from any working
129
+ # directory. Playwright launches Chromium with --no-sandbox
130
+ # (`chromiumSandbox: false`, its default) — what a process without user
131
+ # namespaces needs, root or not.
132
+ # Every claim is PROVEN by the layer itself, so the BUILD fails, not a run:
133
+ # the compilers answer --version, ffmpeg encodes a one-second testsrc clip to
134
+ # h264 and decodes a non-empty frame back out of it, the playwright CLI is on
135
+ # PATH at the pin, and a real headless screenshot of a page lands as a
136
+ # non-empty png — here as root; again as worker1 once the pool exists, below.
137
+ # The proof artefacts, the apt lists, apt's .deb archive (this base has no
138
+ # docker-clean hook: 400 MB of archives stayed behind without the `clean`)
139
+ # and npm's cache go in the same layer.
140
+ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright \
141
+ NODE_PATH=/usr/local/lib/node_modules
142
+ RUN apt-get update \
143
+ && apt-get install -y --no-install-recommends python3 make g++ ffmpeg fonts-liberation fonts-noto-color-emoji \
144
+ && npm install -g playwright@1.63.0 \
145
+ && playwright install --with-deps --only-shell chromium \
146
+ && chmod -R a+rX /opt/ms-playwright \
147
+ && apt-get clean && rm -rf /var/lib/apt/lists/* && npm cache clean --force \
148
+ && python3 --version && g++ --version && make --version \
149
+ && ffmpeg -version \
150
+ && ffmpeg -hide_banner -loglevel error -f lavfi -i testsrc=duration=1:size=320x240:rate=10 -c:v libx264 -pix_fmt yuv420p /tmp/proof.mp4 \
151
+ && ffmpeg -hide_banner -loglevel error -i /tmp/proof.mp4 -vf fps=1 /tmp/proof_%03d.png && test -s /tmp/proof_001.png \
152
+ && playwright --version | grep -qx 'Version 1.63.0' \
153
+ && playwright screenshot --viewport-size=640,480 'data:text/html,<h1>ok</h1>' /tmp/ok.png && test -s /tmp/ok.png \
154
+ && rm -f /tmp/proof.mp4 /tmp/proof_*.png /tmp/ok.png
155
+
77
156
  # Agent commands never run as root (repo code runs unprivileged, one user per
78
157
  # thread — docs/reference/specs/resident-repos.md). The SDK's exec has no user/uid option
79
158
  # (verified against 0.13.0-next.751.1), so the container server itself runs as
@@ -89,3 +168,10 @@ RUN set -eux; \
89
168
  for i in $(seq 1 17); do useradd -m -u "$((2000 + i))" -s /bin/bash "worker$i"; chmod 700 "/home/worker$i"; done; \
90
169
  if dpkg -s sudo >/dev/null 2>&1; then apt-get remove -y --purge sudo; fi; \
91
170
  passwd -l root
171
+
172
+ # The browser proof once more, as worker1 — exactly how a thread's commands
173
+ # run (`su -s /bin/bash workerN -c …`, root's environment kept). The layer
174
+ # above proved it as root; this one proves an unprivileged user can read the
175
+ # browser tree under /opt and launch it, so the BUILD fails if a chmod or the
176
+ # path ever regresses, not a run.
177
+ RUN su -s /bin/bash worker1 -c "playwright screenshot --viewport-size=640,480 'data:text/html,<h1>ok</h1>' /tmp/ok.png && test -s /tmp/ok.png && rm -f /tmp/ok.png"