elanous 0.2.31 → 0.2.32

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 (282) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
  54. package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/ceo/index.html +1 -1
  66. package/apps/pwa/out/ceo/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/decisions/index.html +1 -1
  74. package/apps/pwa/out/decisions/index.txt +14 -14
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +14 -14
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +14 -14
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +14 -14
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +14 -14
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +14 -14
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +14 -14
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +14 -14
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +17 -14
  92. package/apps/pwa/out/live-run/index.html +1 -1
  93. package/apps/pwa/out/live-run/index.txt +14 -14
  94. package/apps/pwa/out/loops/index.html +1 -1
  95. package/apps/pwa/out/loops/index.txt +14 -14
  96. package/apps/pwa/out/market/index.html +1 -1
  97. package/apps/pwa/out/market/index.txt +14 -14
  98. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  99. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  100. package/apps/pwa/out/morning/index.html +1 -1
  101. package/apps/pwa/out/morning/index.txt +14 -14
  102. package/apps/pwa/out/observatory/index.html +1 -1
  103. package/apps/pwa/out/observatory/index.txt +14 -14
  104. package/apps/pwa/out/ops/checklist/index.html +1 -1
  105. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  106. package/apps/pwa/out/ops/release/index.html +1 -1
  107. package/apps/pwa/out/ops/release/index.txt +14 -14
  108. package/apps/pwa/out/ops/seats/index.html +1 -1
  109. package/apps/pwa/out/ops/seats/index.txt +14 -14
  110. package/apps/pwa/out/outputs/index.html +1 -1
  111. package/apps/pwa/out/outputs/index.txt +14 -14
  112. package/apps/pwa/out/reflection/index.html +1 -1
  113. package/apps/pwa/out/reflection/index.txt +15 -15
  114. package/apps/pwa/out/scheduler/index.html +1 -1
  115. package/apps/pwa/out/scheduler/index.txt +14 -14
  116. package/apps/pwa/out/sessions/index.html +1 -1
  117. package/apps/pwa/out/sessions/index.txt +14 -14
  118. package/apps/pwa/out/settings/devices/index.html +1 -1
  119. package/apps/pwa/out/settings/devices/index.txt +14 -14
  120. package/apps/pwa/out/settings/index.html +1 -1
  121. package/apps/pwa/out/settings/index.txt +14 -14
  122. package/apps/pwa/out/setup/done/index.html +1 -1
  123. package/apps/pwa/out/setup/done/index.txt +14 -14
  124. package/apps/pwa/out/setup/index.html +1 -1
  125. package/apps/pwa/out/setup/index.txt +14 -14
  126. package/apps/pwa/out/share/index.html +1 -1
  127. package/apps/pwa/out/share/index.txt +14 -14
  128. package/apps/pwa/out/showroom/index.html +1 -1
  129. package/apps/pwa/out/showroom/index.txt +14 -14
  130. package/apps/pwa/out/tasks/index.html +1 -1
  131. package/apps/pwa/out/tasks/index.txt +14 -14
  132. package/apps/pwa/out/term/index.html +1 -1
  133. package/apps/pwa/out/term/index.txt +14 -14
  134. package/apps/pwa/out/today/index.html +1 -1
  135. package/apps/pwa/out/today/index.txt +14 -14
  136. package/apps/pwa/out/trace/index.html +1 -1
  137. package/apps/pwa/out/trace/index.txt +14 -14
  138. package/apps/pwa/out/vault/index.html +1 -1
  139. package/apps/pwa/out/vault/index.txt +14 -14
  140. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  141. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  142. package/apps/pwa/out/workflows/index.html +1 -1
  143. package/apps/pwa/out/workflows/index.txt +14 -14
  144. package/apps/pwa/out/workspace/index.html +1 -1
  145. package/apps/pwa/out/workspace/index.txt +14 -14
  146. package/apps/pwa/out/worktrees/index.html +1 -1
  147. package/apps/pwa/out/worktrees/index.txt +14 -14
  148. package/graphs/implement-loop.yaml +5 -0
  149. package/package.json +1 -1
  150. package/scripts/ci-typecheck-changed.ts +26 -1
  151. package/scripts/measure-si-task-cpu.ts +143 -0
  152. package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
  153. package/scripts/release-loop/prepare-node.ts +102 -4
  154. package/src/acp/core-turn-bridge.ts +16 -4
  155. package/src/acp/elanous-extensions.ts +3 -0
  156. package/src/acp/server.ts +3 -0
  157. package/src/acp/tool-trace.ts +186 -11
  158. package/src/agent/self-ambient-cache.ts +156 -0
  159. package/src/agent/self-ambient.ts +8 -1
  160. package/src/agent/task-notification.ts +5 -0
  161. package/src/agent-substrate/pr-reviewer.ts +3 -1
  162. package/src/ask-user-question/tool.ts +22 -0
  163. package/src/autopilot/build/llm-conflict-merge.ts +56 -10
  164. package/src/autopilot/pr-manager.ts +23 -5
  165. package/src/capture/frame-state-detect.ts +39 -0
  166. package/src/capture/tui-self-report.ts +134 -6
  167. package/src/chat/index.ts +61 -15
  168. package/src/chat/pickers/modal-runtime.ts +6 -0
  169. package/src/chat/pickers/modals.ts +8 -2
  170. package/src/cli/ask-bare.ts +1 -0
  171. package/src/cli/import-cli.ts +12 -0
  172. package/src/cli/log-event-names.ts +10 -0
  173. package/src/cli/msg-cli.ts +1 -0
  174. package/src/cli/pod-cli.ts +14 -3
  175. package/src/cli/pr-cli.ts +67 -32
  176. package/src/cli/pr-land-base-read.ts +34 -0
  177. package/src/cli/pr-land-heavy-remote.ts +81 -25
  178. package/src/cli/pr-land-overlap-evidence.ts +46 -2
  179. package/src/cli/pr-land-pod-shadow-job.ts +26 -3
  180. package/src/cli/pr-land-refusal.ts +20 -5
  181. package/src/cli/pty-takeover-cli.ts +88 -14
  182. package/src/cli/sim-cli.ts +18 -0
  183. package/src/context-bus/ops-events.ts +6 -2
  184. package/src/core-turn/run-core-turn.ts +1 -0
  185. package/src/core-turn/run-goal-loop.ts +21 -0
  186. package/src/core-turn/types.ts +3 -0
  187. package/src/dashboard/acp-active-model.ts +42 -1
  188. package/src/dashboard/background-jobs-status.ts +66 -0
  189. package/src/dashboard/index.ts +117 -44
  190. package/src/dashboard/input/agent-wake.ts +242 -0
  191. package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
  192. package/src/dashboard/input/external-input-host.ts +23 -9
  193. package/src/dashboard/input/mouse-wiring.ts +108 -11
  194. package/src/dashboard/log-click-runtime.ts +118 -2
  195. package/src/dashboard/turn-preamble.ts +116 -40
  196. package/src/dashboard/turn-session-layers.ts +4 -2
  197. package/src/dashboard/turn-start-segments.ts +222 -0
  198. package/src/display/prompt-row-owner.ts +146 -0
  199. package/src/display/resize-listener.ts +33 -2
  200. package/src/fold-stack.ts +4 -2
  201. package/src/git-fs/index.ts +263 -36
  202. package/src/harness/gate-reserve.ts +87 -0
  203. package/src/harness/harness-incidents.ts +13 -0
  204. package/src/harness/harness-queue.ts +15 -0
  205. package/src/index.ts +8 -6
  206. package/src/input/external-input-queue.ts +34 -9
  207. package/src/llm/route-decision.ts +36 -0
  208. package/src/llm/tool-loop-steered-rejection.ts +8 -0
  209. package/src/llm-request-shape.ts +209 -0
  210. package/src/llm.ts +166 -267
  211. package/src/loops/orchestrator/tick.ts +24 -2
  212. package/src/maturity/feature-maturity.ts +1 -1
  213. package/src/mss/logging/render-categories.ts +5 -0
  214. package/src/ops/overview.ts +59 -2
  215. package/src/pr-tool/census.ts +19 -4
  216. package/src/pr-tool/inflight-registry.ts +143 -0
  217. package/src/pty-shell/pty-manifest.ts +12 -3
  218. package/src/pty-shell/tui-run-state.ts +34 -3
  219. package/src/seat-loop/traffic-signal.ts +2 -1
  220. package/src/self-dev/ask-launch-flow.ts +13 -0
  221. package/src/self-dev/draft-residue.ts +243 -24
  222. package/src/self-dev/shard-plan.ts +11 -3
  223. package/src/self-dev/shard-run.ts +103 -10
  224. package/src/self-implement/elanous-tui-spawn.ts +14 -0
  225. package/src/self-implement/follow-up-placement.ts +79 -0
  226. package/src/self-implement/gate-artifacts.ts +15 -4
  227. package/src/self-implement/gate-baseline.ts +96 -5
  228. package/src/self-implement/gate-cli.ts +7 -2
  229. package/src/self-implement/gate-remote.ts +26 -1
  230. package/src/self-implement/host-regate.ts +36 -4
  231. package/src/self-implement/orchestrator.ts +101 -11
  232. package/src/self-implement/run-ledger.ts +16 -14
  233. package/src/session-runtime/index.ts +22 -267
  234. package/src/shell-runner/background-surface.ts +15 -5
  235. package/src/skills/tools/agent.ts +39 -6
  236. package/src/skills/tools/list-dir.ts +11 -0
  237. package/src/skills/tools/schedule-wakeup.ts +408 -0
  238. package/src/skills/tools/shell.ts +245 -3
  239. package/src/status/bar.ts +26 -0
  240. package/src/steward/digest-delivery.ts +118 -0
  241. package/src/steward/hold-hygiene.ts +120 -0
  242. package/src/steward/triage.ts +10 -0
  243. package/src/surface/shell-runner-boot.ts +5 -2
  244. package/src/task-agent/task-hand.ts +24 -0
  245. package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
  246. package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
  247. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
  248. package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
  249. package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
  250. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
  251. package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
  252. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
  253. package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
  254. package/src/tui/chat/execution-badge.ts +5 -1
  255. package/src/tui/input-lag-probe.ts +230 -0
  256. package/src/tui.ts +4 -0
  257. package/src/ux-sim/scenario.ts +255 -0
  258. package/src/version/packed-revision.json +1 -1
  259. package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
  280. package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
  281. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
  282. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
@@ -0,0 +1,525 @@
1
+ /** Opt-in warm Pod transport. It never creates a Pod or changes the gate's Job verdict. */
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { closeSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync,
5
+ readFileSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { isAbsolute, join, normalize, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import type { Kubectl } from './self-implement-pod.js';
10
+ import { elanousStateRoot } from '../../autopilot/state-paths.js';
11
+ import { parseWarmWorkerReply, type WarmWorkerCommand, type WarmWorkerKey, type WarmWorkerReply } from './pod-warm-worker-protocol.js';
12
+ import type { WarmWorkerOutcome } from './pod-warm-worker-runtime.js';
13
+
14
+ const SHA = /^[a-f0-9]{40}$/;
15
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
16
+ const DNS = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
17
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
18
+ const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex');
19
+ const pendingByKubectl = new WeakMap<Kubectl, Set<string>>();
20
+ const equalKey = (a: WarmWorkerKey, b: WarmWorkerKey) => a.context === b.context
21
+ && a.imageDigest === b.imageDigest && a.lockHash === b.lockHash && a.architecture === b.architecture;
22
+
23
+ /** A test process can exceed the generic 120 s kubectl timeout; the worker itself enforces deadlineAt. */
24
+ const warmKubectl: Kubectl = (args, input) => {
25
+ const env = { ...process.env };
26
+ for (const name of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy']) delete env[name];
27
+ const result = spawnSync('kubectl', [...args], { encoding: 'utf8', input, env,
28
+ timeout: 1_800_000, maxBuffer: 8 * 1024 * 1024 });
29
+ return { status: result.status, stdout: result.stdout ?? '', stderr: (result.stderr ?? '') + (result.error ? String(result.error) : '') };
30
+ };
31
+
32
+ export interface WarmPodSpec {
33
+ name: string;
34
+ namespace: string;
35
+ /** The kubectl context is explicit; the worker key context is a separate pool identity. */
36
+ kubectlContext: string;
37
+ workerId: string;
38
+ image: string;
39
+ /** Public checkout URL, or use the explicit node-local mirror below for private source. */
40
+ sourceUrl?: string;
41
+ /** Existing readable git mirror on a pinned node. Mounted read-only into the init container only. */
42
+ sourceMirrorHostPath?: string;
43
+ nodeName?: string;
44
+ cutSha: string;
45
+ key: WarmWorkerKey;
46
+ /** Caller resolves the live host daemon endpoint; no guessed port. */
47
+ hostProbeUrl: string;
48
+ /** Cluster service endpoint to prove production network isolation. */
49
+ prodProbeUrl: string;
50
+ }
51
+
52
+ /** Same fail-closed host/prod reachability condition as command Jobs, before prep and every request. */
53
+ export const WARM_POD_ISOLATION_PROBE = `async function warmIsolationEnforced() {
54
+ const targets = [process.env.WARM_HOST_PROBE_URL, process.env.WARM_PROD_PROBE_URL];
55
+ if (targets.some(url => !url)) throw new Error('warm Pod isolation probe URLs missing');
56
+ let blocked = 0;
57
+ for (let probe = 0; probe < 60; probe++) {
58
+ const reachable = await Promise.all(targets.map(async url => {
59
+ try { await fetch(url, { signal: AbortSignal.timeout(1000) }); return true; }
60
+ catch { return false; }
61
+ }));
62
+ blocked = reachable.some(Boolean) ? 0 : blocked + 1;
63
+ if (blocked >= 3) return true;
64
+ await Bun.sleep(500);
65
+ }
66
+ return false;
67
+ }`;
68
+ export const WARM_POD_ISOLATION = `${WARM_POD_ISOLATION_PROBE}
69
+ if (!(await warmIsolationEnforced())) throw new Error('warm Pod isolation not enforced');`;
70
+
71
+ /** Clone and verify source while the private mirror is mounted only here. */
72
+ export const WARM_POD_PREPARE = `import { spawnSync } from 'node:child_process';
73
+ import { createHash } from 'node:crypto';
74
+ import { readFileSync } from 'node:fs';
75
+ const checked = (argv) => { const r = spawnSync(argv[0], argv.slice(1), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); if (r.status !== 0) throw new Error(argv[0] + ': ' + r.stderr); return r.stdout.trim(); };
76
+ checked(['git', 'clone', '--quiet', '--no-checkout', process.env.WARM_SOURCE_URL, '/workspace/repo']);
77
+ checked(['git', '-C', '/workspace/repo', 'checkout', '--detach', '--quiet', process.env.WARM_CUT_SHA]);
78
+ if (checked(['git', '-C', '/workspace/repo', 'rev-parse', 'HEAD']) !== process.env.WARM_CUT_SHA) throw new Error('warm source SHA mismatch');
79
+ const lock = createHash('sha256').update(readFileSync('/workspace/repo/bun.lock')).digest('hex');
80
+ if (lock !== process.env.WARM_LOCK_HASH) throw new Error('warm lock mismatch');
81
+ checked(['git', '-C', '/workspace/repo', 'remote', 'remove', 'origin']);`;
82
+
83
+ /** Install after the checkout init container and its private mirror mount have exited. */
84
+ export const WARM_POD_INSTALL = `import { spawnSync } from 'node:child_process';
85
+ const checked = (argv) => { const r = spawnSync(argv[0], argv.slice(1), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); if (r.status !== 0) throw new Error(argv[0] + ': ' + r.stderr); return r.stdout.trim(); };
86
+ checked(['bun', 'install', '--frozen-lockfile', '--cwd', '/workspace/repo']);
87
+ // The test subprocess runs as uid 65534; it must traverse the read-only checkout and node_modules.
88
+ checked(['chmod', '-R', 'a+rX', '/workspace/repo']);`;
89
+
90
+ /** Each exec starts a short-lived Bun process; the Pod, checkout and installed dependencies remain warm. */
91
+ export const WARM_POD_EXEC = `import { createHash } from 'node:crypto';
92
+ import { spawnSync } from 'node:child_process';
93
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
94
+ import { pathToFileURL } from 'node:url';
95
+ ${WARM_POD_ISOLATION_PROBE}
96
+ const command = JSON.parse(await Bun.stdin.text());
97
+ if (command.workerId !== process.env.WARM_WORKER_ID || command.cutSha !== process.env.WARM_CUT_SHA
98
+ || command.key?.context !== process.env.WARM_CONTEXT
99
+ || command.key?.imageDigest !== process.env.WARM_IMAGE_DIGEST
100
+ || command.key?.lockHash !== process.env.WARM_LOCK_HASH
101
+ || command.key?.architecture !== process.env.WARM_ARCHITECTURE)
102
+ throw new Error('warm command differs from Pod identity');
103
+ chmodSync('/workspace/data', 0o755);
104
+ if (statSync('/workspace/data').uid !== Number(process.env.WARM_BRIDGE_UID)) throw new Error('warm data owner changed');
105
+ mkdirSync('/workspace/data/scratch', { recursive: true, mode: 0o755 });
106
+ chmodSync('/workspace/data/scratch', 0o755);
107
+ mkdirSync('/workspace/data/artifacts', { recursive: true, mode: 0o700 });
108
+ chmodSync('/workspace/data/artifacts', 0o700);
109
+ const control = '/workspace/data/control';
110
+ mkdirSync(control, { recursive: true, mode: 0o700 });
111
+ const controlStat = statSync(control);
112
+ if (controlStat.uid !== Number(process.env.WARM_BRIDGE_UID) || (controlStat.mode & 0o077) !== 0) throw new Error('warm control is not private');
113
+ const active = control + '/request-active';
114
+ const quarantine = control + '/quarantine';
115
+ if (existsSync(quarantine) || existsSync(active)) {
116
+ process.stdout.write(JSON.stringify({ reply: null, quarantineWorker: true, reason: 'quarantined' }));
117
+ process.exit(0);
118
+ }
119
+ try { mkdirSync(active, { mode: 0o700 }); }
120
+ catch {
121
+ process.stdout.write(JSON.stringify({ reply: null, quarantineWorker: true, reason: 'quarantined' }));
122
+ process.exit(0);
123
+ }
124
+ try {
125
+ if (!(await warmIsolationEnforced())) throw new Error('warm Pod isolation changed');
126
+ const gitEnv = { ...process.env };
127
+ for (const name of Object.keys(gitEnv)) if (name.startsWith('GIT_')) delete gitEnv[name];
128
+ const git = (args, maxBuffer = 1024 * 1024) => { const result = spawnSync('git', ['-C', '/workspace/repo', ...args],
129
+ { encoding: 'utf8', env: gitEnv, maxBuffer });
130
+ if (result.status !== 0) throw new Error('warm source verification failed'); return result.stdout.trim(); };
131
+ const tracked = git(['ls-files', '-v', '-z'], 16 * 1024 * 1024).split('\\0').filter(Boolean);
132
+ if (realpathSync(git(['rev-parse', '--show-toplevel'])) !== realpathSync('/workspace/repo')
133
+ || git(['rev-parse', 'HEAD']) !== process.env.WARM_CUT_SHA
134
+ || git(['status', '--porcelain', '--untracked-files=no'])
135
+ || tracked.some(entry => !entry.startsWith('H ')))
136
+ throw new Error('warm source changed');
137
+ if (createHash('sha256').update(readFileSync('/workspace/repo/bun.lock')).digest('hex') !== process.env.WARM_LOCK_HASH)
138
+ throw new Error('warm lock changed');
139
+ const { WarmWorkerRuntime } = await import(pathToFileURL('/workspace/repo/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts').href);
140
+ const { runWarmWorkerFile } = await import(pathToFileURL('/workspace/repo/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts').href);
141
+ const runtime = new WarmWorkerRuntime({ repoDir: '/workspace/repo', scratchDir: '/workspace/data/scratch', artifactsRoot: '/workspace/data/artifacts', workerId: process.env.WARM_WORKER_ID,
142
+ runFile: runWarmWorkerFile });
143
+ const result = await runtime.handle(command);
144
+ const names = ['log', 'junit.xml', 'json', 'warm-manifest.json', 'rc'];
145
+ const artifactHashes = result.reply ? result.reply.files.map((_, index) => Object.fromEntries(names.map(name => [name, createHash('sha256').update(readFileSync(result.artifactsDir + '/file-' + index + '.' + name)).digest('hex')]))) : undefined;
146
+ if (result.quarantineWorker || result.reason !== 'complete') writeFileSync(quarantine, result.reason, { mode: 0o600 });
147
+ else writeFileSync(active + '/identity', JSON.stringify({ runId: command.runId, attemptId: command.attemptId,
148
+ leaseId: command.leaseId, generation: command.generation }), { mode: 0o600 });
149
+ process.stdout.write(JSON.stringify({ ...result, artifactHashes }));
150
+ } catch (error) {
151
+ writeFileSync(quarantine, String(error), { mode: 0o600 });
152
+ throw error;
153
+ }`;
154
+
155
+ /** The Pod stays fenced until the host has validated all artifact bytes and acknowledges this identity. */
156
+ export const WARM_POD_ACK = `import { existsSync, readFileSync, rmSync } from 'node:fs';
157
+ const active = '/workspace/data/control/request-active';
158
+ if (existsSync('/workspace/data/control/quarantine')) throw new Error('worker quarantined');
159
+ const expected = JSON.parse(await Bun.stdin.text());
160
+ const actual = JSON.parse(readFileSync(active + '/identity', 'utf8'));
161
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error('warm request identity changed');
162
+ rmSync(active, { recursive: true });
163
+ process.stdout.write('ack');`;
164
+
165
+ /** Artifact bytes travel as ASCII so kubectl's UTF-8 decoding cannot change their hashes. */
166
+ export const WARM_POD_READ = `import { readFileSync } from 'node:fs';
167
+ process.stdout.write(readFileSync(process.argv.at(-1)).toString('base64'));`;
168
+
169
+ function validateSpec(spec: WarmPodSpec): void {
170
+ const publicHttpsSource = (value: string): boolean => {
171
+ try { const url = new URL(value); return url.protocol === 'https:' && !!url.hostname
172
+ && !url.username && !url.password && !url.search && !url.hash; }
173
+ catch { return false; }
174
+ };
175
+ const probeValid = (value: string, host: string) => {
176
+ try { const url = new URL(value); return url.protocol === 'http:' && url.hostname === host
177
+ && !!url.port && !url.username && !url.password && !url.hash; }
178
+ catch { return false; }
179
+ };
180
+ if (!DNS.test(spec.name) || spec.name.length > 63 || !DNS.test(spec.namespace) || !spec.kubectlContext
181
+ || !spec.workerId || !SHA.test(spec.cutSha) || !DIGEST.test(spec.key.imageDigest)
182
+ || !/^[a-f0-9]{64}$/.test(spec.key.lockHash) || !spec.key.context || !spec.key.architecture
183
+ || !spec.image.endsWith(`@${spec.key.imageDigest}`)
184
+ || (spec.sourceUrl === undefined) === (spec.sourceMirrorHostPath === undefined)
185
+ || (spec.sourceUrl !== undefined && !publicHttpsSource(spec.sourceUrl))
186
+ || (spec.sourceMirrorHostPath !== undefined && (!isAbsolute(spec.sourceMirrorHostPath)
187
+ || normalize(spec.sourceMirrorHostPath) !== spec.sourceMirrorHostPath || !spec.nodeName))
188
+ || !probeValid(spec.hostProbeUrl, 'host.orb.internal')
189
+ || !probeValid(spec.prodProbeUrl, 'core.elanous-prod')
190
+ || (spec.nodeName !== undefined && !DNS.test(spec.nodeName.replace(/\./g, '-')))) throw new Error('invalid warm Pod spec');
191
+ }
192
+
193
+ /** The caller may apply this manifest only through an explicit canary flow. This function has no side effects. */
194
+ export function warmWorkerPodManifest(spec: WarmPodSpec): Record<string, unknown> {
195
+ validateSpec(spec);
196
+ const annotations = {
197
+ 'elanous.dev/warm-cut-sha': spec.cutSha,
198
+ 'elanous.dev/warm-lock-hash': spec.key.lockHash,
199
+ 'elanous.dev/warm-image-digest': spec.key.imageDigest,
200
+ 'elanous.dev/warm-worker-id': spec.workerId,
201
+ 'elanous.dev/warm-context': spec.key.context,
202
+ 'elanous.dev/warm-architecture': spec.key.architecture,
203
+ };
204
+ const mirror = spec.sourceMirrorHostPath;
205
+ return {
206
+ apiVersion: 'v1', kind: 'Pod', metadata: { name: spec.name, namespace: spec.namespace,
207
+ labels: { 'elanous.kind': 'warm-worker' }, annotations },
208
+ spec: { restartPolicy: 'Never', automountServiceAccountToken: false,
209
+ nodeSelector: { 'kubernetes.io/arch': spec.key.architecture },
210
+ ...(spec.nodeName ? { nodeName: spec.nodeName } : {}),
211
+ securityContext: { runAsUser: 1000, fsGroup: 1000 },
212
+ initContainers: [{ name: 'isolation-gate', image: spec.image, imagePullPolicy: 'IfNotPresent',
213
+ command: ['bun', '-e', WARM_POD_ISOLATION], env: [
214
+ { name: 'WARM_HOST_PROBE_URL', value: spec.hostProbeUrl },
215
+ { name: 'WARM_PROD_PROBE_URL', value: spec.prodProbeUrl }] },
216
+ { name: 'prepare', image: spec.image, imagePullPolicy: 'IfNotPresent',
217
+ securityContext: { runAsUser: 0, allowPrivilegeEscalation: false },
218
+ command: ['bun', '-e', WARM_POD_PREPARE], env: [
219
+ { name: 'WARM_SOURCE_URL', value: mirror ? '/source-mirror' : spec.sourceUrl },
220
+ { name: 'WARM_CUT_SHA', value: spec.cutSha },
221
+ { name: 'WARM_LOCK_HASH', value: spec.key.lockHash },
222
+ ], volumeMounts: [{ name: 'workspace', mountPath: '/workspace' },
223
+ ...(mirror ? [{ name: 'source-mirror', mountPath: '/source-mirror', readOnly: true }] : [])] },
224
+ { name: 'install', image: spec.image, imagePullPolicy: 'IfNotPresent',
225
+ securityContext: { runAsUser: 0, allowPrivilegeEscalation: false },
226
+ command: ['bun', '-e', WARM_POD_INSTALL],
227
+ volumeMounts: [{ name: 'workspace', mountPath: '/workspace' }] }],
228
+ containers: [{ name: 'worker', image: spec.image, imagePullPolicy: 'IfNotPresent',
229
+ securityContext: { runAsUser: 0, allowPrivilegeEscalation: false,
230
+ capabilities: { drop: ['ALL'], add: ['CHOWN', 'SETUID', 'SETGID', 'KILL'] } },
231
+ command: ['sleep', 'infinity'], env: [
232
+ { name: 'WARM_WORKER_ID', value: spec.workerId },
233
+ { name: 'WARM_CUT_SHA', value: spec.cutSha },
234
+ { name: 'WARM_LOCK_HASH', value: spec.key.lockHash },
235
+ { name: 'WARM_IMAGE_DIGEST', value: spec.key.imageDigest },
236
+ { name: 'WARM_CONTEXT', value: spec.key.context },
237
+ { name: 'WARM_ARCHITECTURE', value: spec.key.architecture },
238
+ { name: 'WARM_BRIDGE_UID', value: '0' },
239
+ { name: 'WARM_POD_UID', valueFrom: { fieldRef: { apiVersion: 'v1', fieldPath: 'metadata.uid' } } },
240
+ { name: 'WARM_HOST_PROBE_URL', value: spec.hostProbeUrl },
241
+ { name: 'WARM_PROD_PROBE_URL', value: spec.prodProbeUrl },
242
+ ],
243
+ volumeMounts: [{ name: 'workspace', mountPath: '/workspace/repo', subPath: 'repo', readOnly: true },
244
+ { name: 'runtime-data', mountPath: '/workspace/data' }] }],
245
+ volumes: [{ name: 'workspace', emptyDir: {} },
246
+ { name: 'runtime-data', emptyDir: {} },
247
+ ...(mirror ? [{ name: 'source-mirror', hostPath: { path: mirror, type: 'Directory' } }] : [])],
248
+ },
249
+ };
250
+ }
251
+
252
+ export interface WarmPodTransportResult {
253
+ /** Every non-complete response leaves the existing one-shot Job authoritative. */
254
+ status: 'observed' | 'fallback';
255
+ reason: 'complete' | 'key-drift' | 'pod-unready' | 'pod-replaced' | 'exec-failed' | 'invalid-result' | 'worker-quarantined';
256
+ reply: WarmWorkerReply | null;
257
+ artifactsDir?: string;
258
+ /** Base64 preserves the exact bytes copied from the same Pod. Keys are artifact suffixes. */
259
+ artifacts?: readonly { file: string; data: Readonly<Record<string, string>> }[];
260
+ }
261
+ const fallback = (reason: WarmPodTransportResult['reason']): WarmPodTransportResult => ({ status: 'fallback', reason, reply: null });
262
+
263
+ function canonicalJson(value: unknown): unknown {
264
+ if (Array.isArray(value)) return value.map(canonicalJson);
265
+ if (value !== null && typeof value === 'object') return Object.fromEntries(
266
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
267
+ .map(([key, item]) => [key, canonicalJson(item)]));
268
+ return value;
269
+ }
270
+ function sameJson(left: unknown, right: unknown): boolean {
271
+ return JSON.stringify(canonicalJson(left)) === JSON.stringify(canonicalJson(right));
272
+ }
273
+
274
+ function syncDirectory(path: string): void {
275
+ const descriptor = openSync(path, 'r');
276
+ try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
277
+ }
278
+
279
+ function writePendingLedger(path: string, contents: string): void {
280
+ const descriptor = openSync(path, 'wx', 0o600);
281
+ try { writeFileSync(descriptor, contents); fsyncSync(descriptor); }
282
+ finally { closeSync(descriptor); }
283
+ syncDirectory(resolve(path, '..'));
284
+ }
285
+
286
+ /** Re-run the same complete-JUnit parser against the bytes fetched from the Pod. */
287
+ const WARM_HOST_REPARSE = `import { pathToFileURL } from 'node:url';
288
+ const [parserPath, prefix, file, cutSha, attemptId] = process.argv.slice(-5);
289
+ const { warmReferenceFromJobArtifacts } = await import(pathToFileURL(parserPath).href);
290
+ process.stdout.write(JSON.stringify(warmReferenceFromJobArtifacts(prefix, file,
291
+ { cutSha, attemptId })));`;
292
+ function reparseCopiedArtifacts(bytes: Record<string, Buffer>, file: string, command: WarmWorkerCommand) {
293
+ const directory = mkdtempSync(join(tmpdir(), 'elanous-warm-pod-'));
294
+ try {
295
+ const prefix = join(directory, 'file');
296
+ for (const [suffix, value] of Object.entries(bytes)) writeFileSync(`${prefix}.${suffix}`, value, { mode: 0o600 });
297
+ const sourceParser = fileURLToPath(new URL('../../../scripts/release-loop/gate-warm-artifacts.ts', import.meta.url));
298
+ const parserPath = existsSync(sourceParser) ? sourceParser
299
+ : resolve(process.cwd(), 'scripts/release-loop/gate-warm-artifacts.ts');
300
+ if (!existsSync(parserPath)) return null;
301
+ const parsed = spawnSync(process.execPath,
302
+ ['-e', WARM_HOST_REPARSE, parserPath, prefix, file, command.cutSha, command.attemptId],
303
+ { encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024 });
304
+ if (parsed.status !== 0) return null;
305
+ try { return JSON.parse(parsed.stdout); } catch { return null; }
306
+ } finally { rmSync(directory, { recursive: true }); }
307
+ }
308
+
309
+ /** Admission checks the actual source and isolation init contract, not just copied annotations. */
310
+ function matchesPodManifest(pod: any, spec: WarmPodSpec): boolean {
311
+ const expected = warmWorkerPodManifest(spec).spec as any;
312
+ const actual = pod?.spec;
313
+ if (!actual || actual.restartPolicy !== expected.restartPolicy
314
+ || actual.automountServiceAccountToken !== false
315
+ || actual.hostNetwork === true || actual.hostPID === true || actual.hostIPC === true
316
+ || actual.hostUsers === true || actual.shareProcessNamespace === true
317
+ || actual.hostAliases !== undefined || actual.dnsConfig !== undefined
318
+ || (actual.dnsPolicy !== undefined && actual.dnsPolicy !== 'ClusterFirst')
319
+ || (Array.isArray(actual.ephemeralContainers) && actual.ephemeralContainers.length > 0)
320
+ || !sameJson(actual.nodeSelector, expected.nodeSelector)
321
+ || !sameJson(actual.securityContext, expected.securityContext)
322
+ || !sameJson(actual.volumes, expected.volumes)
323
+ || (spec.nodeName !== undefined && actual.nodeName !== spec.nodeName)
324
+ || !Array.isArray(actual.initContainers) || actual.initContainers.length !== expected.initContainers.length
325
+ || !Array.isArray(actual.containers) || actual.containers.length !== expected.containers.length) return false;
326
+ for (const section of ['initContainers', 'containers'] as const) {
327
+ for (let index = 0; index < expected[section].length; index++) {
328
+ const want = expected[section][index];
329
+ const got = actual[section][index];
330
+ // API-server defaults may add resources/termination fields; execution-bearing fields stay exact.
331
+ for (const field of ['name', 'image', 'imagePullPolicy', 'command', 'args', 'workingDir', 'env', 'envFrom',
332
+ 'volumeMounts', 'volumeDevices', 'ports', 'securityContext', 'stdin', 'stdinOnce', 'tty', 'lifecycle',
333
+ 'startupProbe', 'livenessProbe', 'readinessProbe', 'resizePolicy', 'restartPolicy']) {
334
+ if (!sameJson(got?.[field], want?.[field])) return false;
335
+ }
336
+ }
337
+ }
338
+ return true;
339
+ }
340
+
341
+ /** Host-side serial queue. It observes an existing Pod; it never applies the manifest. */
342
+ export class WarmPodTransport {
343
+ private tail: Promise<unknown> = Promise.resolve();
344
+ private podUid: string | null = null;
345
+ private quarantined = false;
346
+ private readonly ledgerDir: string | null;
347
+ constructor(private readonly spec: WarmPodSpec, private readonly kubectl: Kubectl = warmKubectl,
348
+ quarantineLedgerDir?: string) {
349
+ validateSpec(spec);
350
+ if (quarantineLedgerDir !== undefined && !isAbsolute(quarantineLedgerDir)) throw new Error('quarantine ledger must be absolute');
351
+ this.ledgerDir = quarantineLedgerDir ?? (kubectl === warmKubectl
352
+ ? join(elanousStateRoot(), 'warm-pod-quarantine') : null);
353
+ if (this.ledgerDir && !isAbsolute(this.ledgerDir)) throw new Error('quarantine ledger must be absolute');
354
+ }
355
+
356
+ request(command: WarmWorkerCommand): Promise<WarmPodTransportResult> {
357
+ const next = this.tail.then(() => this.send(command));
358
+ this.tail = next.catch(() => undefined);
359
+ return next;
360
+ }
361
+
362
+ private send(command: WarmWorkerCommand): WarmPodTransportResult {
363
+ if (this.quarantined) return fallback('worker-quarantined');
364
+ if (!command?.key || command.envProfile !== 'gate-v1'
365
+ || command.workerId !== this.spec.workerId || command.cutSha !== this.spec.cutSha
366
+ || !equalKey(command.key, this.spec.key)
367
+ || ![command.runId, command.attemptId, command.leaseId, command.workerId].every(value => TOKEN.test(value))
368
+ || !Number.isSafeInteger(command.generation) || command.generation < 1) return fallback('key-drift');
369
+ const prefix = ['--context', this.spec.kubectlContext, '-n', this.spec.namespace];
370
+ const observed = this.kubectl([...prefix, 'get', 'pod', this.spec.name, '-o', 'json']);
371
+ if (observed.status !== 0) return fallback('pod-unready');
372
+ let pod: any;
373
+ try { pod = JSON.parse(observed.stdout); } catch { return fallback('pod-unready'); }
374
+ const a = pod?.metadata?.annotations;
375
+ const uid = pod?.metadata?.uid;
376
+ if (pod?.metadata?.name !== this.spec.name || pod?.metadata?.namespace !== this.spec.namespace
377
+ || typeof uid !== 'string' || !uid || pod?.status?.phase !== 'Running'
378
+ || !pod?.status?.containerStatuses?.some((row: any) => row.name === 'worker' && row.ready === true)
379
+ || !matchesPodManifest(pod, this.spec)
380
+ || a?.['elanous.dev/warm-cut-sha'] !== this.spec.cutSha
381
+ || a?.['elanous.dev/warm-lock-hash'] !== this.spec.key.lockHash
382
+ || a?.['elanous.dev/warm-image-digest'] !== this.spec.key.imageDigest
383
+ || a?.['elanous.dev/warm-worker-id'] !== this.spec.workerId
384
+ || a?.['elanous.dev/warm-context'] !== this.spec.key.context
385
+ || a?.['elanous.dev/warm-architecture'] !== this.spec.key.architecture) return fallback('pod-unready');
386
+ const nodeName = pod?.spec?.nodeName;
387
+ if (typeof nodeName !== 'string' || !nodeName) return fallback('pod-unready');
388
+ const sameAdmittedPod = (raw: string): boolean => {
389
+ try {
390
+ const current = JSON.parse(raw);
391
+ return current?.metadata?.uid === uid && current?.metadata?.name === this.spec.name
392
+ && current?.metadata?.namespace === this.spec.namespace
393
+ && sameJson(current?.metadata?.annotations, a)
394
+ && current?.status?.phase === 'Running'
395
+ && current?.status?.containerStatuses?.some((row: any) => row.name === 'worker' && row.ready === true)
396
+ && current?.spec?.nodeName === nodeName && matchesPodManifest(current, this.spec);
397
+ } catch { return false; }
398
+ };
399
+ const node = this.kubectl(['--context', this.spec.kubectlContext, 'get', 'node', nodeName, '-o', 'json']);
400
+ let nodeArch: unknown;
401
+ try { nodeArch = node.status === 0 ? JSON.parse(node.stdout)?.metadata?.labels?.['kubernetes.io/arch'] : null; }
402
+ catch { nodeArch = null; }
403
+ if (nodeArch !== this.spec.key.architecture) return fallback('pod-unready');
404
+ if (this.podUid && uid !== this.podUid) { this.quarantined = true; return fallback('pod-replaced'); }
405
+ this.podUid = uid;
406
+ const ledgerKey = sha256([this.spec.kubectlContext, this.spec.namespace, this.spec.name, uid].join('\0'));
407
+ const ledgerPath = this.ledgerDir ? join(this.ledgerDir, ledgerKey) : null;
408
+ let pending = pendingByKubectl.get(this.kubectl);
409
+ if (!pending) { pending = new Set<string>(); pendingByKubectl.set(this.kubectl, pending); }
410
+ if (pending.has(ledgerKey) || (ledgerPath && existsSync(ledgerPath))) {
411
+ this.quarantined = true;
412
+ return fallback('worker-quarantined');
413
+ }
414
+ const exec = this.kubectl([...prefix, 'exec', '-i', this.spec.name, '-c', 'worker', '--', 'bun', '-e', WARM_POD_EXEC], JSON.stringify(command));
415
+ if (exec.status !== 0) { this.quarantined = true; return fallback('exec-failed'); }
416
+ // UID alone is insufficient: ephemeral containers can be added to a live Pod under the same UID.
417
+ const after = this.kubectl([...prefix, 'get', 'pod', this.spec.name, '-o', 'json']);
418
+ if (after.status !== 0 || !sameAdmittedPod(after.stdout)) { this.quarantined = true; return fallback('pod-replaced'); }
419
+ let outcome: WarmWorkerOutcome & { artifactHashes?: Array<Record<string, string>> };
420
+ try { outcome = JSON.parse(exec.stdout) as WarmWorkerOutcome & { artifactHashes?: Array<Record<string, string>> }; }
421
+ catch { this.quarantined = true; return fallback('invalid-result'); }
422
+ if (outcome.quarantineWorker || outcome.reason !== 'complete') {
423
+ this.quarantined = true;
424
+ return fallback('worker-quarantined');
425
+ }
426
+ const expectedArtifactsDir = `/workspace/data/artifacts/${[
427
+ command.runId, command.attemptId, command.leaseId, String(command.generation), command.workerId,
428
+ ].join('/')}`;
429
+ const reply = parseWarmWorkerReply(outcome.reply);
430
+ if (!reply || outcome.artifactsDir !== expectedArtifactsDir
431
+ || !Array.isArray(outcome.artifactHashes) || outcome.artifactHashes.length !== command.files.length
432
+ || reply.runId !== command.runId || reply.attemptId !== command.attemptId
433
+ || reply.cutSha !== command.cutSha || reply.leaseId !== command.leaseId
434
+ || reply.generation !== command.generation || reply.workerId !== command.workerId
435
+ || reply.envProfile !== command.envProfile || reply.baselineSha !== command.baselineSha
436
+ || !equalKey(reply.key, command.key) || reply.finishedAt > command.deadlineAt
437
+ || reply.files.length !== command.files.length
438
+ || reply.files.some((part, index) => part.file !== command.files[index] || !part.junitComplete)) {
439
+ this.quarantined = true;
440
+ return fallback('invalid-result');
441
+ }
442
+ const audit: Array<{ file: string; data: Readonly<Record<string, string>> }> = [];
443
+ try {
444
+ for (const [index, file] of command.files.entries()) {
445
+ const prefix = `${expectedArtifactsDir}/file-${index}`;
446
+ const fetched: Record<string, Buffer> = {};
447
+ const encoded: Record<string, string> = {};
448
+ for (const suffix of ['log', 'junit.xml', 'json', 'warm-manifest.json', 'rc']) {
449
+ const got = this.kubectl([...prefixArgs(this.spec), 'exec', this.spec.name, '-c', 'worker', '--', 'bun', '-e', WARM_POD_READ, `${prefix}.${suffix}`]);
450
+ if (got.status !== 0 || got.stdout.length > 8 * 1024 * 1024
451
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(got.stdout)) throw new Error('artifact unavailable');
452
+ const bytes = Buffer.from(got.stdout, 'base64');
453
+ if (bytes.toString('base64') !== got.stdout || sha256(bytes) !== outcome.artifactHashes[index]?.[suffix])
454
+ throw new Error('artifact changed after reply');
455
+ fetched[suffix] = bytes;
456
+ encoded[suffix] = got.stdout;
457
+ }
458
+ const manifest = JSON.parse(fetched['warm-manifest.json']!.toString('utf8')) as Record<string, unknown>;
459
+ const meta = JSON.parse(fetched.json!.toString('utf8')) as Record<string, unknown>;
460
+ const reported = reply.files[index];
461
+ const metaKey = meta.warmKey as WarmWorkerKey | undefined;
462
+ if (!reported || manifest.protocol !== 1 || manifest.cutSha !== command.cutSha
463
+ || manifest.attemptId !== command.attemptId || manifest.file !== file || manifest.rc !== reported.rc
464
+ || manifest.logSha256 !== sha256(fetched.log!)
465
+ || manifest.junitSha256 !== sha256(fetched['junit.xml']!)
466
+ || manifest.metaSha256 !== sha256(fetched.json!) || manifest.rcSha256 !== sha256(fetched.rc!)
467
+ || fetched.rc?.toString('utf8').trim() !== String(reported.rc)
468
+ || meta.rc !== reported.rc || meta.commit !== command.cutSha || meta.attemptId !== command.attemptId
469
+ || meta.jobFailed !== false || meta.jobExitCode !== 0
470
+ || !Array.isArray(meta.files) || meta.files.length !== 1 || meta.files[0] !== file
471
+ || !metaKey || !equalKey(metaKey, command.key)) throw new Error('artifact mismatch');
472
+ const reparsed = reparseCopiedArtifacts(fetched, file, command);
473
+ if (!reparsed || reparsed.rc !== reported.rc || reparsed.junitComplete !== reported.junitComplete
474
+ || !sameJson(reparsed.failureIds, reported.failureIds)
475
+ || !sameJson(reparsed.errorIds, reported.errorIds)) throw new Error('artifact parse mismatch');
476
+ audit.push({ file, data: encoded });
477
+ }
478
+ const finalPod = this.kubectl([...prefixArgs(this.spec), 'get', 'pod', this.spec.name, '-o', 'json']);
479
+ if (finalPod.status !== 0 || !sameAdmittedPod(finalPod.stdout)) throw new Error('Pod changed during artifact return');
480
+ } catch {
481
+ this.quarantined = true;
482
+ return fallback('invalid-result');
483
+ }
484
+ const identity = { runId: command.runId, attemptId: command.attemptId,
485
+ leaseId: command.leaseId, generation: command.generation };
486
+ try {
487
+ if (ledgerPath) {
488
+ mkdirSync(this.ledgerDir!, { recursive: true, mode: 0o700 });
489
+ syncDirectory(resolve(this.ledgerDir!, '..'));
490
+ writePendingLedger(ledgerPath, JSON.stringify({ uid, identity }));
491
+ }
492
+ pending.add(ledgerKey);
493
+ } catch {
494
+ this.quarantined = true;
495
+ return fallback('invalid-result');
496
+ }
497
+ const ack = this.kubectl([...prefixArgs(this.spec), 'exec', '-i', this.spec.name, '-c', 'worker',
498
+ '--', 'bun', '-e', WARM_POD_ACK], JSON.stringify(identity));
499
+ if (ack.status !== 0 || ack.stdout !== 'ack') {
500
+ this.quarantined = true;
501
+ return fallback('invalid-result');
502
+ }
503
+ const afterAck = this.kubectl([...prefixArgs(this.spec), 'get', 'pod', this.spec.name, '-o', 'json']);
504
+ if (afterAck.status !== 0 || !sameAdmittedPod(afterAck.stdout)) {
505
+ this.quarantined = true;
506
+ return fallback('pod-replaced');
507
+ }
508
+ try { if (ledgerPath) { rmSync(ledgerPath); syncDirectory(this.ledgerDir!); } }
509
+ catch { this.quarantined = true; return fallback('invalid-result'); }
510
+ pending.delete(ledgerKey);
511
+ return { status: 'observed', reason: 'complete', reply, artifactsDir: expectedArtifactsDir, artifacts: audit };
512
+ }
513
+ }
514
+
515
+ const prefixArgs = (spec: WarmPodSpec) => ['--context', spec.kubectlContext, '-n', spec.namespace];
516
+
517
+ /** Explicit shadow caller for a pre-existing warm Pod. Input file is `{spec, command}`; no apply/create path exists here. */
518
+ if (import.meta.main) {
519
+ const [mode, inputPath] = process.argv.slice(2);
520
+ if (mode !== '--shadow-request' || !inputPath) throw new Error('usage: bun pod-warm-worker-pod.ts --shadow-request <payload.json>');
521
+ const input = JSON.parse(readFileSync(inputPath, 'utf8')) as { spec: WarmPodSpec; command: WarmWorkerCommand };
522
+ const result = await new WarmPodTransport(input.spec).request(input.command);
523
+ process.stdout.write(`${JSON.stringify(result)}\n`);
524
+ if (result.status !== 'observed') process.exitCode = 2;
525
+ }
@@ -0,0 +1,90 @@
1
+ /** Deterministic file runner for a dedicated Linux warm Pod PID namespace. */
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { appendFileSync, existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import type { WarmWorkerRunFile } from './pod-warm-worker-runtime.js';
7
+
8
+ /** Pure namespace inventory; the production call always uses the kernel /proc mount. */
9
+ export function warmPodNamespaceResidue(procRoot: string, selfPid: number): number[] {
10
+ return readdirSync(procRoot, { withFileTypes: true })
11
+ .filter(entry => entry.isDirectory() && /^\d+$/.test(entry.name))
12
+ .map(entry => Number(entry.name))
13
+ .filter(pid => pid !== 1 && pid !== selfPid && existsSync(join(procRoot, String(pid))))
14
+ .sort((a, b) => a - b);
15
+ }
16
+
17
+ function assertWarmPod(request: WarmWorkerRunFile): void {
18
+ if (process.platform !== 'linux' || !process.env.WARM_WORKER_ID
19
+ || request.env.WARM_WORKER_ID !== process.env.WARM_WORKER_ID
20
+ || !process.env.WARM_BRIDGE_UID || request.env.WARM_BRIDGE_UID !== process.env.WARM_BRIDGE_UID
21
+ || !process.env.WARM_POD_UID || request.env.WARM_POD_UID !== process.env.WARM_POD_UID)
22
+ throw new Error('warm runner unsupported outside Linux Pod worker');
23
+ let init: string;
24
+ let selfNamespace: string;
25
+ let initNamespace: string;
26
+ try {
27
+ init = readFileSync('/proc/1/comm', 'utf8').trim();
28
+ selfNamespace = readlinkSync('/proc/self/ns/pid');
29
+ initNamespace = readlinkSync('/proc/1/ns/pid');
30
+ }
31
+ catch { throw new Error('warm runner PID namespace unmeasured'); }
32
+ if (init !== 'sleep' || process.pid === 1 || !/^pid:\[\d+\]$/.test(selfNamespace)
33
+ || selfNamespace !== initNamespace) throw new Error('warm runner unsupported PID namespace');
34
+ }
35
+
36
+ function stopGroup(pgid: number | undefined): boolean {
37
+ if (!pgid) return false;
38
+ try { process.kill(-pgid, 'SIGKILL'); }
39
+ catch (error) {
40
+ const code = (error as NodeJS.ErrnoException).code;
41
+ if (code === 'ESRCH') return true;
42
+ if (code === 'EPERM') return false;
43
+ throw error;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ /** A lingering process anywhere in this dedicated namespace fences the whole Pod. */
49
+ export async function runWarmWorkerFile(request: WarmWorkerRunFile): Promise<number> {
50
+ assertWarmPod(request);
51
+ if (Date.now() >= request.deadlineAt) return 124;
52
+ if (warmPodNamespaceResidue('/proc', process.pid).length > 0) return 124;
53
+ const output = Bun.file(request.logPath).writer();
54
+ output.write(`[warm-worker] file=${request.file}\n[warm-worker] cut=${request.cutSha} attempt=${request.attemptId}\n`);
55
+ if (Date.now() >= request.deadlineAt) { await output.end(); return 124; }
56
+ const child = spawn('bun', ['run', 'scripts/test-deterministic.ts', request.file, '--reporter=junit', `--reporter-outfile=${request.junitPath}`],
57
+ { cwd: request.cwd, env: request.env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
58
+ child.stdout?.on('data', chunk => output.write(chunk));
59
+ child.stderr?.on('data', chunk => output.write(chunk));
60
+ let timedOut = false;
61
+ let expire!: () => void;
62
+ const expired = new Promise<number>(resolve => { expire = () => resolve(124); });
63
+ const timeout = setTimeout(() => {
64
+ timedOut = true;
65
+ if (!stopGroup(child.pid)) child.kill('SIGKILL');
66
+ child.stdout?.destroy();
67
+ child.stderr?.destroy();
68
+ expire();
69
+ }, Math.max(0, request.deadlineAt - Date.now()));
70
+ try {
71
+ const closed = new Promise<number>((done, fail) => {
72
+ child.once('error', fail);
73
+ child.once('close', (code, signal) => done(signal ? 124 : code ?? 1));
74
+ });
75
+ const rc = await Promise.race([closed, expired]);
76
+ const residueBeforeStop = warmPodNamespaceResidue('/proc', process.pid).length > 0;
77
+ if (!stopGroup(child.pid)) child.kill('SIGKILL');
78
+ const residueAfterStop = warmPodNamespaceResidue('/proc', process.pid).length > 0;
79
+ if (residueBeforeStop || residueAfterStop || timedOut || Date.now() >= request.deadlineAt) return 124;
80
+ return rc;
81
+ } finally {
82
+ clearTimeout(timeout);
83
+ if (!stopGroup(child.pid)) child.kill('SIGKILL');
84
+ await output.end();
85
+ if (existsSync(request.junitPath)) {
86
+ const digest = createHash('sha256').update(readFileSync(request.junitPath)).digest('hex');
87
+ appendFileSync(request.logPath, `\n[warm-worker] junit-sha256=${digest}\n`);
88
+ }
89
+ }
90
+ }