@provos/ironcurtain 0.11.0 → 0.12.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 (365) hide show
  1. package/README.md +9 -7
  2. package/dist/cli-help.d.ts +12 -0
  3. package/dist/cli-help.js +21 -0
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.js +8 -6
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config/agent-model-guard.d.ts +14 -0
  8. package/dist/config/agent-model-guard.js +24 -0
  9. package/dist/config/agent-model-guard.js.map +1 -0
  10. package/dist/config/config-command.js +116 -1
  11. package/dist/config/config-command.js.map +1 -1
  12. package/dist/config/first-start.js +2 -8
  13. package/dist/config/first-start.js.map +1 -1
  14. package/dist/config/model-provider.d.ts +9 -0
  15. package/dist/config/model-provider.js +13 -0
  16. package/dist/config/model-provider.js.map +1 -1
  17. package/dist/config/paths.d.ts +48 -6
  18. package/dist/config/paths.js +63 -8
  19. package/dist/config/paths.js.map +1 -1
  20. package/dist/config/user-config.d.ts +48 -1
  21. package/dist/config/user-config.js +49 -1
  22. package/dist/config/user-config.js.map +1 -1
  23. package/dist/daemon/daemon-command.js +14 -5
  24. package/dist/daemon/daemon-command.js.map +1 -1
  25. package/dist/daemon/ironcurtain-daemon.d.ts +18 -0
  26. package/dist/daemon/ironcurtain-daemon.js +22 -1
  27. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  28. package/dist/daemon-client/daemon-client.d.ts +123 -0
  29. package/dist/daemon-client/daemon-client.js +324 -0
  30. package/dist/daemon-client/daemon-client.js.map +1 -0
  31. package/dist/docker/adapters/codex.d.ts +9 -0
  32. package/dist/docker/adapters/codex.js +297 -0
  33. package/dist/docker/adapters/codex.js.map +1 -0
  34. package/dist/docker/agent-adapter.d.ts +5 -1
  35. package/dist/docker/agent-adapter.js +2 -0
  36. package/dist/docker/agent-adapter.js.map +1 -1
  37. package/dist/docker/agent-registry.js +5 -0
  38. package/dist/docker/agent-registry.js.map +1 -1
  39. package/dist/docker/apple-container-manager.d.ts +49 -0
  40. package/dist/docker/apple-container-manager.js +464 -0
  41. package/dist/docker/apple-container-manager.js.map +1 -0
  42. package/dist/docker/code-mode-proxy.d.ts +11 -0
  43. package/dist/docker/code-mode-proxy.js +3 -1
  44. package/dist/docker/code-mode-proxy.js.map +1 -1
  45. package/dist/docker/container-lifecycle.d.ts +2 -2
  46. package/dist/docker/container-lifecycle.js.map +1 -1
  47. package/dist/docker/container-runtime.d.ts +29 -0
  48. package/dist/docker/container-runtime.js +76 -0
  49. package/dist/docker/container-runtime.js.map +1 -0
  50. package/dist/docker/docker-agent-session.js +30 -1
  51. package/dist/docker/docker-agent-session.js.map +1 -1
  52. package/dist/docker/docker-infrastructure.d.ts +186 -9
  53. package/dist/docker/docker-infrastructure.js +521 -77
  54. package/dist/docker/docker-infrastructure.js.map +1 -1
  55. package/dist/docker/docker-manager.d.ts +25 -5
  56. package/dist/docker/docker-manager.js +262 -22
  57. package/dist/docker/docker-manager.js.map +1 -1
  58. package/dist/docker/docker-probe.d.ts +45 -0
  59. package/dist/docker/docker-probe.js +86 -0
  60. package/dist/docker/docker-probe.js.map +1 -0
  61. package/dist/docker/docker-progress-sink.d.ts +1 -1
  62. package/dist/docker/mitm-proxy.d.ts +52 -1
  63. package/dist/docker/mitm-proxy.js +234 -10
  64. package/dist/docker/mitm-proxy.js.map +1 -1
  65. package/dist/docker/network-topology.d.ts +81 -0
  66. package/dist/docker/network-topology.js +160 -0
  67. package/dist/docker/network-topology.js.map +1 -0
  68. package/dist/docker/oauth-credentials.d.ts +13 -0
  69. package/dist/docker/oauth-credentials.js +125 -0
  70. package/dist/docker/oauth-credentials.js.map +1 -1
  71. package/dist/docker/package-types.d.ts +1 -1
  72. package/dist/docker/provider-config.d.ts +29 -1
  73. package/dist/docker/provider-config.js +93 -3
  74. package/dist/docker/provider-config.js.map +1 -1
  75. package/dist/docker/provision-lock.d.ts +46 -0
  76. package/dist/docker/provision-lock.js +152 -0
  77. package/dist/docker/provision-lock.js.map +1 -0
  78. package/dist/docker/pty-session.d.ts +15 -0
  79. package/dist/docker/pty-session.js +139 -67
  80. package/dist/docker/pty-session.js.map +1 -1
  81. package/dist/docker/registry-proxy.d.ts +46 -0
  82. package/dist/docker/registry-proxy.js +255 -1
  83. package/dist/docker/registry-proxy.js.map +1 -1
  84. package/dist/docker/resource-limits.d.ts +14 -5
  85. package/dist/docker/resource-limits.js +11 -8
  86. package/dist/docker/resource-limits.js.map +1 -1
  87. package/dist/docker/trajectory-capture.d.ts +79 -0
  88. package/dist/docker/trajectory-capture.js +521 -0
  89. package/dist/docker/trajectory-capture.js.map +1 -0
  90. package/dist/docker/trajectory-reassembler.d.ts +286 -0
  91. package/dist/docker/trajectory-reassembler.js +990 -0
  92. package/dist/docker/trajectory-reassembler.js.map +1 -0
  93. package/dist/docker/trajectory-tap.d.ts +100 -0
  94. package/dist/docker/trajectory-tap.js +410 -0
  95. package/dist/docker/trajectory-tap.js.map +1 -0
  96. package/dist/docker/trajectory-types.d.ts +187 -0
  97. package/dist/docker/trajectory-types.js +43 -0
  98. package/dist/docker/trajectory-types.js.map +1 -0
  99. package/dist/docker/types.d.ts +53 -7
  100. package/dist/doctor/checks.d.ts +36 -3
  101. package/dist/doctor/checks.js +102 -15
  102. package/dist/doctor/checks.js.map +1 -1
  103. package/dist/doctor/doctor-command.js +39 -9
  104. package/dist/doctor/doctor-command.js.map +1 -1
  105. package/dist/doctor/mcp-liveness.d.ts +1 -1
  106. package/dist/doctor/mcp-liveness.js +12 -4
  107. package/dist/doctor/mcp-liveness.js.map +1 -1
  108. package/dist/index.js +30 -12
  109. package/dist/index.js.map +1 -1
  110. package/dist/mux/mux-app.d.ts +2 -0
  111. package/dist/mux/mux-app.js +1 -0
  112. package/dist/mux/mux-app.js.map +1 -1
  113. package/dist/mux/mux-command.js +19 -6
  114. package/dist/mux/mux-command.js.map +1 -1
  115. package/dist/mux/pty-bridge.d.ts +2 -0
  116. package/dist/mux/pty-bridge.js +3 -0
  117. package/dist/mux/pty-bridge.js.map +1 -1
  118. package/dist/observe/observe-command.js +112 -125
  119. package/dist/observe/observe-command.js.map +1 -1
  120. package/dist/persona/broad-policy-validator.d.ts +95 -0
  121. package/dist/persona/broad-policy-validator.js +171 -0
  122. package/dist/persona/broad-policy-validator.js.map +1 -0
  123. package/dist/persona/compile-persona-policy.d.ts +54 -2
  124. package/dist/persona/compile-persona-policy.js +29 -4
  125. package/dist/persona/compile-persona-policy.js.map +1 -1
  126. package/dist/persona/event-bus-progress-reporter.d.ts +57 -0
  127. package/dist/persona/event-bus-progress-reporter.js +66 -0
  128. package/dist/persona/event-bus-progress-reporter.js.map +1 -0
  129. package/dist/persona/persona-command.js +46 -76
  130. package/dist/persona/persona-command.js.map +1 -1
  131. package/dist/persona/persona-compile-orchestrator.d.ts +101 -0
  132. package/dist/persona/persona-compile-orchestrator.js +504 -0
  133. package/dist/persona/persona-compile-orchestrator.js.map +1 -0
  134. package/dist/persona/persona-service.d.ts +137 -0
  135. package/dist/persona/persona-service.js +263 -0
  136. package/dist/persona/persona-service.js.map +1 -0
  137. package/dist/persona/policy-mutation-audit.d.ts +109 -0
  138. package/dist/persona/policy-mutation-audit.js +179 -0
  139. package/dist/persona/policy-mutation-audit.js.map +1 -0
  140. package/dist/persona/rule-delta.d.ts +30 -0
  141. package/dist/persona/rule-delta.js +82 -0
  142. package/dist/persona/rule-delta.js.map +1 -0
  143. package/dist/persona/types.d.ts +12 -0
  144. package/dist/pipeline/annotate.js +10 -2
  145. package/dist/pipeline/annotate.js.map +1 -1
  146. package/dist/pipeline/compile.js +14 -2
  147. package/dist/pipeline/compile.js.map +1 -1
  148. package/dist/pipeline/pipeline-runner.d.ts +63 -4
  149. package/dist/pipeline/pipeline-runner.js +159 -50
  150. package/dist/pipeline/pipeline-runner.js.map +1 -1
  151. package/dist/pipeline/pipeline-shared.d.ts +32 -3
  152. package/dist/pipeline/pipeline-shared.js +76 -8
  153. package/dist/pipeline/pipeline-shared.js.map +1 -1
  154. package/dist/pipeline/refresh-lists.js +3 -5
  155. package/dist/pipeline/refresh-lists.js.map +1 -1
  156. package/dist/sandbox/index.d.ts +19 -4
  157. package/dist/sandbox/index.js +31 -18
  158. package/dist/sandbox/index.js.map +1 -1
  159. package/dist/session/index.js +10 -1
  160. package/dist/session/index.js.map +1 -1
  161. package/dist/session/preflight.d.ts +1 -30
  162. package/dist/session/preflight.js +52 -85
  163. package/dist/session/preflight.js.map +1 -1
  164. package/dist/session/types.d.ts +9 -0
  165. package/dist/signal/bot-command.js +6 -1
  166. package/dist/signal/bot-command.js.map +1 -1
  167. package/dist/signal/setup-signal.js +1 -1
  168. package/dist/signal/setup-signal.js.map +1 -1
  169. package/dist/signal/signal-container.d.ts +3 -3
  170. package/dist/signal/signal-container.js +2 -2
  171. package/dist/signal/signal-container.js.map +1 -1
  172. package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
  173. package/dist/trusted-process/call-circuit-breaker.js +22 -3
  174. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  175. package/dist/trusted-process/container-command.d.ts +35 -0
  176. package/dist/trusted-process/container-command.js +49 -0
  177. package/dist/trusted-process/container-command.js.map +1 -0
  178. package/dist/trusted-process/mcp-proxy-server.js +35 -3
  179. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  180. package/dist/trusted-process/policy-engine.js +13 -2
  181. package/dist/trusted-process/policy-engine.js.map +1 -1
  182. package/dist/trusted-process/tcp-server-transport.d.ts +12 -1
  183. package/dist/trusted-process/tcp-server-transport.js +9 -1
  184. package/dist/trusted-process/tcp-server-transport.js.map +1 -1
  185. package/dist/trusted-process/tool-call-coordinator.d.ts +22 -8
  186. package/dist/trusted-process/tool-call-coordinator.js +149 -65
  187. package/dist/trusted-process/tool-call-coordinator.js.map +1 -1
  188. package/dist/trusted-process/tool-call-pipeline.d.ts +8 -0
  189. package/dist/trusted-process/tool-call-pipeline.js +68 -34
  190. package/dist/trusted-process/tool-call-pipeline.js.map +1 -1
  191. package/dist/util/atomic-write.d.ts +31 -0
  192. package/dist/util/atomic-write.js +43 -0
  193. package/dist/util/atomic-write.js.map +1 -0
  194. package/dist/web-ui/conn-registry.d.ts +14 -0
  195. package/dist/web-ui/conn-registry.js +25 -0
  196. package/dist/web-ui/conn-registry.js.map +1 -0
  197. package/dist/web-ui/dispatch/persona-dispatch.d.ts +46 -2
  198. package/dist/web-ui/dispatch/persona-dispatch.js +209 -56
  199. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -1
  200. package/dist/web-ui/dispatch/session-dispatch.js +22 -4
  201. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -1
  202. package/dist/web-ui/dispatch/types.d.ts +23 -0
  203. package/dist/web-ui/dispatch/types.js +4 -0
  204. package/dist/web-ui/dispatch/types.js.map +1 -1
  205. package/dist/web-ui/dispatch/workflow-dispatch.js +116 -5
  206. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -1
  207. package/dist/web-ui/json-rpc-dispatch.js +1 -1
  208. package/dist/web-ui/json-rpc-dispatch.js.map +1 -1
  209. package/dist/web-ui/web-event-bus.d.ts +25 -1
  210. package/dist/web-ui/web-event-bus.js.map +1 -1
  211. package/dist/web-ui/web-ui-server.d.ts +14 -0
  212. package/dist/web-ui/web-ui-server.js +16 -0
  213. package/dist/web-ui/web-ui-server.js.map +1 -1
  214. package/dist/web-ui/web-ui-types.d.ts +133 -5
  215. package/dist/web-ui/web-ui-types.js.map +1 -1
  216. package/dist/web-ui-static/assets/index-BD5U6ILh.css +1 -0
  217. package/dist/web-ui-static/assets/index-FMsHWEfK.js +79 -0
  218. package/dist/web-ui-static/index.html +2 -2
  219. package/dist/workflow/cli-shared.d.ts +62 -0
  220. package/dist/workflow/cli-shared.js +117 -0
  221. package/dist/workflow/cli-shared.js.map +1 -0
  222. package/dist/workflow/cli-support.js +0 -2
  223. package/dist/workflow/cli-support.js.map +1 -1
  224. package/dist/workflow/container-snapshots.d.ts +29 -0
  225. package/dist/workflow/container-snapshots.js +137 -0
  226. package/dist/workflow/container-snapshots.js.map +1 -0
  227. package/dist/workflow/daemon-gate-commands.d.ts +21 -0
  228. package/dist/workflow/daemon-gate-commands.js +599 -0
  229. package/dist/workflow/daemon-gate-commands.js.map +1 -0
  230. package/dist/workflow/discovery.d.ts +29 -0
  231. package/dist/workflow/discovery.js +67 -7
  232. package/dist/workflow/discovery.js.map +1 -1
  233. package/dist/workflow/lane-template.d.ts +89 -0
  234. package/dist/workflow/lane-template.js +141 -0
  235. package/dist/workflow/lane-template.js.map +1 -0
  236. package/dist/workflow/lint.d.ts +7 -1
  237. package/dist/workflow/lint.js +186 -3
  238. package/dist/workflow/lint.js.map +1 -1
  239. package/dist/workflow/machine-builder.d.ts +90 -2
  240. package/dist/workflow/machine-builder.js +222 -29
  241. package/dist/workflow/machine-builder.js.map +1 -1
  242. package/dist/workflow/message-log.d.ts +15 -1
  243. package/dist/workflow/message-log.js.map +1 -1
  244. package/dist/workflow/orchestrator.d.ts +505 -1
  245. package/dist/workflow/orchestrator.js +1722 -69
  246. package/dist/workflow/orchestrator.js.map +1 -1
  247. package/dist/workflow/prompt-builder.js +2 -1
  248. package/dist/workflow/prompt-builder.js.map +1 -1
  249. package/dist/workflow/run-state-command.js +1 -1
  250. package/dist/workflow/run-state-command.js.map +1 -1
  251. package/dist/workflow/status-parser.js +19 -4
  252. package/dist/workflow/status-parser.js.map +1 -1
  253. package/dist/workflow/types.d.ts +123 -23
  254. package/dist/workflow/types.js +2 -0
  255. package/dist/workflow/types.js.map +1 -1
  256. package/dist/workflow/validate.d.ts +23 -1
  257. package/dist/workflow/validate.js +311 -22
  258. package/dist/workflow/validate.js.map +1 -1
  259. package/dist/workflow/workflow-command.d.ts +0 -17
  260. package/dist/workflow/workflow-command.js +110 -88
  261. package/dist/workflow/workflow-command.js.map +1 -1
  262. package/dist/workflow/workflow-discovery.d.ts +2 -0
  263. package/dist/workflow/workflow-discovery.js +22 -1
  264. package/dist/workflow/workflow-discovery.js.map +1 -1
  265. package/dist/workflow/workflow-manager.d.ts +34 -0
  266. package/dist/workflow/workflow-manager.js +52 -1
  267. package/dist/workflow/workflow-manager.js.map +1 -1
  268. package/dist/workflow/workflows/design-and-code/README.md +50 -0
  269. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  270. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  271. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  272. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  273. package/dist/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  274. package/dist/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  275. package/dist/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  276. package/dist/workflow/workflows/evolve/scripts/LICENSE +201 -0
  277. package/dist/workflow/workflows/evolve/scripts/README.md +314 -0
  278. package/dist/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  279. package/dist/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  280. package/dist/workflow/workflows/evolve/scripts/evolve-db +16 -0
  281. package/dist/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  282. package/dist/workflow/workflows/evolve/scripts/evolve-files +16 -0
  283. package/dist/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  284. package/dist/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  285. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  286. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  287. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  288. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  289. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  290. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  291. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  292. package/dist/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  293. package/dist/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  294. package/dist/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  295. package/dist/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  296. package/dist/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  297. package/dist/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  298. package/dist/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  299. package/dist/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  300. package/dist/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  301. package/dist/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  302. package/dist/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  303. package/dist/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  304. package/dist/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  305. package/dist/workflow/workflows/evolve/workflow.yaml +646 -0
  306. package/dist/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  307. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  308. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  309. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  310. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  311. package/docker/Dockerfile.base +22 -0
  312. package/docker/Dockerfile.base.arm64 +18 -0
  313. package/docker/Dockerfile.claude-code +3 -1
  314. package/docker/Dockerfile.codex +17 -0
  315. package/docker/Dockerfile.goose +3 -1
  316. package/docker/entrypoint-claude-code.sh +6 -45
  317. package/docker/entrypoint-codex.sh +42 -0
  318. package/docker/entrypoint-goose.sh +5 -24
  319. package/docker/entrypoint-uid-remap.sh +93 -0
  320. package/package.json +9 -4
  321. package/src/workflow/workflows/design-and-code/README.md +50 -0
  322. package/src/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  323. package/src/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  324. package/src/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  325. package/src/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  326. package/src/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  327. package/src/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  328. package/src/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  329. package/src/workflow/workflows/evolve/scripts/LICENSE +201 -0
  330. package/src/workflow/workflows/evolve/scripts/README.md +314 -0
  331. package/src/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  332. package/src/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  333. package/src/workflow/workflows/evolve/scripts/evolve-db +16 -0
  334. package/src/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  335. package/src/workflow/workflows/evolve/scripts/evolve-files +16 -0
  336. package/src/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  337. package/src/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  338. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  339. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  340. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  341. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  342. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  343. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  344. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  345. package/src/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  346. package/src/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  347. package/src/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  348. package/src/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  349. package/src/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  350. package/src/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  351. package/src/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  352. package/src/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  353. package/src/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  354. package/src/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  355. package/src/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  356. package/src/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  357. package/src/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  358. package/src/workflow/workflows/evolve/workflow.yaml +646 -0
  359. package/src/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  360. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  361. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  362. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  363. package/src/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  364. package/dist/web-ui-static/assets/index-BFO5z1js.js +0 -76
  365. package/dist/web-ui-static/assets/index-CCUwrkIL.css +0 -1
@@ -14,13 +14,14 @@ import { quote } from 'shell-quote';
14
14
  import { getBundleRuntimeRoot } from '../config/paths.js';
15
15
  import { getBundleShortId } from '../session/types.js';
16
16
  import { DEFAULT_CONTAINER_SCOPE } from '../workflow/types.js';
17
- import { CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
17
+ import { CONTAINER_SCRIPTS_DIR, CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
18
18
  import { parseUpstreamBaseUrl } from './provider-config.js';
19
19
  import { getInternalNetworkName } from './platform.js';
20
20
  import { cleanupContainers } from './container-lifecycle.js';
21
21
  import { clampDockerResources } from './resource-limits.js';
22
22
  import { errorMessage } from '../utils/error-message.js';
23
23
  import { createCachedStager } from '../skills/staging.js';
24
+ import { withProvisionLock } from './provision-lock.js';
24
25
  import * as logger from '../logger.js';
25
26
  /**
26
27
  * Create a bundle-owned directory and enforce 0o700 permissions even if
@@ -64,29 +65,19 @@ export function ensureSecureBundleDir(path) {
64
65
  }
65
66
  /** Hosts that use Anthropic OAuth credentials when available. */
66
67
  const ANTHROPIC_HOSTS = new Set(['api.anthropic.com', 'platform.claude.com']);
68
+ /** Hosts that use Codex ChatGPT OAuth credentials when available. */
69
+ const CODEX_CHATGPT_HOSTS = new Set(['chatgpt.com', 'auth.openai.com']);
67
70
  /** Prefix for container/sidecar names. Keep in sync with `docker ps` filters. */
68
71
  const CONTAINER_NAME_PREFIX = 'ironcurtain-';
69
72
  /** Host gateway alias used by Docker containers on macOS/Windows. */
70
73
  const DOCKER_HOST_GATEWAY = 'host.docker.internal';
71
74
  /** Bundle-relative subdir name for the skills staging dir. */
72
75
  const BUNDLE_SKILLS_SUBDIR = 'skills';
73
- /**
74
- * Prepares the shared (non-container) parts of Docker session infrastructure.
75
- *
76
- * Sets up proxies (Code Mode + MITM), CA, fake keys, orientation files, and
77
- * ensures the agent image. Does NOT create the agent container that step is
78
- * specific to the session mode: standalone sessions go through
79
- * `createDockerInfrastructure()` (which wraps this with `sleep infinity`
80
- * container creation); PTY sessions call this directly and then create their
81
- * own TTY-enabled container.
82
- *
83
- * `workflowId` drives Docker labelling: when set, the main container (and
84
- * any sidecar created during `createSessionContainers`) carries
85
- * `ironcurtain.workflow=<workflowId>` + `ironcurtain.scope=<scope>`
86
- * alongside the always-present `ironcurtain.bundle=<bundleId>`. When
87
- * unset, only `ironcurtain.bundle` is emitted (standalone CLI / PTY).
88
- */
89
- export async function prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills) {
76
+ /** Container path for runtime-provisioned workflow Python dependencies. */
77
+ const WORKFLOW_PYTHON_VENV_DIR = '/opt/workflow-venv';
78
+ /** Container path for runtime-provisioned workflow Node dependencies. */
79
+ const WORKFLOW_NODE_MODULES_DIR = '/opt/workflow-node_modules';
80
+ export async function prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir) {
90
81
  // The audit log path is read from config so the bundle is
91
82
  // self-describing: downstream consumers (AuditLogTailer, sandbox
92
83
  // coordinator) can take it from either `config.auditLogPath` or
@@ -98,16 +89,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
98
89
  const { createMitmProxy } = await import('./mitm-proxy.js');
99
90
  const { loadOrCreateCA } = await import('./ca.js');
100
91
  const { generateFakeKey } = await import('./fake-keys.js');
101
- const { createDockerManager } = await import('./docker-manager.js');
102
- const { useTcpTransport } = await import('./platform.js');
92
+ const { createContainerRuntime, resolveRuntimeKind } = await import('./container-runtime.js');
93
+ const { resolveNetworkTopology, createHostOnlyNetwork, makeSourceAddressGuard } = await import('./network-topology.js');
103
94
  const { getIronCurtainHome } = await import('../config/paths.js');
104
95
  const { prepareSession } = await import('./orientation.js');
105
- const { detectAuthMethod, writeToKeychain } = await import('./oauth-credentials.js');
96
+ const { detectAuthMethod, writeToKeychain, getCodexAuthFilePath, loadCodexOAuthCredentials, refreshCodexOAuthToken, refreshResultToCreds, saveCodexOAuthCredentials, } = await import('./oauth-credentials.js');
106
97
  const { OAuthTokenManager } = await import('./oauth-token-manager.js');
107
98
  const { getBundleSocketsDir, getBundleHostOnlyDir, getBundleProxySocketPath, getBundleMitmProxySocketPath, getBundleMitmControlSocketPath, } = await import('../config/paths.js');
108
99
  await registerBuiltinAdapters(config.userConfig);
109
100
  const adapter = getAgent(mode.agent);
110
- const useTcp = useTcpTransport();
101
+ const runtimeKind = await resolveRuntimeKind(config.userConfig.containerRuntime);
102
+ const topology = resolveNetworkTopology(runtimeKind);
103
+ const useTcp = topology !== 'uds';
111
104
  // Detect authentication method. Adapters with detectCredential() handle
112
105
  // their own credential detection (e.g., Goose checks provider-specific keys).
113
106
  // Adapters without it fall back to detectAuthMethod() (Anthropic OAuth + API key).
@@ -142,10 +135,26 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
142
135
  ensureSecureBundleDir(socketsDir);
143
136
  ensureSecureBundleDir(hostOnlyDir);
144
137
  const socketPath = getBundleProxySocketPath(bundleId);
138
+ const docker = createContainerRuntime(runtimeKind);
139
+ // tcp-hostonly: create the per-bundle host-only network BEFORE the
140
+ // proxies are constructed. The gateway address feeds the container env,
141
+ // the orientation proxy address, and the connection-source guard both
142
+ // proxies use while listening on 0.0.0.0 (the vmnet gateway interface
143
+ // only materializes once the first container attaches, so binding the
144
+ // gateway address directly is not possible at this point).
145
+ let hostOnlyNetwork;
146
+ let allowRemoteAddress;
147
+ if (topology === 'tcp-hostonly') {
148
+ hostOnlyNetwork = await createHostOnlyNetwork(docker, getInternalNetworkName(getBundleShortId(bundleId)));
149
+ allowRemoteAddress = makeSourceAddressGuard(hostOnlyNetwork.subnet);
150
+ logger.info(`Host-only network ${hostOnlyNetwork.name} (${hostOnlyNetwork.subnet}, gateway ${hostOnlyNetwork.gateway})`);
151
+ }
145
152
  const proxy = createCodeModeProxy({
146
153
  socketPath,
147
154
  config,
148
155
  listenMode: useTcp ? 'tcp' : 'uds',
156
+ bindHost: topology === 'tcp-hostonly' ? '0.0.0.0' : undefined,
157
+ allowRemoteAddress,
149
158
  });
150
159
  // Load or generate the IronCurtain CA for TLS termination
151
160
  const caDir = resolve(getIronCurtainHome(), 'ca');
@@ -158,8 +167,19 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
158
167
  const tokenManagerKeychainDeps = authMethod.kind === 'oauth' && authMethod.source === 'keychain'
159
168
  ? { writeToKeychain, keychainServiceName: authMethod.keychainServiceName }
160
169
  : undefined;
170
+ const tokenManagerCodexDeps = authMethod.kind === 'oauth' && adapter.id === 'codex'
171
+ ? {
172
+ loadCredentials: loadCodexOAuthCredentials,
173
+ refreshToken: async (rt) => refreshResultToCreds(await refreshCodexOAuthToken(rt)),
174
+ saveCredentials: saveCodexOAuthCredentials,
175
+ credentialsFilePath: getCodexAuthFilePath(),
176
+ }
177
+ : undefined;
161
178
  const tokenManager = authMethod.kind === 'oauth'
162
- ? new OAuthTokenManager(authMethod.credentials, { canRefresh: true }, tokenManagerKeychainDeps)
179
+ ? new OAuthTokenManager(authMethod.credentials, { canRefresh: canRefreshOAuth(authMethod.credentials.refreshToken) }, {
180
+ ...tokenManagerKeychainDeps,
181
+ ...tokenManagerCodexDeps,
182
+ })
163
183
  : undefined;
164
184
  const providers = adapter.getProviders(authKind);
165
185
  const resolvedProviders = applyUpstreamOverrides(providers, parseUpstreamBaseUrl, {
@@ -178,8 +198,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
178
198
  }
179
199
  fakeKeys.set(providerConfig.host, fakeKey);
180
200
  const realKey = resolveRealKey(providerConfig.host, config, oauthAccessToken);
181
- // Attach the token manager only to Anthropic hosts (the ones using OAuth)
182
- const hostTokenManager = tokenManager && ANTHROPIC_HOSTS.has(providerConfig.host) ? tokenManager : undefined;
201
+ const isManagedOAuthHost = ANTHROPIC_HOSTS.has(providerConfig.host) ||
202
+ (adapter.id === 'codex' && CODEX_CHATGPT_HOSTS.has(providerConfig.host));
203
+ const hostTokenManager = tokenManager && isManagedOAuthHost ? tokenManager : undefined;
183
204
  providerMappings.push({ config: providerConfig, fakeKey, realKey, tokenManager: hostTokenManager });
184
205
  }
185
206
  // Build package installation proxy config if enabled
@@ -187,9 +208,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
187
208
  let registries;
188
209
  let packageValidation;
189
210
  if (pkgConfig.enabled) {
190
- const { npmRegistry, pypiRegistry, debianRegistry } = await import('./registry-proxy.js');
211
+ const { npmRegistry, pypiRegistry, debianRegistry, cargoRegistry } = await import('./registry-proxy.js');
191
212
  const { createPackageValidator } = await import('./package-validator.js');
192
- registries = [npmRegistry, pypiRegistry, debianRegistry];
213
+ registries = [npmRegistry, pypiRegistry, debianRegistry, cargoRegistry];
193
214
  const validator = createPackageValidator({
194
215
  allowedPackages: pkgConfig.allowedPackages,
195
216
  deniedPackages: pkgConfig.deniedPackages,
@@ -208,6 +229,28 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
208
229
  // A workflow bundle serves only workflow agents for its entire lifetime,
209
230
  // so agentKind is fixed at construction time.
210
231
  const agentKind = workflowId !== undefined ? 'workflow' : undefined;
232
+ // Single resolution point for trajectory-capture enablement. The raw
233
+ // CLI/RPC override wins; otherwise fall through to config; otherwise
234
+ // off. Consumers pass the raw override only — they never re-resolve
235
+ // against `userConfig.capture?.enabled`.
236
+ const captureEnabled = captureInput ? (captureInput.override ?? config.userConfig.capture?.enabled ?? false) : false;
237
+ // Construct the trajectory-capture writer when capture is enabled.
238
+ // When disabled, no writer is created, no taps are installed, and the
239
+ // forwarding path is byte-identical to today (per §10 "zero cost when
240
+ // disabled").
241
+ let captureWriter;
242
+ if (captureEnabled && captureInput) {
243
+ const { createTrajectoryCaptureWriter } = await import('./trajectory-capture.js');
244
+ captureWriter = createTrajectoryCaptureWriter({ capturesDir: captureInput.capturesDir });
245
+ }
246
+ const captureProxyOptions = captureWriter
247
+ ? {
248
+ capture: captureWriter,
249
+ recordedAgentName: captureInput?.recordedAgentName,
250
+ workflowRunId: captureInput?.workflowRunId,
251
+ bundleId: String(bundleId),
252
+ }
253
+ : {};
211
254
  const mitmProxy = useTcp
212
255
  ? createMitmProxy({
213
256
  listenPort: 0,
@@ -216,8 +259,10 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
216
259
  registries,
217
260
  packageValidation,
218
261
  controlPort: 0,
219
- sessionId: routingId,
262
+ initialTokenSessionId: routingId,
220
263
  agentKind,
264
+ allowRemoteAddress,
265
+ ...captureProxyOptions,
221
266
  })
222
267
  : createMitmProxy({
223
268
  socketPath: getBundleMitmProxySocketPath(bundleId),
@@ -226,16 +271,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
226
271
  registries,
227
272
  packageValidation,
228
273
  controlSocketPath: getBundleMitmControlSocketPath(bundleId),
229
- sessionId: routingId,
274
+ initialTokenSessionId: routingId,
230
275
  agentKind,
276
+ ...captureProxyOptions,
231
277
  });
232
- const docker = createDockerManager();
233
278
  // Start MITM proxy FIRST so config.mitmControlAddr is set before proxy.start().
234
279
  // proxy.start() initializes the UTCP sandbox, which checks config.mitmControlAddr
235
280
  // to decide whether to register the proxy virtual MCP server for domain management.
236
281
  const mitmAddr = await mitmProxy.start();
237
282
  if (mitmAddr.port !== undefined) {
238
- logger.info(`MITM proxy listening on 127.0.0.1:${mitmAddr.port}`);
283
+ logger.info(hostOnlyNetwork
284
+ ? `MITM proxy listening on ${hostOnlyNetwork.gateway}:${mitmAddr.port} (0.0.0.0, subnet-guarded)`
285
+ : `MITM proxy listening on 127.0.0.1:${mitmAddr.port}`);
239
286
  }
240
287
  else {
241
288
  logger.info(`MITM proxy listening on ${mitmAddr.socketPath}`);
@@ -254,7 +301,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
254
301
  // registers the proxy virtual server for network domain management.
255
302
  await proxy.start();
256
303
  if (useTcp && proxy.port !== undefined) {
257
- logger.info(`Code Mode proxy listening on 127.0.0.1:${proxy.port}`);
304
+ logger.info(hostOnlyNetwork
305
+ ? `Code Mode proxy listening on ${hostOnlyNetwork.gateway}:${proxy.port} (0.0.0.0, subnet-guarded)`
306
+ : `Code Mode proxy listening on 127.0.0.1:${proxy.port}`);
258
307
  }
259
308
  else {
260
309
  logger.info(`Code Mode proxy listening on ${proxy.socketPath}`);
@@ -277,11 +326,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
277
326
  });
278
327
  }
279
328
  logger.info(`Available servers: ${serverListings.map((s) => s.name).join(', ')}`);
280
- const proxyAddress = useTcp && proxy.port !== undefined ? `${DOCKER_HOST_GATEWAY}:${proxy.port}` : undefined;
329
+ // The address the agent uses to reach the Code Mode proxy: the vmnet
330
+ // gateway on host-only networks, the Docker host alias otherwise.
331
+ const proxyHost = hostOnlyNetwork ? hostOnlyNetwork.gateway : DOCKER_HOST_GATEWAY;
332
+ const proxyAddress = useTcp && proxy.port !== undefined ? `${proxyHost}:${proxy.port}` : undefined;
281
333
  const { systemPrompt } = prepareSession(adapter, serverListings, bundleDir, config, workspaceDir, proxyAddress);
282
- // Ensure Docker image is built and up-to-date
283
- const image = await adapter.getImage();
284
- await ensureImage(image, docker, ca);
334
+ // Ensure the stock agent image is built and up-to-date. Workflow
335
+ // dependencies are provisioned at runtime into mounted caches below;
336
+ // they are no longer baked into per-workflow Docker images.
337
+ const agentImage = await adapter.getImage();
338
+ const agentBuildHash = await ensureImage(agentImage, docker, ca);
339
+ const image = agentImage;
340
+ const workflowDependencyMounts = prepareWorkflowDependencyMounts(agentBuildHash, scriptsDir, getIronCurtainHome());
285
341
  const orientationDir = resolve(bundleDir, 'orientation');
286
342
  // Set up conversation state directory if the adapter supports resume
287
343
  const conversationStateConfig = adapter.getConversationStateConfig?.();
@@ -312,6 +368,11 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
312
368
  logger.info(`Re-staged ${skills.length} skill(s) to ${skillsMount.hostDir}`);
313
369
  }
314
370
  };
371
+ let scriptsMount;
372
+ if (scriptsDir !== undefined && existsSync(scriptsDir)) {
373
+ scriptsMount = { hostDir: scriptsDir, target: CONTAINER_SCRIPTS_DIR };
374
+ logger.info(`Staged workflow scripts available at ${scriptsDir}`);
375
+ }
315
376
  return {
316
377
  bundleId,
317
378
  workflowId,
@@ -329,23 +390,62 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
329
390
  orientationDir,
330
391
  systemPrompt,
331
392
  image,
393
+ runtimeKind,
394
+ topology,
332
395
  useTcp,
396
+ hostOnlyNetwork,
333
397
  socketsDir,
334
398
  mitmAddr,
335
399
  authKind,
336
400
  conversationStateDir,
337
401
  conversationStateConfig,
338
402
  skillsMount,
403
+ scriptsMount,
404
+ ...workflowDependencyMounts,
339
405
  restageSkills,
340
406
  setTokenSessionId: (id) => {
341
407
  mitmProxy.setTokenSessionId(id);
342
408
  },
409
+ // Trajectory-capture lifecycle. When captureWriter is undefined
410
+ // (capture disabled, the common case), every method is a cheap
411
+ // no-op — zero cost on the forwarding path. When set, the bundle
412
+ // owns the three-step atomic begin (setCaptureSessionId →
413
+ // setCapturePersona → writer.beginSession) and the two-phase end
414
+ // (writer.endSession → null out proxy attribution).
415
+ beginCaptureSession: (opts) => {
416
+ if (!captureWriter)
417
+ return;
418
+ mitmProxy.setCaptureSessionId(opts.sessionId);
419
+ mitmProxy.setCapturePersona(opts.persona);
420
+ captureWriter.beginSession(opts);
421
+ },
422
+ endCaptureSession: async (sessionId) => {
423
+ if (!captureWriter)
424
+ return;
425
+ try {
426
+ await captureWriter.endSession(sessionId);
427
+ }
428
+ finally {
429
+ // Clear proxy attribution AFTER the drain settles so any
430
+ // late-arriving response chunks already in flight are still
431
+ // attributed to the correct session.
432
+ mitmProxy.setCaptureSessionId(undefined);
433
+ mitmProxy.setCapturePersona(undefined);
434
+ }
435
+ },
436
+ captureWriter,
343
437
  };
344
438
  }
345
439
  catch (error) {
346
440
  // Best-effort cleanup of proxies started above
347
441
  await mitmProxy.stop().catch(() => { });
348
442
  await proxy.stop().catch(() => { });
443
+ // Host-only network was created before the proxies; remove it too.
444
+ // (A leak through the narrow window before this catch is self-healing:
445
+ // createHostOnlyNetwork removes the stale same-named network first.)
446
+ if (hostOnlyNetwork) {
447
+ await docker.removeNetwork(hostOnlyNetwork.name).catch(() => { });
448
+ }
349
449
  throw error;
350
450
  }
351
451
  }
@@ -358,16 +458,27 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
358
458
  * failure after the proxies are started, all started resources are torn
359
459
  * down before the error propagates.
360
460
  */
361
- export async function createDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills) {
362
- const core = await prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills);
461
+ export async function createDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir, options) {
462
+ const core = await prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir);
463
+ let containerResources;
363
464
  try {
364
- const containerResources = await createSessionContainers(core, config);
365
- return { ...core, ...containerResources };
465
+ containerResources = await createSessionContainers(core, config, options);
466
+ const infra = { ...core, ...containerResources };
467
+ await provisionWorkflowDependencies(infra, config.userConfig.packageInstall.enabled);
468
+ return infra;
366
469
  }
367
470
  catch (error) {
368
471
  // Any partial container/sidecar/network cleanup happened inside
369
- // createSessionContainers(). Here we just tear down the proxies that
370
- // prepareDockerInfrastructure() started, to avoid leaking them.
472
+ // createSessionContainers(). If provisioning failed after a complete
473
+ // container bundle was created, clean that bundle here before tearing
474
+ // down the proxies.
475
+ if (containerResources) {
476
+ await cleanupContainers(core.docker, {
477
+ containerId: containerResources.containerId,
478
+ sidecarContainerId: containerResources.sidecarContainerId ?? null,
479
+ networkName: containerResources.internalNetwork ?? null,
480
+ });
481
+ }
371
482
  await core.mitmProxy.stop().catch(() => { });
372
483
  await core.proxy.stop().catch(() => { });
373
484
  throw error;
@@ -409,6 +520,17 @@ export async function destroyDockerInfrastructure(infra) {
409
520
  .stop()
410
521
  .catch((err) => logger.warn(`destroyDockerInfrastructure: proxy.stop() failed: ${errorMessage(err)}`)),
411
522
  ]);
523
+ // Trajectory-capture safety net (§9): close the writer AFTER the proxies
524
+ // have stopped, so no more records arrive mid-close. The writer emits
525
+ // synthetic `session-end` entries (with `closedReason:
526
+ // 'infrastructure-teardown'`) for any session whose explicit
527
+ // endCaptureSession was not called — covering Ctrl-C / abort / crash
528
+ // paths where the orchestrator's `finally` did not run.
529
+ if (infra.captureWriter) {
530
+ await infra.captureWriter
531
+ .close()
532
+ .catch((err) => logger.warn(`destroyDockerInfrastructure: captureWriter.close() failed: ${errorMessage(err)}`));
533
+ }
412
534
  // CA and fake keys are intentionally absent: neither owns any
413
535
  // process-level resources. CA material is persisted in ~/.ironcurtain/ca/
414
536
  // and reused across sessions; fake keys are just strings in a Map that
@@ -488,9 +610,9 @@ export function buildAgentUidRemap(useTcp) {
488
610
  *
489
611
  * Exported for testability: tests exercise the mount/env configuration and
490
612
  * the rollback-on-failure path by passing a mock `PreContainerInfrastructure`
491
- * with a scripted `DockerManager`.
613
+ * with a scripted `ContainerRuntime`.
492
614
  */
493
- export async function createSessionContainers(core, config) {
615
+ export async function createSessionContainers(core, config, options) {
494
616
  const shortId = getBundleShortId(core.bundleId);
495
617
  const mainContainerName = `${CONTAINER_NAME_PREFIX}${shortId}`;
496
618
  // Labels applied to every IronCurtain-owned container (main + sidecar).
@@ -507,6 +629,9 @@ export async function createSessionContainers(core, config) {
507
629
  let sidecarContainerId;
508
630
  let internalNetwork;
509
631
  try {
632
+ const mainImage = options?.baseImageOverride && (await core.docker.imageExists(options.baseImageOverride))
633
+ ? options.baseImageOverride
634
+ : core.image;
510
635
  // Base mounts shared by TCP and UDS modes: the sandbox as the
511
636
  // workspace and the orientation dir. Mode-specific mounts (apt proxy
512
637
  // config, sockets dir, conversation state) are appended below.
@@ -519,7 +644,34 @@ export async function createSessionContainers(core, config) {
519
644
  };
520
645
  let network;
521
646
  let extraHosts;
522
- if (core.useTcp && core.mitmAddr.port !== undefined && core.proxy.port !== undefined) {
647
+ // tcp-hostonly only: apt proxy config to write into the container via
648
+ // exec after start. Apple container's virtiofs shares directories
649
+ // only — the single-file bind mount the Docker topologies use for
650
+ // /etc/apt/apt.conf.d/90-ironcurtain-proxy is rejected with
651
+ // "path ... is not a directory".
652
+ let hostOnlyAptProxyUrl;
653
+ if (core.topology === 'tcp-hostonly') {
654
+ // Apple container host-only mode: the agent VM reaches the host
655
+ // proxies directly at the vmnet gateway address. No sidecar, no
656
+ // extra host mappings — egress is blocked at the network layer
657
+ // (`--internal`) and verified by the connectivity check below.
658
+ if (core.hostOnlyNetwork === undefined || core.mitmAddr.port === undefined || core.proxy.port === undefined) {
659
+ throw new Error('tcp-hostonly bundle is missing its host-only network or proxy ports');
660
+ }
661
+ const proxyUrl = `http://${core.hostOnlyNetwork.gateway}:${core.mitmAddr.port}`;
662
+ env = {
663
+ ...env,
664
+ HTTPS_PROXY: proxyUrl,
665
+ HTTP_PROXY: proxyUrl,
666
+ };
667
+ hostOnlyAptProxyUrl = proxyUrl;
668
+ network = core.hostOnlyNetwork.name;
669
+ // Report the host-only network as `internalNetwork` so the standard
670
+ // teardown paths (destroyDockerInfrastructure, rollback below)
671
+ // remove it with the containers.
672
+ internalNetwork = core.hostOnlyNetwork.name;
673
+ }
674
+ else if (core.topology === 'tcp-sidecar' && core.mitmAddr.port !== undefined && core.proxy.port !== undefined) {
523
675
  // macOS TCP mode: internal bridge network blocks egress.
524
676
  // A socat sidecar bridges the internal network to the host
525
677
  // because Docker Desktop VMs don't forward gateway traffic.
@@ -614,6 +766,23 @@ export async function createSessionContainers(core, config) {
614
766
  if (core.skillsMount) {
615
767
  mounts.push({ source: core.skillsMount.hostDir, target: core.skillsMount.target, readonly: true });
616
768
  }
769
+ if (core.scriptsMount) {
770
+ mounts.push({ source: core.scriptsMount.hostDir, target: core.scriptsMount.target, readonly: true });
771
+ }
772
+ if (core.workflowPythonVenvMount) {
773
+ mounts.push({
774
+ source: core.workflowPythonVenvMount.hostDir,
775
+ target: core.workflowPythonVenvMount.target,
776
+ readonly: false,
777
+ });
778
+ }
779
+ if (core.workflowNodeModulesMount) {
780
+ mounts.push({
781
+ source: core.workflowNodeModulesMount.hostDir,
782
+ target: core.workflowNodeModulesMount.target,
783
+ readonly: false,
784
+ });
785
+ }
617
786
  // Linux-only UID-remap wiring (issue #232). On Linux, run the
618
787
  // container as root and pass the host UID/GID via env so the
619
788
  // entrypoint can renumber codespace before dropping privileges.
@@ -627,11 +796,22 @@ export async function createSessionContainers(core, config) {
627
796
  // "no flag emitted" (see clampDockerResources docs).
628
797
  const { effective: containerResources } = clampDockerResources(config.userConfig.dockerResources);
629
798
  mainContainerId = await core.docker.create({
630
- image: core.image,
799
+ image: mainImage,
631
800
  name: mainContainerName,
632
801
  network: network ?? 'none',
633
802
  mounts,
634
- env: { ...env, ...uidRemap.env },
803
+ env: {
804
+ ...env,
805
+ // Do NOT override PATH here. Docker `-e PATH=...` REPLACES the image's
806
+ // PATH (it does not append), which would discard the base image's real
807
+ // PATH — including the NVM directory where `node`/`npm` live on the x86
808
+ // devcontainer base. Bare-`node` workflow helpers would then fail to
809
+ // resolve. The workflow venv bin is instead prepended to the live
810
+ // `$PATH` at exec time (see buildWorkflowExecCommand), which is
811
+ // base-image-agnostic and preserves the image's own PATH.
812
+ ...(core.workflowNodeModulesMount ? { NODE_PATH: core.workflowNodeModulesMount.target } : {}),
813
+ ...uidRemap.env,
814
+ },
635
815
  user: uidRemap.user,
636
816
  command: ['sleep', 'infinity'],
637
817
  ...bundleLabels,
@@ -648,9 +828,19 @@ export async function createSessionContainers(core, config) {
648
828
  });
649
829
  await core.docker.start(mainContainerId);
650
830
  logger.info(`Container started: ${mainContainerId.substring(0, 12)}`);
831
+ // tcp-hostonly: write the apt proxy config inside the container (the
832
+ // Docker topologies bind-mount it; see hostOnlyAptProxyUrl above).
833
+ if (hostOnlyAptProxyUrl !== undefined) {
834
+ await writeHostOnlyAptProxyConfig(core.docker, mainContainerId, hostOnlyAptProxyUrl);
835
+ }
651
836
  // Connectivity check: verify the container can reach host proxies
652
- // through the internal network. Abort if unreachable.
653
- if (core.useTcp && internalNetwork !== undefined && core.proxy.port !== undefined) {
837
+ // through the internal network. Abort if unreachable. Host-only
838
+ // bundles additionally assert the inverse internet egress must be
839
+ // blocked — and never fall back to a weaker configuration.
840
+ if (core.topology === 'tcp-hostonly' && core.hostOnlyNetwork !== undefined && core.proxy.port !== undefined) {
841
+ await checkHostOnlyConnectivity(core.docker, mainContainerId, core.hostOnlyNetwork.gateway, core.proxy.port);
842
+ }
843
+ else if (core.useTcp && internalNetwork !== undefined && core.proxy.port !== undefined) {
654
844
  await checkInternalNetworkConnectivity(core.docker, mainContainerId, core.proxy.port);
655
845
  }
656
846
  return {
@@ -687,16 +877,76 @@ async function checkInternalNetworkConnectivity(docker, containerId, mcpPort) {
687
877
  `Check that the sidecar container is running and connected to the internal network.`);
688
878
  }
689
879
  }
880
+ /**
881
+ * Writes /etc/apt/apt.conf.d/90-ironcurtain-proxy inside a running
882
+ * container via exec (as root). Used by the tcp-hostonly topology in
883
+ * both batch and PTY modes — Apple container's virtiofs shares
884
+ * directories only, so the single-file bind mount the Docker topologies
885
+ * use is rejected. The URL is built from our own gateway address and
886
+ * OS-assigned port — runtime-generated values, not untrusted input — so
887
+ * embedding it in the sh script is safe.
888
+ */
889
+ export async function writeHostOnlyAptProxyConfig(docker, containerId, proxyUrl) {
890
+ const aptWrite = await docker.exec(containerId, [
891
+ 'sh',
892
+ '-c',
893
+ `printf 'Acquire::http::Proxy "%s";\\nAcquire::https::Proxy "%s";\\n' '${proxyUrl}' '${proxyUrl}' > /etc/apt/apt.conf.d/90-ironcurtain-proxy`,
894
+ ], 10_000, 'root');
895
+ if (aptWrite.exitCode !== 0) {
896
+ throw new Error(`Failed to write apt proxy config in container (exit=${aptWrite.exitCode}): ${aptWrite.stderr}`);
897
+ }
898
+ }
899
+ /**
900
+ * External address used to probe that internet egress is blocked. Any
901
+ * globally-routable address works; the check asserts the connection
902
+ * FAILS, so the probe never carries data off the machine on a healthy
903
+ * setup.
904
+ */
905
+ const EGRESS_PROBE_ADDRESS = '1.1.1.1:443';
906
+ /**
907
+ * Fail-closed startup gate for the tcp-hostonly topology
908
+ * (docs/designs/apple-container-runtime.md, design decision 4). Asserts
909
+ * from inside the container that (a) the host-side proxies are reachable
910
+ * at the vmnet gateway and (b) internet egress is blocked by the
911
+ * host-only network. Either failure aborts session initialization —
912
+ * never a silent fallback to a weaker network configuration. Shared by
913
+ * batch (`createSessionContainers`) and PTY (`runPtySession`) modes.
914
+ */
915
+ export async function checkHostOnlyConnectivity(docker, containerId, gateway, mcpPort) {
916
+ const reach = await docker.exec(containerId, ['socat', '-u', '/dev/null', `TCP:${gateway}:${mcpPort},connect-timeout=5`], 6_000);
917
+ if (reach.exitCode !== 0) {
918
+ throw new Error(`Host-only network connectivity check failed (exit=${reach.exitCode}). ` +
919
+ `The container cannot reach host-side proxies at gateway ${gateway}:${mcpPort}. ` +
920
+ `Check that the host-only network exists and the proxies are listening.`);
921
+ }
922
+ const egress = await docker.exec(containerId, ['socat', '-u', '/dev/null', `TCP:${EGRESS_PROBE_ADDRESS},connect-timeout=3`], 5_000);
923
+ if (egress.exitCode === 0) {
924
+ throw new Error(`Host-only network egress check failed: the container reached ${EGRESS_PROBE_ADDRESS}. ` +
925
+ `The network is not blocking internet egress as required; refusing to start the session.`);
926
+ }
927
+ }
928
+ /**
929
+ * Whether an OAuth credential set can be refreshed: true only when a
930
+ * non-empty refresh token is present. Externally-managed Codex tokens
931
+ * (`auth_mode: 'chatgptAuthTokens'`) carry an empty refresh token and must
932
+ * NOT be refreshed by IronCurtain. Pure helper, exported for testability.
933
+ */
934
+ export function canRefreshOAuth(refreshToken) {
935
+ return refreshToken.length > 0;
936
+ }
690
937
  /**
691
938
  * Resolves the real credential for a provider host.
692
939
  *
693
940
  * For Anthropic hosts in OAuth mode, uses the OAuth access token.
694
941
  * For all other cases, falls back to the API key from config.
695
942
  */
696
- function resolveRealKey(host, config, oauthAccessToken) {
943
+ export function resolveRealKey(host, config, oauthAccessToken) {
697
944
  if (oauthAccessToken && ANTHROPIC_HOSTS.has(host)) {
698
945
  return oauthAccessToken;
699
946
  }
947
+ if (oauthAccessToken && CODEX_CHATGPT_HOSTS.has(host)) {
948
+ return oauthAccessToken;
949
+ }
700
950
  let key;
701
951
  switch (host) {
702
952
  case 'api.anthropic.com':
@@ -706,6 +956,10 @@ function resolveRealKey(host, config, oauthAccessToken) {
706
956
  case 'api.openai.com':
707
957
  key = config.userConfig.openaiApiKey;
708
958
  break;
959
+ case 'chatgpt.com':
960
+ case 'auth.openai.com':
961
+ key = '';
962
+ break;
709
963
  case 'generativelanguage.googleapis.com':
710
964
  key = config.userConfig.googleApiKey;
711
965
  break;
@@ -723,9 +977,13 @@ function resolveRealKey(host, config, oauthAccessToken) {
723
977
  * support session resume. Idempotent: skips seeding if the directory
724
978
  * already exists (resume case).
725
979
  *
726
- * As a defense-in-depth measure, always deletes `.credentials.json`
727
- * from the state directory the MITM proxy handles auth independently,
728
- * so any credentials file left by the agent is stale.
980
+ * As a defense-in-depth measure, always deletes stale credential files
981
+ * (`.credentials.json` for Claude Code, `auth.json` for Codex) from the
982
+ * state directory the MITM proxy handles auth independently, and each
983
+ * agent's entrypoint recreates its credential file from env on every
984
+ * start, so any credential file lingering across resumes is stale. The
985
+ * unlinks are no-ops for adapters whose state dir has neither file (e.g.
986
+ * Goose), since a missing file is swallowed.
729
987
  */
730
988
  export function prepareConversationStateDir(sessionDir, config) {
731
989
  const stateDir = resolve(sessionDir, config.hostDirName);
@@ -752,13 +1010,16 @@ export function prepareConversationStateDir(sessionDir, config) {
752
1010
  }
753
1011
  }
754
1012
  }
755
- // Defense-in-depth: remove stale credentials on every start
756
- const credentialsPath = resolve(stateDir, '.credentials.json');
757
- try {
758
- unlinkSync(credentialsPath);
759
- }
760
- catch {
761
- // File doesn't exist — expected on first run
1013
+ // Defense-in-depth: remove stale credential files on every start. The
1014
+ // entrypoint recreates them from env each start, so scrubbing host-side
1015
+ // is safe. Both unlinks are no-ops when the file is absent.
1016
+ for (const fileName of ['.credentials.json', 'auth.json']) {
1017
+ try {
1018
+ unlinkSync(resolve(stateDir, fileName));
1019
+ }
1020
+ catch {
1021
+ // File doesn't exist — expected on first run / other adapters
1022
+ }
762
1023
  }
763
1024
  return stateDir;
764
1025
  }
@@ -777,12 +1038,12 @@ export function prepareConversationStateDir(sessionDir, config) {
777
1038
  export async function ensureDockerImage(agentId, userConfig) {
778
1039
  const { registerBuiltinAdapters, getAgent } = await import('./agent-registry.js');
779
1040
  const { loadOrCreateCA } = await import('./ca.js');
780
- const { createDockerManager } = await import('./docker-manager.js');
1041
+ const { createContainerRuntime, resolveRuntimeKind } = await import('./container-runtime.js');
781
1042
  const { getIronCurtainHome } = await import('../config/paths.js');
782
1043
  await registerBuiltinAdapters(userConfig);
783
1044
  const adapter = getAgent(agentId);
784
1045
  const image = await adapter.getImage();
785
- const docker = createDockerManager();
1046
+ const docker = createContainerRuntime(await resolveRuntimeKind(userConfig.containerRuntime));
786
1047
  const ca = loadOrCreateCA(resolve(getIronCurtainHome(), 'ca'));
787
1048
  await ensureImage(image, docker, ca);
788
1049
  }
@@ -792,7 +1053,33 @@ export async function ensureDockerImage(agentId, userConfig) {
792
1053
  * agent-specific image. Content-hash labels on each image drive staleness
793
1054
  * detection so repeated calls skip rebuilds when nothing has changed.
794
1055
  */
795
- async function ensureImage(image, docker, ca) {
1056
+ /**
1057
+ * Builds `image` from a fresh temp directory populated with the contents of
1058
+ * `dockerDir` (plus any `extraFiles`, keyed dest→src). Building from a clean
1059
+ * dir outside any git repo is REQUIRED for Apple `container build`, which
1060
+ * resolves an EMPTY context when handed a git-tracked source directory (the
1061
+ * repo's docker/ in a checkout/worktree) — making `COPY` fail with "not
1062
+ * found"; harmless on Docker. The Dockerfiles only COPY files that live in
1063
+ * `dockerDir` / `extraFiles`.
1064
+ */
1065
+ async function buildImageFromCleanContext(docker, image, dockerDir, dockerfile, labels, extraFiles = {}) {
1066
+ const tmpContext = mkdtempSync(resolve(tmpdir(), 'ironcurtain-build-'));
1067
+ try {
1068
+ for (const file of readdirSync(dockerDir)) {
1069
+ copyFileSync(resolve(dockerDir, file), resolve(tmpContext, file));
1070
+ }
1071
+ for (const [dest, src] of Object.entries(extraFiles)) {
1072
+ copyFileSync(src, resolve(tmpContext, dest));
1073
+ }
1074
+ await docker.buildImage(image, resolve(tmpContext, dockerfile), tmpContext, labels);
1075
+ }
1076
+ finally {
1077
+ rmSync(tmpContext, { recursive: true, force: true });
1078
+ }
1079
+ }
1080
+ // `docker` is typed `ContainerRuntime` (the apple-container generalization of
1081
+ // the former `DockerManager`); exported because callers/tests import it.
1082
+ export async function ensureImage(image, docker, ca) {
796
1083
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
797
1084
  const dockerDir = resolve(packageRoot, 'docker');
798
1085
  // On arm64 hosts (Apple Silicon), use the lightweight arm64-native Dockerfile
@@ -814,29 +1101,186 @@ async function ensureImage(image, docker, ca) {
814
1101
  const needsAgentBuild = baseRebuilt || (await isImageStale(image, docker, agentBuildHash));
815
1102
  if (needsAgentBuild) {
816
1103
  logger.info(`Building Docker image ${image}...`);
817
- await docker.buildImage(image, agentDockerfilePath, dockerDir, {
1104
+ await buildImageFromCleanContext(docker, image, dockerDir, dockerfile, {
818
1105
  'ironcurtain.build-hash': agentBuildHash,
819
1106
  });
820
1107
  logger.info(`Docker image ${image} built successfully`);
821
1108
  }
1109
+ return agentBuildHash;
1110
+ }
1111
+ export function computeWorkflowDependencyHash(agentBuildHash, scriptsDir) {
1112
+ const hash = createHash('sha256');
1113
+ hash.update(`agent:${agentBuildHash}\n`);
1114
+ for (const manifest of ['requirements.txt', 'package.json', 'package-lock.json']) {
1115
+ const manifestPath = resolve(scriptsDir, manifest);
1116
+ if (!existsSync(manifestPath))
1117
+ continue;
1118
+ hash.update(`file:${manifest}\n`);
1119
+ hash.update(readFileSync(manifestPath));
1120
+ hash.update('\n');
1121
+ }
1122
+ return hash.digest('hex');
1123
+ }
1124
+ /**
1125
+ * Wraps an in-container command so the workflow dependency bins are prepended
1126
+ * to the container's LIVE `$PATH` at exec time, rather than replacing the
1127
+ * image's PATH at container-creation time.
1128
+ *
1129
+ * Why a runtime shell instead of an `-e PATH=...` env: Docker `-e` REPLACES the
1130
+ * image PATH (it does not append), which would discard the base image's own
1131
+ * PATH — including the NVM directory where `node`/`npm` live on the x86
1132
+ * devcontainer base. Expanding `$PATH` inside the container at exec time is
1133
+ * base-image-agnostic: it preserves whatever PATH the image ships and merely
1134
+ * prepends the workflow venv bin (for bare `python`) and the installed Node
1135
+ * package bins (`node_modules/.bin`).
1136
+ *
1137
+ * Returns the original command unchanged when neither dependency mount is
1138
+ * present, so non-dependency workflows keep the plain exec path.
1139
+ *
1140
+ * Shell-safety: the only interpolated values are the hardcoded container
1141
+ * constants `WORKFLOW_PYTHON_VENV_DIR` / `WORKFLOW_NODE_MODULES_DIR`. The
1142
+ * caller's command and its arguments are passed verbatim as positional
1143
+ * parameters consumed by `exec "$@"` — never string-interpolated — so no
1144
+ * word-splitting or injection is possible.
1145
+ */
1146
+ export function buildWorkflowExecCommand(bundle, command) {
1147
+ const prefixDirs = [];
1148
+ if (bundle.workflowPythonVenvMount)
1149
+ prefixDirs.push(`${WORKFLOW_PYTHON_VENV_DIR}/bin`);
1150
+ if (bundle.workflowNodeModulesMount)
1151
+ prefixDirs.push(`${WORKFLOW_NODE_MODULES_DIR}/.bin`);
1152
+ if (prefixDirs.length === 0 || command.length === 0)
1153
+ return command;
1154
+ const pathPrefix = prefixDirs.join(':');
1155
+ // `exec "$@"` runs the original argv verbatim; the leading `sh` is $0.
1156
+ return ['/bin/sh', '-lc', `export PATH=${pathPrefix}:"$PATH"; exec "$@"`, 'sh', ...command];
1157
+ }
1158
+ function prepareWorkflowDependencyMounts(agentBuildHash, scriptsDir, ironcurtainHome) {
1159
+ if (scriptsDir === undefined || !existsSync(scriptsDir))
1160
+ return {};
1161
+ const requirementsPath = resolve(scriptsDir, 'requirements.txt');
1162
+ const packageJsonPath = resolve(scriptsDir, 'package.json');
1163
+ const packageLockPath = resolve(scriptsDir, 'package-lock.json');
1164
+ const hasPythonManifest = existsSync(requirementsPath);
1165
+ const hasNodeManifest = existsSync(packageJsonPath);
1166
+ if (!hasPythonManifest && !hasNodeManifest)
1167
+ return {};
1168
+ const dependencyHash = computeWorkflowDependencyHash(agentBuildHash, scriptsDir);
1169
+ const cacheRoot = resolve(ironcurtainHome, 'workflow-deps', dependencyHash.slice(0, 24));
1170
+ mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
1171
+ let workflowPythonVenvMount;
1172
+ let workflowNodeModulesMount;
1173
+ if (hasPythonManifest) {
1174
+ const hostDir = resolve(cacheRoot, 'python-venv');
1175
+ mkdirSync(hostDir, { recursive: true, mode: 0o700 });
1176
+ workflowPythonVenvMount = {
1177
+ hostDir,
1178
+ target: WORKFLOW_PYTHON_VENV_DIR,
1179
+ cacheKey: dependencyHash,
1180
+ };
1181
+ }
1182
+ if (hasNodeManifest) {
1183
+ const hostDir = resolve(cacheRoot, 'node_modules');
1184
+ mkdirSync(hostDir, { recursive: true, mode: 0o700 });
1185
+ workflowNodeModulesMount = {
1186
+ hostDir,
1187
+ target: WORKFLOW_NODE_MODULES_DIR,
1188
+ cacheKey: dependencyHash,
1189
+ hasPackageLock: existsSync(packageLockPath),
1190
+ };
1191
+ }
1192
+ return {
1193
+ ...(workflowPythonVenvMount ? { workflowPythonVenvMount } : {}),
1194
+ ...(workflowNodeModulesMount ? { workflowNodeModulesMount } : {}),
1195
+ };
1196
+ }
1197
+ async function provisionWorkflowDependencies(infra, packageInstallEnabled) {
1198
+ if (!infra.workflowPythonVenvMount && !infra.workflowNodeModulesMount)
1199
+ return;
1200
+ // Runtime provisioning installs through the MITM registry proxy, which is
1201
+ // only wired when packageInstall is enabled (see prepareDockerInfrastructure
1202
+ // — `registries`/`packageValidation` are left undefined otherwise). The
1203
+ // mounts above only exist when the workflow actually ships a
1204
+ // requirements.txt / package.json, so reaching here with package install
1205
+ // disabled means the run genuinely needs deps it can never fetch under
1206
+ // `--network=none`. Fail fast with an actionable message rather than letting
1207
+ // `uv pip install` / `npm install` die with an opaque network error.
1208
+ if (!packageInstallEnabled) {
1209
+ throw new Error('This workflow requires installing dependencies at runtime ' +
1210
+ '(a requirements.txt and/or package.json is present in its scripts), ' +
1211
+ 'but packageInstall is disabled. Enable packageInstall in your IronCurtain ' +
1212
+ 'config to run workflows that declare runtime dependencies.');
1213
+ }
1214
+ if (infra.workflowPythonVenvMount) {
1215
+ await provisionWorkflowPythonDependencies(infra);
1216
+ }
1217
+ if (infra.workflowNodeModulesMount) {
1218
+ await provisionWorkflowNodeDependencies(infra);
1219
+ }
1220
+ }
1221
+ async function provisionWorkflowPythonDependencies(infra) {
1222
+ const mount = infra.workflowPythonVenvMount;
1223
+ if (!mount)
1224
+ return;
1225
+ const sentinel = `${mount.target}/.ironcurtain-provisioned-${mount.cacheKey}`;
1226
+ const command = [
1227
+ 'set -eu',
1228
+ `if [ -f ${quote([sentinel])} ]; then exit 0; fi`,
1229
+ `find ${quote([mount.target])} -mindepth 1 -maxdepth 1 -exec rm -rf {} +`,
1230
+ `UV_NATIVE_TLS=1 uv venv ${quote([mount.target])}`,
1231
+ `VIRTUAL_ENV=${quote([mount.target])} UV_NATIVE_TLS=1 uv pip install -r ${quote([`${CONTAINER_SCRIPTS_DIR}/requirements.txt`])}`,
1232
+ `touch ${quote([sentinel])}`,
1233
+ ].join('\n');
1234
+ // Serialize concurrent provisioning of this content-keyed cache across runs.
1235
+ // The lock is host-side because in-container flock does not propagate across
1236
+ // containers on a Docker Desktop bind mount; the in-shell sentinel check
1237
+ // provides the short-circuit, the host lock makes the second run wait for the
1238
+ // first so it observes the populated cache instead of racing on the clean.
1239
+ await withProvisionLock(mount.hostDir, async () => {
1240
+ logger.info(`Provisioning workflow Python dependencies into ${mount.target}`);
1241
+ const result = await infra.docker.exec(infra.containerId, ['/bin/sh', '-lc', command], 1_200_000, 'codespace', CONTAINER_WORKSPACE_DIR);
1242
+ if (result.exitCode !== 0) {
1243
+ throw new Error(`Workflow Python dependency provisioning failed: ${result.stderr || result.stdout}`);
1244
+ }
1245
+ });
1246
+ }
1247
+ async function provisionWorkflowNodeDependencies(infra) {
1248
+ const mount = infra.workflowNodeModulesMount;
1249
+ if (!mount)
1250
+ return;
1251
+ const sentinel = `${mount.target}/.ironcurtain-provisioned-${mount.cacheKey}`;
1252
+ const installCommand = mount.hasPackageLock ? 'npm ci --omit=dev' : 'npm install --omit=dev';
1253
+ const command = [
1254
+ 'set -eu',
1255
+ `if [ -f ${quote([sentinel])} ]; then exit 0; fi`,
1256
+ 'tmp="$(mktemp -d)"',
1257
+ 'cleanup() { rm -rf "$tmp"; }',
1258
+ 'trap cleanup EXIT',
1259
+ `cp ${quote([`${CONTAINER_SCRIPTS_DIR}/package.json`])} "$tmp/package.json"`,
1260
+ mount.hasPackageLock ? `cp ${quote([`${CONTAINER_SCRIPTS_DIR}/package-lock.json`])} "$tmp/package-lock.json"` : '',
1261
+ 'cd "$tmp"',
1262
+ installCommand,
1263
+ `find ${quote([mount.target])} -mindepth 1 -maxdepth 1 -exec rm -rf {} +`,
1264
+ `cp -a node_modules/. ${quote([mount.target])}/`,
1265
+ `touch ${quote([sentinel])}`,
1266
+ ]
1267
+ .filter(Boolean)
1268
+ .join('\n');
1269
+ // Host-side serialization of this content-keyed cache (see the Python path
1270
+ // for the bind-mount-flock rationale).
1271
+ await withProvisionLock(mount.hostDir, async () => {
1272
+ logger.info(`Provisioning workflow Node dependencies into ${mount.target}`);
1273
+ const result = await infra.docker.exec(infra.containerId, ['/bin/sh', '-lc', command], 1_200_000, 'codespace', CONTAINER_WORKSPACE_DIR);
1274
+ if (result.exitCode !== 0) {
1275
+ throw new Error(`Workflow Node dependency provisioning failed: ${result.stderr || result.stdout}`);
1276
+ }
1277
+ });
822
1278
  }
823
1279
  async function ensureBaseImage(baseImage, docker, ca, dockerDir, dockerfile, buildHash) {
824
1280
  if (!(await isImageStale(baseImage, docker, buildHash)))
825
1281
  return false;
826
1282
  logger.info('Building base Docker image (this may take a while on first run)...');
827
- const tmpContext = mkdtempSync(resolve(tmpdir(), 'ironcurtain-build-'));
828
- try {
829
- for (const file of readdirSync(dockerDir)) {
830
- copyFileSync(resolve(dockerDir, file), resolve(tmpContext, file));
831
- }
832
- copyFileSync(ca.certPath, resolve(tmpContext, 'ironcurtain-ca-cert.pem'));
833
- await docker.buildImage(baseImage, resolve(tmpContext, dockerfile), tmpContext, {
834
- 'ironcurtain.build-hash': buildHash,
835
- });
836
- }
837
- finally {
838
- rmSync(tmpContext, { recursive: true, force: true });
839
- }
1283
+ await buildImageFromCleanContext(docker, baseImage, dockerDir, dockerfile, { 'ironcurtain.build-hash': buildHash }, { 'ironcurtain-ca-cert.pem': ca.certPath });
840
1284
  logger.info('Base Docker image built successfully');
841
1285
  return true;
842
1286
  }