@provos/ironcurtain 0.11.0 → 0.13.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 (472) hide show
  1. package/README.md +23 -19
  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 +14 -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.d.ts +33 -1
  11. package/dist/config/config-command.js +723 -11
  12. package/dist/config/config-command.js.map +1 -1
  13. package/dist/config/first-start.js +2 -8
  14. package/dist/config/first-start.js.map +1 -1
  15. package/dist/config/model-provider.d.ts +9 -0
  16. package/dist/config/model-provider.js +13 -0
  17. package/dist/config/model-provider.js.map +1 -1
  18. package/dist/config/openrouter-catalog.d.ts +46 -0
  19. package/dist/config/openrouter-catalog.js +111 -0
  20. package/dist/config/openrouter-catalog.js.map +1 -0
  21. package/dist/config/openrouter-models-fallback.d.ts +15 -0
  22. package/dist/config/openrouter-models-fallback.js +49 -0
  23. package/dist/config/openrouter-models-fallback.js.map +1 -0
  24. package/dist/config/paths.d.ts +48 -6
  25. package/dist/config/paths.js +63 -8
  26. package/dist/config/paths.js.map +1 -1
  27. package/dist/config/types.d.ts +8 -0
  28. package/dist/config/user-config.d.ts +179 -6
  29. package/dist/config/user-config.js +282 -5
  30. package/dist/config/user-config.js.map +1 -1
  31. package/dist/daemon/daemon-command.js +14 -5
  32. package/dist/daemon/daemon-command.js.map +1 -1
  33. package/dist/daemon/ironcurtain-daemon.d.ts +18 -0
  34. package/dist/daemon/ironcurtain-daemon.js +28 -1
  35. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  36. package/dist/daemon-client/daemon-client.d.ts +123 -0
  37. package/dist/daemon-client/daemon-client.js +324 -0
  38. package/dist/daemon-client/daemon-client.js.map +1 -0
  39. package/dist/docker/adapters/claude-code.js +109 -1
  40. package/dist/docker/adapters/claude-code.js.map +1 -1
  41. package/dist/docker/adapters/codex.d.ts +18 -0
  42. package/dist/docker/adapters/codex.js +376 -0
  43. package/dist/docker/adapters/codex.js.map +1 -0
  44. package/dist/docker/adapters/goose.js +38 -2
  45. package/dist/docker/adapters/goose.js.map +1 -1
  46. package/dist/docker/agent-adapter.d.ts +34 -6
  47. package/dist/docker/agent-adapter.js +2 -0
  48. package/dist/docker/agent-adapter.js.map +1 -1
  49. package/dist/docker/agent-registry.js +5 -0
  50. package/dist/docker/agent-registry.js.map +1 -1
  51. package/dist/docker/apple-container-manager.d.ts +49 -0
  52. package/dist/docker/apple-container-manager.js +486 -0
  53. package/dist/docker/apple-container-manager.js.map +1 -0
  54. package/dist/docker/ca.js +9 -2
  55. package/dist/docker/ca.js.map +1 -1
  56. package/dist/docker/code-mode-proxy.d.ts +11 -0
  57. package/dist/docker/code-mode-proxy.js +3 -1
  58. package/dist/docker/code-mode-proxy.js.map +1 -1
  59. package/dist/docker/container-lifecycle.d.ts +2 -2
  60. package/dist/docker/container-lifecycle.js +9 -0
  61. package/dist/docker/container-lifecycle.js.map +1 -1
  62. package/dist/docker/container-runtime.d.ts +29 -0
  63. package/dist/docker/container-runtime.js +76 -0
  64. package/dist/docker/container-runtime.js.map +1 -0
  65. package/dist/docker/docker-agent-session.d.ts +31 -0
  66. package/dist/docker/docker-agent-session.js +97 -7
  67. package/dist/docker/docker-agent-session.js.map +1 -1
  68. package/dist/docker/docker-infrastructure.d.ts +232 -15
  69. package/dist/docker/docker-infrastructure.js +762 -118
  70. package/dist/docker/docker-infrastructure.js.map +1 -1
  71. package/dist/docker/docker-manager.d.ts +25 -5
  72. package/dist/docker/docker-manager.js +388 -21
  73. package/dist/docker/docker-manager.js.map +1 -1
  74. package/dist/docker/docker-probe.d.ts +45 -0
  75. package/dist/docker/docker-probe.js +86 -0
  76. package/dist/docker/docker-probe.js.map +1 -0
  77. package/dist/docker/docker-progress-sink.d.ts +1 -1
  78. package/dist/docker/docker-resource-lifecycle.d.ts +71 -0
  79. package/dist/docker/docker-resource-lifecycle.js +454 -0
  80. package/dist/docker/docker-resource-lifecycle.js.map +1 -0
  81. package/dist/docker/gc-command.d.ts +2 -0
  82. package/dist/docker/gc-command.js +37 -0
  83. package/dist/docker/gc-command.js.map +1 -0
  84. package/dist/docker/mitm-proxy.d.ts +58 -5
  85. package/dist/docker/mitm-proxy.js +338 -26
  86. package/dist/docker/mitm-proxy.js.map +1 -1
  87. package/dist/docker/network-topology.d.ts +88 -0
  88. package/dist/docker/network-topology.js +167 -0
  89. package/dist/docker/network-topology.js.map +1 -0
  90. package/dist/docker/oauth-credentials.d.ts +13 -0
  91. package/dist/docker/oauth-credentials.js +125 -0
  92. package/dist/docker/oauth-credentials.js.map +1 -1
  93. package/dist/docker/openrouter.d.ts +107 -0
  94. package/dist/docker/openrouter.js +236 -0
  95. package/dist/docker/openrouter.js.map +1 -0
  96. package/dist/docker/orientation.js +1 -1
  97. package/dist/docker/orientation.js.map +1 -1
  98. package/dist/docker/package-types.d.ts +1 -1
  99. package/dist/docker/platform.d.ts +3 -1
  100. package/dist/docker/platform.js +3 -1
  101. package/dist/docker/platform.js.map +1 -1
  102. package/dist/docker/provider-config.d.ts +35 -1
  103. package/dist/docker/provider-config.js +93 -3
  104. package/dist/docker/provider-config.js.map +1 -1
  105. package/dist/docker/provision-lock.d.ts +46 -0
  106. package/dist/docker/provision-lock.js +152 -0
  107. package/dist/docker/provision-lock.js.map +1 -0
  108. package/dist/docker/pty-session.d.ts +22 -0
  109. package/dist/docker/pty-session.js +238 -84
  110. package/dist/docker/pty-session.js.map +1 -1
  111. package/dist/docker/pty-types.d.ts +15 -0
  112. package/dist/docker/pty-types.js +8 -0
  113. package/dist/docker/pty-types.js.map +1 -1
  114. package/dist/docker/registry-proxy.d.ts +46 -0
  115. package/dist/docker/registry-proxy.js +255 -1
  116. package/dist/docker/registry-proxy.js.map +1 -1
  117. package/dist/docker/resource-limits.d.ts +14 -5
  118. package/dist/docker/resource-limits.js +11 -8
  119. package/dist/docker/resource-limits.js.map +1 -1
  120. package/dist/docker/sse-extractor.js +55 -5
  121. package/dist/docker/sse-extractor.js.map +1 -1
  122. package/dist/docker/stream-delay.d.ts +86 -0
  123. package/dist/docker/stream-delay.js +218 -0
  124. package/dist/docker/stream-delay.js.map +1 -0
  125. package/dist/docker/token-stream-types.d.ts +15 -0
  126. package/dist/docker/trajectory-capture.d.ts +79 -0
  127. package/dist/docker/trajectory-capture.js +521 -0
  128. package/dist/docker/trajectory-capture.js.map +1 -0
  129. package/dist/docker/trajectory-reassembler.d.ts +286 -0
  130. package/dist/docker/trajectory-reassembler.js +1010 -0
  131. package/dist/docker/trajectory-reassembler.js.map +1 -0
  132. package/dist/docker/trajectory-tap.d.ts +100 -0
  133. package/dist/docker/trajectory-tap.js +410 -0
  134. package/dist/docker/trajectory-tap.js.map +1 -0
  135. package/dist/docker/trajectory-types.d.ts +187 -0
  136. package/dist/docker/trajectory-types.js +43 -0
  137. package/dist/docker/trajectory-types.js.map +1 -0
  138. package/dist/docker/types.d.ts +116 -12
  139. package/dist/doctor/checks.d.ts +39 -6
  140. package/dist/doctor/checks.js +129 -25
  141. package/dist/doctor/checks.js.map +1 -1
  142. package/dist/doctor/doctor-command.js +39 -9
  143. package/dist/doctor/doctor-command.js.map +1 -1
  144. package/dist/doctor/mcp-liveness.d.ts +1 -1
  145. package/dist/doctor/mcp-liveness.js +12 -4
  146. package/dist/doctor/mcp-liveness.js.map +1 -1
  147. package/dist/escalation/escalation-watcher.js +6 -0
  148. package/dist/escalation/escalation-watcher.js.map +1 -1
  149. package/dist/{mux → escalation}/trusted-input.d.ts +7 -4
  150. package/dist/{mux → escalation}/trusted-input.js +8 -5
  151. package/dist/escalation/trusted-input.js.map +1 -0
  152. package/dist/hash.js.map +1 -1
  153. package/dist/index.js +65 -16
  154. package/dist/index.js.map +1 -1
  155. package/dist/mux/mux-app.d.ts +9 -0
  156. package/dist/mux/mux-app.js +18 -16
  157. package/dist/mux/mux-app.js.map +1 -1
  158. package/dist/mux/mux-command.js +26 -8
  159. package/dist/mux/mux-command.js.map +1 -1
  160. package/dist/mux/mux-input-handler.d.ts +26 -2
  161. package/dist/mux/mux-input-handler.js +93 -11
  162. package/dist/mux/mux-input-handler.js.map +1 -1
  163. package/dist/mux/mux-renderer.d.ts +2 -1
  164. package/dist/mux/mux-renderer.js +67 -0
  165. package/dist/mux/mux-renderer.js.map +1 -1
  166. package/dist/mux/provider-profile-snapshot.d.ts +44 -0
  167. package/dist/mux/provider-profile-snapshot.js +56 -0
  168. package/dist/mux/provider-profile-snapshot.js.map +1 -0
  169. package/dist/mux/types.d.ts +3 -2
  170. package/dist/mux/types.js +6 -2
  171. package/dist/mux/types.js.map +1 -1
  172. package/dist/observe/observe-command.js +112 -125
  173. package/dist/observe/observe-command.js.map +1 -1
  174. package/dist/persona/broad-policy-validator.d.ts +95 -0
  175. package/dist/persona/broad-policy-validator.js +171 -0
  176. package/dist/persona/broad-policy-validator.js.map +1 -0
  177. package/dist/persona/compile-persona-policy.d.ts +54 -2
  178. package/dist/persona/compile-persona-policy.js +29 -4
  179. package/dist/persona/compile-persona-policy.js.map +1 -1
  180. package/dist/persona/event-bus-progress-reporter.d.ts +57 -0
  181. package/dist/persona/event-bus-progress-reporter.js +66 -0
  182. package/dist/persona/event-bus-progress-reporter.js.map +1 -0
  183. package/dist/persona/persona-command.js +46 -76
  184. package/dist/persona/persona-command.js.map +1 -1
  185. package/dist/persona/persona-compile-orchestrator.d.ts +101 -0
  186. package/dist/persona/persona-compile-orchestrator.js +504 -0
  187. package/dist/persona/persona-compile-orchestrator.js.map +1 -0
  188. package/dist/persona/persona-service.d.ts +137 -0
  189. package/dist/persona/persona-service.js +263 -0
  190. package/dist/persona/persona-service.js.map +1 -0
  191. package/dist/persona/policy-mutation-audit.d.ts +109 -0
  192. package/dist/persona/policy-mutation-audit.js +179 -0
  193. package/dist/persona/policy-mutation-audit.js.map +1 -0
  194. package/dist/persona/rule-delta.d.ts +30 -0
  195. package/dist/persona/rule-delta.js +82 -0
  196. package/dist/persona/rule-delta.js.map +1 -0
  197. package/dist/persona/types.d.ts +12 -0
  198. package/dist/pipeline/annotate.js +10 -2
  199. package/dist/pipeline/annotate.js.map +1 -1
  200. package/dist/pipeline/compile.js +14 -2
  201. package/dist/pipeline/compile.js.map +1 -1
  202. package/dist/pipeline/constitution-compiler.js.map +1 -1
  203. package/dist/pipeline/generate-with-repair.js.map +1 -1
  204. package/dist/pipeline/pipeline-runner.d.ts +63 -4
  205. package/dist/pipeline/pipeline-runner.js +159 -50
  206. package/dist/pipeline/pipeline-runner.js.map +1 -1
  207. package/dist/pipeline/pipeline-shared.d.ts +32 -3
  208. package/dist/pipeline/pipeline-shared.js +76 -8
  209. package/dist/pipeline/pipeline-shared.js.map +1 -1
  210. package/dist/pipeline/refresh-lists.js +3 -5
  211. package/dist/pipeline/refresh-lists.js.map +1 -1
  212. package/dist/pipeline/scenario-schema-validator.js.map +1 -1
  213. package/dist/{mux → pty}/pty-bridge.d.ts +41 -3
  214. package/dist/{mux → pty}/pty-bridge.js +71 -18
  215. package/dist/pty/pty-bridge.js.map +1 -0
  216. package/dist/pty/resolve-ironcurtain-bin.d.ts +17 -0
  217. package/dist/pty/resolve-ironcurtain-bin.js +22 -0
  218. package/dist/pty/resolve-ironcurtain-bin.js.map +1 -0
  219. package/dist/sandbox/index.d.ts +19 -4
  220. package/dist/sandbox/index.js +31 -18
  221. package/dist/sandbox/index.js.map +1 -1
  222. package/dist/servers/fetch-server.js +2 -0
  223. package/dist/servers/fetch-server.js.map +1 -1
  224. package/dist/session/index.d.ts +1 -1
  225. package/dist/session/index.js +20 -2
  226. package/dist/session/index.js.map +1 -1
  227. package/dist/session/preflight.d.ts +20 -35
  228. package/dist/session/preflight.js +92 -109
  229. package/dist/session/preflight.js.map +1 -1
  230. package/dist/session/resource-budget-tracker.js +8 -0
  231. package/dist/session/resource-budget-tracker.js.map +1 -1
  232. package/dist/session/session-manager.d.ts +7 -0
  233. package/dist/session/session-manager.js.map +1 -1
  234. package/dist/session/types.d.ts +23 -0
  235. package/dist/signal/bot-command.js +6 -1
  236. package/dist/signal/bot-command.js.map +1 -1
  237. package/dist/signal/setup-signal.js +1 -1
  238. package/dist/signal/setup-signal.js.map +1 -1
  239. package/dist/signal/signal-bot-daemon.d.ts +1 -0
  240. package/dist/signal/signal-bot-daemon.js +9 -2
  241. package/dist/signal/signal-bot-daemon.js.map +1 -1
  242. package/dist/signal/signal-container.d.ts +3 -3
  243. package/dist/signal/signal-container.js +2 -2
  244. package/dist/signal/signal-container.js.map +1 -1
  245. package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
  246. package/dist/trusted-process/call-circuit-breaker.js +22 -3
  247. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  248. package/dist/trusted-process/container-command.d.ts +35 -0
  249. package/dist/trusted-process/container-command.js +49 -0
  250. package/dist/trusted-process/container-command.js.map +1 -0
  251. package/dist/trusted-process/mcp-error-utils.js +4 -1
  252. package/dist/trusted-process/mcp-error-utils.js.map +1 -1
  253. package/dist/trusted-process/mcp-proxy-server.js +37 -3
  254. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  255. package/dist/trusted-process/policy-engine.js +13 -2
  256. package/dist/trusted-process/policy-engine.js.map +1 -1
  257. package/dist/trusted-process/tcp-server-transport.d.ts +19 -1
  258. package/dist/trusted-process/tcp-server-transport.js +56 -10
  259. package/dist/trusted-process/tcp-server-transport.js.map +1 -1
  260. package/dist/trusted-process/tool-call-coordinator.d.ts +22 -8
  261. package/dist/trusted-process/tool-call-coordinator.js +149 -65
  262. package/dist/trusted-process/tool-call-coordinator.js.map +1 -1
  263. package/dist/trusted-process/tool-call-pipeline.d.ts +8 -0
  264. package/dist/trusted-process/tool-call-pipeline.js +68 -34
  265. package/dist/trusted-process/tool-call-pipeline.js.map +1 -1
  266. package/dist/util/atomic-write.d.ts +31 -0
  267. package/dist/util/atomic-write.js +43 -0
  268. package/dist/util/atomic-write.js.map +1 -0
  269. package/dist/utils/preflight-checks.js +1 -3
  270. package/dist/utils/preflight-checks.js.map +1 -1
  271. package/dist/utils/stdio-guard.d.ts +1 -0
  272. package/dist/utils/stdio-guard.js +27 -0
  273. package/dist/utils/stdio-guard.js.map +1 -0
  274. package/dist/web-ui/__tests__/config-dispatch.test.d.ts +13 -0
  275. package/dist/web-ui/__tests__/config-dispatch.test.js +383 -0
  276. package/dist/web-ui/__tests__/config-dispatch.test.js.map +1 -0
  277. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +6 -0
  278. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -1
  279. package/dist/web-ui/conn-registry.d.ts +14 -0
  280. package/dist/web-ui/conn-registry.js +25 -0
  281. package/dist/web-ui/conn-registry.js.map +1 -0
  282. package/dist/web-ui/dispatch/config-dispatch.d.ts +15 -0
  283. package/dist/web-ui/dispatch/config-dispatch.js +277 -0
  284. package/dist/web-ui/dispatch/config-dispatch.js.map +1 -0
  285. package/dist/web-ui/dispatch/escalation-dispatch.js +11 -2
  286. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -1
  287. package/dist/web-ui/dispatch/persona-dispatch.d.ts +46 -2
  288. package/dist/web-ui/dispatch/persona-dispatch.js +209 -56
  289. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -1
  290. package/dist/web-ui/dispatch/pty-dispatch.d.ts +15 -0
  291. package/dist/web-ui/dispatch/pty-dispatch.js +66 -0
  292. package/dist/web-ui/dispatch/pty-dispatch.js.map +1 -0
  293. package/dist/web-ui/dispatch/session-dispatch.js +89 -6
  294. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -1
  295. package/dist/web-ui/dispatch/types.d.ts +35 -0
  296. package/dist/web-ui/dispatch/types.js +42 -1
  297. package/dist/web-ui/dispatch/types.js.map +1 -1
  298. package/dist/web-ui/dispatch/workflow-dispatch.js +116 -5
  299. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -1
  300. package/dist/web-ui/json-rpc-dispatch.js +4 -1
  301. package/dist/web-ui/json-rpc-dispatch.js.map +1 -1
  302. package/dist/web-ui/pty-session-manager.d.ts +234 -0
  303. package/dist/web-ui/pty-session-manager.js +599 -0
  304. package/dist/web-ui/pty-session-manager.js.map +1 -0
  305. package/dist/web-ui/web-event-bus.d.ts +34 -1
  306. package/dist/web-ui/web-event-bus.js.map +1 -1
  307. package/dist/web-ui/web-session-transport.js.map +1 -1
  308. package/dist/web-ui/web-ui-server.d.ts +22 -0
  309. package/dist/web-ui/web-ui-server.js +41 -0
  310. package/dist/web-ui/web-ui-server.js.map +1 -1
  311. package/dist/web-ui/web-ui-types.d.ts +212 -5
  312. package/dist/web-ui/web-ui-types.js.map +1 -1
  313. package/dist/web-ui-static/assets/index-CCVI4nEK.css +1 -0
  314. package/dist/web-ui-static/assets/index-Dm012gvY.js +114 -0
  315. package/dist/web-ui-static/index.html +2 -2
  316. package/dist/workflow/cli-shared.d.ts +62 -0
  317. package/dist/workflow/cli-shared.js +117 -0
  318. package/dist/workflow/cli-shared.js.map +1 -0
  319. package/dist/workflow/cli-support.js +0 -2
  320. package/dist/workflow/cli-support.js.map +1 -1
  321. package/dist/workflow/container-snapshots.d.ts +29 -0
  322. package/dist/workflow/container-snapshots.js +137 -0
  323. package/dist/workflow/container-snapshots.js.map +1 -0
  324. package/dist/workflow/daemon-gate-commands.d.ts +21 -0
  325. package/dist/workflow/daemon-gate-commands.js +595 -0
  326. package/dist/workflow/daemon-gate-commands.js.map +1 -0
  327. package/dist/workflow/discovery.d.ts +29 -0
  328. package/dist/workflow/discovery.js +67 -7
  329. package/dist/workflow/discovery.js.map +1 -1
  330. package/dist/workflow/lane-template.d.ts +89 -0
  331. package/dist/workflow/lane-template.js +141 -0
  332. package/dist/workflow/lane-template.js.map +1 -0
  333. package/dist/workflow/lint.d.ts +7 -1
  334. package/dist/workflow/lint.js +186 -3
  335. package/dist/workflow/lint.js.map +1 -1
  336. package/dist/workflow/machine-builder.d.ts +90 -2
  337. package/dist/workflow/machine-builder.js +222 -29
  338. package/dist/workflow/machine-builder.js.map +1 -1
  339. package/dist/workflow/message-log.d.ts +15 -1
  340. package/dist/workflow/message-log.js.map +1 -1
  341. package/dist/workflow/orchestrator.d.ts +505 -1
  342. package/dist/workflow/orchestrator.js +1725 -69
  343. package/dist/workflow/orchestrator.js.map +1 -1
  344. package/dist/workflow/prompt-builder.js +2 -1
  345. package/dist/workflow/prompt-builder.js.map +1 -1
  346. package/dist/workflow/run-state-command.js +1 -1
  347. package/dist/workflow/run-state-command.js.map +1 -1
  348. package/dist/workflow/shutdown-signals.d.ts +7 -0
  349. package/dist/workflow/shutdown-signals.js +40 -0
  350. package/dist/workflow/shutdown-signals.js.map +1 -0
  351. package/dist/workflow/status-parser.js +19 -4
  352. package/dist/workflow/status-parser.js.map +1 -1
  353. package/dist/workflow/types.d.ts +123 -23
  354. package/dist/workflow/types.js +2 -0
  355. package/dist/workflow/types.js.map +1 -1
  356. package/dist/workflow/validate.d.ts +23 -1
  357. package/dist/workflow/validate.js +311 -22
  358. package/dist/workflow/validate.js.map +1 -1
  359. package/dist/workflow/workflow-command.d.ts +0 -17
  360. package/dist/workflow/workflow-command.js +118 -94
  361. package/dist/workflow/workflow-command.js.map +1 -1
  362. package/dist/workflow/workflow-discovery.d.ts +2 -0
  363. package/dist/workflow/workflow-discovery.js +22 -1
  364. package/dist/workflow/workflow-discovery.js.map +1 -1
  365. package/dist/workflow/workflow-manager.d.ts +34 -0
  366. package/dist/workflow/workflow-manager.js +52 -1
  367. package/dist/workflow/workflow-manager.js.map +1 -1
  368. package/dist/workflow/workflows/design-and-code/README.md +50 -0
  369. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  370. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  371. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  372. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  373. package/dist/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  374. package/dist/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  375. package/dist/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  376. package/dist/workflow/workflows/evolve/README.md +66 -0
  377. package/dist/workflow/workflows/evolve/scripts/LICENSE +201 -0
  378. package/dist/workflow/workflows/evolve/scripts/README.md +314 -0
  379. package/dist/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  380. package/dist/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  381. package/dist/workflow/workflows/evolve/scripts/evolve-db +16 -0
  382. package/dist/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  383. package/dist/workflow/workflows/evolve/scripts/evolve-files +16 -0
  384. package/dist/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  385. package/dist/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  386. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  387. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  388. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  389. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  390. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  391. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  392. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  393. package/dist/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  394. package/dist/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  395. package/dist/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  396. package/dist/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  397. package/dist/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  398. package/dist/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  399. package/dist/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  400. package/dist/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  401. package/dist/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  402. package/dist/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  403. package/dist/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  404. package/dist/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  405. package/dist/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  406. package/dist/workflow/workflows/evolve/workflow.yaml +646 -0
  407. package/dist/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  408. package/dist/workflow/workflows/vuln-discovery/README.md +54 -0
  409. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  410. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  411. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  412. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  413. package/docker/Dockerfile.base +22 -0
  414. package/docker/Dockerfile.base.arm64 +18 -0
  415. package/docker/Dockerfile.claude-code +3 -1
  416. package/docker/Dockerfile.codex +17 -0
  417. package/docker/Dockerfile.goose +3 -1
  418. package/docker/entrypoint-claude-code.sh +28 -45
  419. package/docker/entrypoint-codex.sh +42 -0
  420. package/docker/entrypoint-goose.sh +5 -24
  421. package/docker/entrypoint-uid-remap.sh +93 -0
  422. package/package.json +39 -30
  423. package/src/workflow/workflows/design-and-code/README.md +50 -0
  424. package/src/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  425. package/src/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  426. package/src/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  427. package/src/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  428. package/src/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  429. package/src/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  430. package/src/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  431. package/src/workflow/workflows/evolve/README.md +66 -0
  432. package/src/workflow/workflows/evolve/scripts/LICENSE +201 -0
  433. package/src/workflow/workflows/evolve/scripts/README.md +314 -0
  434. package/src/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  435. package/src/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  436. package/src/workflow/workflows/evolve/scripts/evolve-db +16 -0
  437. package/src/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  438. package/src/workflow/workflows/evolve/scripts/evolve-files +16 -0
  439. package/src/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  440. package/src/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  441. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  442. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  443. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  444. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  445. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  446. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  447. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  448. package/src/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  449. package/src/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  450. package/src/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  451. package/src/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  452. package/src/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  453. package/src/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  454. package/src/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  455. package/src/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  456. package/src/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  457. package/src/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  458. package/src/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  459. package/src/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  460. package/src/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  461. package/src/workflow/workflows/evolve/workflow.yaml +646 -0
  462. package/src/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  463. package/src/workflow/workflows/vuln-discovery/README.md +54 -0
  464. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  465. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  466. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  467. package/src/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  468. package/dist/mux/pty-bridge.js.map +0 -1
  469. package/dist/mux/trusted-input.js.map +0 -1
  470. package/dist/web-ui-static/assets/index-BFO5z1js.js +0 -76
  471. package/dist/web-ui-static/assets/index-CCUwrkIL.css +0 -1
  472. package/packages/web-ui/README.md +0 -113
@@ -0,0 +1,54 @@
1
+ # Vulnerability Discovery
2
+
3
+ An orchestrator-driven workflow that points a Docker-isolated `claude-code` agent at a body of code, **builds tiered instrumented harnesses, and tries to execute its way to a confirmed vulnerability** — analysis → harnessing → discovery → triage → report, with human gates whenever the machine gets stuck or finishes.
4
+
5
+ Run it from the web UI (**Start Workflow → `vuln-discovery`**) or the CLI:
6
+
7
+ ```bash
8
+ ironcurtain workflow start vuln-discovery \
9
+ "find a memory-corruption bug reachable from a crafted H.264 stream" \
10
+ --workspace ~/src/ffmpeg
11
+ ```
12
+
13
+ Point `--workspace` at a checkout of the code under investigation. The task description carries the **scope, vulnerability class, and threat model**; a one-liner like `"find a vulnerability in libavcodec/h264_slice.c"` is enough to start, since the agent prompts supply the methodology. Add detail to steer the hunt.
14
+
15
+ > **Requires Docker.** This workflow only runs in Docker Agent Mode (`--network=none`); there is no builtin-mode fallback. Run `ironcurtain doctor` first if you're unsure your environment is ready. For a fresh-checkout walkthrough (build, daemon, web UI, non-Anthropic models, troubleshooting) see [`docs/vuln-discovery-onboarding/README.md`](https://github.com/provos/ironcurtain/blob/master/docs/vuln-discovery-onboarding/README.md).
16
+
17
+ ## What it does
18
+
19
+ The orchestrator sits at the center and decides what to do next each round, reading and updating the investigation journal. Around it, the agents move a hypothesis from structural understanding to an executed, triaged finding. All artifacts land under `.workflow/` in the workspace.
20
+
21
+ | Phase | State(s) | Role | Output |
22
+ | ----- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
23
+ | 1 | `analyze` | Code analyst — maps call chains, entry points, build system, trust boundaries | `.workflow/analysis/analysis.md` |
24
+ | — | `orchestrator` | Strategist — picks the next state, enforces budget, owns the journal | `.workflow/journal/journal.md` |
25
+ | 2 | `harness_design` → `harness_design_review` | Architect + LLM reviewer — spec an instrumented, tiered harness | `.workflow/harness_design/` |
26
+ | 3 | `harness_build` → `harness_validate` | Engineer + verifier — implement the harness, confirm it runs and produces coverage | `.workflow/harness_build/`, `harness_validate/` |
27
+ | 4 | `discover` (+ `differential_validate`) | Researcher — hypothesis-driven search using the harness; diagnose isolated-vs-live divergence | `.workflow/discoveries/findings.h*.md` |
28
+ | 5 | `triage` | Independent reproducer — re-prove each finding and assess severity | `.workflow/triage/triage.h*.md` |
29
+ | 6 | `conclude` → `review` | Report writer + LLM reviewer — assemble the final report, catch scope drift and evidence/headline contradictions | `.workflow/report/report.md` |
30
+
31
+ ## Human gates
32
+
33
+ Four gates pause the run for your decision. Each presents the relevant artifacts (journal, analysis, designs, reports) and accepts **APPROVE** (continue), **FORCE_REVISION** (loop back — your feedback is routed verbatim into the next agent's prompt, so brief it like an orchestrator), or **ABORT**.
34
+
35
+ - **`human_escalation`** — the orchestrator detected a stall (same state 3+ times, contradictory results, or no clear next direction). Give it another round, redirect it, or stop.
36
+ - **`harness_design_escalation`** — the design-review loop rejected the design 3× in a row. Accept the design as-is, redirect, or stop.
37
+ - **`harness_validate_escalation`** — the validate loop rejected the build 4× in a row. Accept the harness despite validation failures, redirect, or stop.
38
+ - **`report_review`** — the investigation reached `complete` and the final report is ready. Approve it, send it back (your assessment overrides the agent's — e.g. dispute a "mitigated" verdict and it re-investigates at a higher tier), or discard.
39
+
40
+ ## The orchestration loop
41
+
42
+ Unlike a fixed pipeline, the **orchestrator** re-routes after every round based on the journal: forward when a harness validates and findings hold, back to re-design or re-discover when they don't. The loop is bounded by `maxRounds` (**12**) and `maxSessionSeconds` (**10800** = 3 hours). When either limit or a stall is hit, control surfaces to a human gate rather than spinning — it never loops forever.
43
+
44
+ ## At a glance
45
+
46
+ - **Mode:** Docker (`claude-code` agent, `--network=none`, shared container)
47
+ - **Model:** `anthropic:claude-opus-4-8`
48
+ - **Persona:** `global` for every agent state
49
+ - **Caps:** 12 rounds · 3-hour session budget
50
+ - **Skills:** `memory-safety-c-cpp`, `harness-design-fuzzing`, `vulnerability-triage` (co-packaged under `skills/`)
51
+ - **Artifacts:** `analysis`, `journal`, `harness_*`, `discoveries`, `triage`, `report` — all under `.workflow/`
52
+ - **Terminal states:** `done` (report approved) · `aborted` (stopped at a gate)
53
+
54
+ > Tip: the more precisely the task states the **threat model** (who the attacker is, how input reaches the code), the less the analyst has to guess. "Memory corruption reachable from an untrusted network packet" steers the whole run; "find bugs" does not. To run bigger targets past the default caps, copy the whole `vuln-discovery/` directory (it carries the skills it needs) into `~/.ironcurtain/workflows/` and raise the limits there.
@@ -61,13 +61,15 @@ Use Tier 3 when the bug depends on initialization sequences, global state, runti
61
61
 
62
62
  The harness must FUZZ, not unit-test. For each swept variable (hypothesis inputs for trigger-driven, dispatch axes for coverage-driven), the design must specify:
63
63
 
64
- - **Sweep range.** The full range of values to iterate over. Cover the entire representable range of the type, not just "a few specific values." For wide ranges, specify dense sampling near type boundaries (min, max, zero, `2^N - 1` for each relevant N, sign-flip points) plus sparse sampling elsewhere. For coverage-driven dispatch axes, the range is the full set of dispatch values plus their realistic combinations.
64
+ - **Sweep range.** The full range of values to iterate over. Cover the entire representable range of the type, not just "a few specific values." For wide ranges, specify dense sampling near type boundaries (min, max, zero, `2^N - 1` for each relevant N, sign-flip points) plus sparse sampling elsewhere. When the variable feeds an allocation size through a product or other multi-term arithmetic, also sample values whose wrapped result is *small but positive*: these keep the consumer's loop/index bound large while the undersized allocation succeeds, whereas the value that wraps exactly to zero is usually intercepted by a separate zero-size guard. For coverage-driven dispatch axes, the range is the full set of dispatch values plus their realistic combinations.
65
65
  - **Search strategy.** Exhaustive enumeration if the space is < 100M combinations; boundary-dense + random sampling otherwise; coverage-guided fuzzer for large or structured input spaces.
66
66
  - **Observables.** At each input value, what does the harness record? Stored value in the target buffer, result of the bounds check at `<file>:<line>`, return value of the target function, sanitizer error, canary state, dispatch arm hit. Be specific by site.
67
67
  - **Positive-finding condition.** What pattern in the observables confirms the design's claim? For trigger-driven: be specific — not "a crash happens" but "the bounds check at `<file>:<line>` evaluates true when the input value exceeds the buffer's allocated size." For coverage-driven: "any sanitizer error within the named code region" is acceptable, but the region must be concrete.
68
68
 
69
69
  The design specifies input variables and ranges. The implementer drives the sweep. Do NOT prescribe individual "Test A / Test B / Test C" scenarios with fixed values — that's unit testing and will miss the actual boundary.
70
70
 
71
+ **Per-input cost bound.** A coverage-guided fuzzer needs many iterations per second to work. The design must keep per-input cost bounded: cap input size (libFuzzer `-max_len`, AFL++ `-G`), and keep the fuzz entry point free of full re-initialization, process spawns, or multi-connection sweeps — move one-time setup behind a `call_once` / singleton. If an oracle genuinely needs a many-connection or many-iteration measurement (e.g. RSS amplification), that is a *separate* harness, not per-input work folded into the sweep.
72
+
71
73
  ### Multi-hypothesis bundling
72
74
 
73
75
  A trigger-driven design may scope a single hypothesis or a related set when they share input space and code region. Bundle when consolidating doesn't dilute the sweep (e.g., three integer-overflow hypotheses on the same allocator → one harness). Keep separate when input distributions conflict (one wants huge dimensions, another wants small) or observation points differ enough that one harness can't see both.
@@ -145,6 +147,8 @@ The harness MUST test against UNMODIFIED source code. Stripping a guard, weakeni
145
147
 
146
148
  A Tier-1 harness with hardening stripped tells you nothing about production outcomes. Triage anchors on what the harness demonstrated under production-equivalent conditions.
147
149
 
150
+ Default the value-producing integer checks (overflow, shift, truncation) to **recover / warn-only mode**: aborting at the arithmetic site stops execution before any downstream undersized-allocation or out-of-bounds sink, masking the real impact and inviting a false "mitigated" verdict. Recover mode lets execution continue with the wrapped value (production behavior), so one run surfaces both the root-cause diagnostic and the sink.
151
+
148
152
  ## Delegate library realism
149
153
 
150
154
  Mitigation discipline applies at the link layer too. A harness that links against a project-internal stub of an upstream library produces evidence about the stub, not about production. Every stub the design introduces must be classified.
@@ -174,6 +178,7 @@ List every external dependency (sanitizer toolchain, fuzzer binary, coverage too
174
178
  - **Hand-picked positive scenarios masquerading as a sweep.** Five "boundary cases" are chosen by hand. The actual boundary in the target's arithmetic is not one of the five.
175
179
  - **Hardening stripped for "easier debugging."** ASLR off, canaries off, custom permissive allocator. The harness fires; production does not. Tells you nothing.
176
180
  - **Validator stubbed at link time.** A project-internal stub of a parser, validator, or decoder library is linked into the harness, omitting the upstream component's own validation. The harness fires; production rejects the trigger before the violation site is reached. Tells you nothing about reachability.
181
+ - **Per-input heavyweight setup.** The harness does a full handshake, fixture rebuild, or N-connection sweep inside the fuzz entry point, so it runs at single-digit exec/s — an integration test in a loop, not a fuzzer. The feedback loop never gets enough iterations to reach boundary values. Move one-time setup to a singleton; split a measurement oracle that needs many connections into its own non-fuzzed harness.
177
182
 
178
183
  ## Design-document scope
179
184
 
@@ -163,4 +163,5 @@ If any of the three cannot be answered from the artifacts alone, the finding is
163
163
  - **Round-tripped attacker input mistaken for leaked target state.** If the bytes the bug "leaks" are bytes the attacker just sent, that is not a confidentiality finding. Memory-read findings need to demonstrate adjacency — what _target_ state sits next to the affected region.
164
164
  - **Padding or zeroed memory mistaken for sensitive content.** A heap OOB read that returns NUL bytes, schema-sanitized placeholders, or default-config values has not demonstrated impact.
165
165
  - **Hardening-stripped harness vs production binary.** A harness compiled without `-D_FORTIFY_SOURCE`, without stack canaries, without ASLR, or with custom allocators tells you nothing about the production binary's behavior. The triage question is "what does the production build do under this input," not "what does my Tier-1 reproduction show."
166
+ - **An abort at the arithmetic site mistaken for unreachability of the downstream sink.** In an overflow-into-undersized-allocation chain, a check that aborts at the overflow itself — an integer sanitizer in halt mode, or a precondition/zero-size guard that only fires when the value wraps exactly to zero — stops execution before the write, making the bug look pre-empted. Production builds wrap silently and reach the sink. Run the overflow checks in warn-only / recover mode (do not force-abort on them) so a single run surfaces both the root-cause diagnostic and the downstream sanitizer trip; or confirm on an allocation-sanitized build driven with a trigger whose wrapped value is small-positive. A "mitigated/pre-empted" verdict for an overflow-into-sink hypothesis is valid only when established on a build that lets execution continue past the overflow, not on the abort-at-overflow configuration.
166
167
  - **Variant tunnel-vision.** Reporting only the variant the harness reproduced when the underlying pattern (e.g., the same arithmetic in a sibling dispatch handler) reaches more of the surface. Every variant is its own attack surface; document each.
@@ -43,6 +43,8 @@ For each finding, the rubric below has eleven items. Skipping any one of them is
43
43
 
44
44
  6. **Production hardening.** What mitigations present in production (sanitizers, schema validators, CSP, ASLR, stack canaries, auth middleware, sandbox, rate limits, RELRO, FORTIFY_SOURCE) change the attacker's success rate? Did the experiment use the production profile or bypass it? A harness with hardening stripped tells you nothing about production outcomes — score what production does, not what the harness did.
45
45
 
46
+ When a mitigation table asserts a runtime cap, flag, or mode is at a particular default value in production, cite the line that *registers* that default (GObject property spec, struct initializer, constant read at startup) — not a call site that happens to leave it unset. A harness that overrides the default via the library's public API (`set_max_X(0)`, `set_strict(false)`) looks identical at the bug site to a real default of 0; the registration cite is what distinguishes them.
47
+
46
48
  7. **External exploitability trace.** Does the input originate from an external / untrusted source, and through what delivery channel? Name it concretely — network protocol, file upload, queue consumer, environment-variable propagation, IPC. Complete answer: a named delivery channel from an untrusted boundary to the bug site. Incomplete: "an attacker could supply input" without naming how.
47
49
 
48
50
  8. **Duplicate / CVE check.** Search the project's issue tracker, CVE databases, and changelog for the code location. Note overlap with existing fixes even when partial — a fix for one variant does not necessarily cover sibling variants.
@@ -162,6 +164,8 @@ Patterns the rubric catches that get missed in practice. These are surface-neutr
162
164
 
163
165
  - **Hardening-stripped harness scored as production.** The harness ran without ASLR, without canaries, without FORTIFY_SOURCE, with a permissive custom allocator, or with the validating middleware bypassed. The production binary intercepts the effect; the harness did not see that interception. The Achievable / Environmental score is what production does, not what the harness did.
164
166
 
167
+ - **Harness API override mistaken for production default.** The harness called the library's public configuration API to lift a safety valve (`set_max_X(0)`, disable strict mode, suppress validation) so a latent bug becomes reachable — a legitimate research move. The mitigations table then asserts that the *production* default is the lifted value, conflating "the harness set it to 0" with "the default is 0". Distinct from compiler-hardening stripping: this is the library's own runtime configuration, and the registration site (property spec, initializer) is the ground truth, not the call-site override.
168
+
165
169
  - **Theoretical ceiling promoted to demonstrated severity by rewording.** "This pattern *could* yield RCE" gets quoted, then rewritten without the hedge, then cited as "demonstrated RCE" two paragraphs later. The rubric's reproduction and effect-realism items refute this; the hedging-elimination rule names it.
166
170
 
167
171
  - **Variant tunnel-vision.** The harness reproduced one variant of the underlying pattern; the finding reports only that variant. When the same arithmetic / lifetime / dispatch shape reaches sibling variants on the same surface, those are findings in their own right. Document each, even when the experiment exercised only one.
@@ -6,7 +6,7 @@ settings:
6
6
  mode: docker
7
7
  dockerAgent: claude-code
8
8
  sharedContainer: true
9
- model: anthropic:claude-opus-4-7
9
+ model: anthropic:claude-opus-4-8
10
10
  maxRounds: 12
11
11
  maxSessionSeconds: 10800
12
12
  unversionedArtifacts:
@@ -162,11 +162,12 @@ states:
162
162
 
163
163
  **If discover returned `approved` (confirmed):** route to `triage` if the evidence is solid, or back to `discover` for more evidence if it is thin.
164
164
 
165
- **If discover returned `blocked` (mitigated or exhausted):** the "mitigated" claim is authoritative only when BOTH of:
165
+ **If discover returned `blocked` (mitigated or exhausted):** the "mitigated" claim is authoritative only when ALL of:
166
166
  - The tier matched the hypothesis scope — cross-component hypotheses require Tier 2+.
167
167
  - Evidence shows execution, not reasoning: coverage of the mitigation code AND a completed input sweep over the hypothesis's value range (fuzzing for value-dependent claims, bounded enumeration for discrete claims).
168
+ - The mitigation was exercised, not just present. If non-impact is attributed to an invariant the harness had to disable to reach the site (a concurrency/serialization contract, an upstream guard, a runtime/allocator check, or a first-abort that shadows a later sink), the claim holds only if discover ran the trigger under a configuration where that invariant is present and showed the effect absorbed there. An invariant-only mitigation backed by source reading is the masked-finding pattern — non-terminal.
168
169
 
169
- If the evidence is text-only ("the code looks safe"), re-route to `discover` to exercise the mitigation, or to `harness_design` at a higher tier if the harness cannot exercise it.
170
+ If the evidence is text-only ("the code looks safe") or the mitigation rests on an un-exercised invariant, re-route to `discover` to exercise it under an invariant-present config, or to `harness_design` at a higher tier if the harness cannot. If no invariant-present config that still reaches the site can be built, treat the evidence as insufficient and keep the hypothesis open — do NOT record it as a terminal `harness-tested-and-rejected`; route to `harness_design` for an alternative reachability approach, or defer the hypothesis with an explicit reason. ("Insufficient" here describes the evidence; it is not a routing verdict to emit.)
170
171
 
171
172
  **If discover returned `rejected` (couldn't run):**
172
173
  - Coverage shows the target function IS reached but the condition doesn't fire → `differential_validate` (values need adjustment for the live context: protocol encoding, byte order, framing).
@@ -330,7 +331,7 @@ states:
330
331
 
331
332
  Do NOT include:
332
333
  - Full C/Python/etc. source code for the harness
333
- - Build scripts or compile commands with full flag lists (reference them abstractly: "compile with ASAN+UBSAN")
334
+ - Build scripts or compile commands with full flag lists (reference them abstractly: "compile with ASAN+UBSAN, integer checks in recover/warn-only mode")
334
335
  - Test execution results or tables of "expected outcomes" — you haven't run anything yet
335
336
 
336
337
  DO include:
@@ -485,6 +486,8 @@ states:
485
486
 
486
487
  The harness should be invocable as a CLI tool or script. The discovery agent needs to: start a session, perform multiple steps, observe coverage, and reset cleanly.
487
488
 
489
+ **Bounded harness execution (required).** Every command that runs the built harness — reset check, smoke/baseline run, instrumentation probe, any burst — MUST be wrapped in a hard-kill wall-clock guard: `timeout -s KILL <seconds> <command>`. Plain `timeout` sends a catchable SIGTERM that a wedged harness can defer or ignore; only `-s KILL` (or `-k`) guarantees termination. This is tool-agnostic — it holds whether the harness is a fuzzer, a bounded enumerator, an analyzer, or a one-shot driver.
490
+
488
491
  **Instrumentation verification (required before approved).** Confirm fuzzer-feedback is present on the target, not only the harness. Method depends on the mechanism:
489
492
  - Compile-time (libFuzzer / sanitizer-coverage): `nm` / `readelf` the target archive and expect symbols like `__sanitizer_cov_trace_pc_guard` in the target's object files.
490
493
  - Compile-wrapper (AFL++): instrumented objects contain `__afl_area_ptr`; `afl-showmap` on a baseline reports non-trivial map density (≫100 edges).
@@ -523,15 +526,17 @@ states:
523
526
  2. **Linkage audit (gating).** Run `ldd` (Linux), `otool -L` (macOS), or the platform equivalent against the test binary. Confirm no shared library resolves to a project-internal stub directory (typical names: `*-stub/`, `*_mock/`, `mock-*/`, `fake-*/`). If linkage resolves to a project-internal stub of any component performing trust-boundary validation in production (parser, validator, decoder, signature checker, schema validator), return `rejected_design` regardless of coverage and crash signals. Cite the specific link path and stub component in the rejection. The linkage check is gating: it runs before the coverage and crash gates and short-circuits on failure.
524
527
  3. **Baseline run.** Run the harness with a simple benign input. Verify it exits without errors.
525
528
  4. **Target function reached.** Run the harness coverage command. Verify the **target function** (not just the target file) appears with non-zero hits. If the file has coverage but the function does not, the call chain is broken — fail.
526
- 5. **Fuzzer-feedback reach (load-bearing).** Run a short representative fuzz burst (30–60s) from the seed corpus. Capture the fuzzer's own reported coverage metric:
529
+ 5. **Fuzzer-feedback reach (load-bearing).** Run a short representative fuzz burst (30–60s) from the seed corpus. Run it in the FOREGROUND under a hard kill guard — `timeout -s KILL <burst+30s>` wrapping the binary — and pass the fuzzer's own watchdogs (libFuzzer: `-max_total_time=<burst> -timeout=20 -rss_limit_mb=4096`; AFL++: the `-V` / `-t` / `-m` equivalents). Plain `timeout` sends a catchable SIGTERM that a slow fuzzer defers between iterations — it will not stop a wedged burst. Do NOT background the burst and poll with a `pgrep`/`sleep` loop: `pgrep -af <name>` matches the poll command's own arguments, so the loop never exits. Capture the fuzzer's own reported coverage metric:
527
530
  - libFuzzer (also the tools that wrap its output: jazzer.js, atheris, cargo-fuzz, Jazzer): stderr prints `#N ... cov: C ft: F`. Record `C`.
528
531
  - AFL++: `afl-fuzz` reports `map density` / `edges found`; `afl-showmap` on a sample prints non-zero map entries. Record the edge count.
529
532
  - For any other tool, the design must name the exact metric field (see harness_design coverage-instrumentation rules). If the design does not name it, that is a design defect — route `rejected_design` with a directive "design.md must specify the fuzzer-feedback metric field name for the chosen tool."
530
- Fail approval unless BOTH:
533
+ Fail approval unless ALL of:
531
534
  (a) Target-code fuzzer-feedback count ≥ **1000** (default; configurable via directive). A count in the tens is the signature of wrapper-only instrumentation. For tools reporting in different units, interpret the threshold as "several orders of magnitude larger than a thin wrapper's footprint" and state the equivalent number used.
532
535
  (b) Audit-coverage line coverage of the target source file ≥ **20%** (default; configurable via directive) after the burst.
536
+ (c) Burst throughput is not pathological: record `exec/s` and the slowest-unit time. `exec/s` in the single digits, or any single unit exceeding ~10s, means the harness is too slow to fuzz — a per-input heavyweight-setup defect that both wedges the burst's own time limit and would wedge `discover`.
533
537
  If (a) fails: target code was not instrumented for fuzzer feedback — diagnose build vs. design.
534
538
  If (b) fails but (a) passes: seed corpus is too narrow — route to `harness_design` for seed-corpus revision.
539
+ If (c) fails: route `rejected_design` with directive "cap input size (`-max_len`) and move per-input heavyweight setup (full re-init, process spawn, multi-connection sweep) out of the fuzz entry point."
535
540
  6. **Stateful session sanity.** If the harness supports multi-step sessions, run a short sequence; verify state persists.
536
541
  7. **Branch-level audit coverage exists.** Confirm the audit tool reports branch data, not just line counts.
537
542
 
@@ -596,7 +601,7 @@ states:
596
601
  The orchestrator's directive tells you what to test. Use the harness to:
597
602
 
598
603
  1. **Design an input sequence** that exercises the hypothesized code path. Use the analysis's function catalog for guidance on types, boundaries, and assumptions.
599
- 2. **Send it through the harness** and observe results. Read coverage to see which lines and branches were hit. Check for sanitizer output, crashes, and invariant violations.
604
+ 2. **Send it through the harness** and observe results. Read coverage to see which lines and branches were hit. Check for sanitizer output, crashes, and invariant violations. If you run ANY work in the background (fuzz campaign, build, etc.), wait on the launched PID (`cmd & PID=$!; wait "$PID"`); never poll with a `pgrep -f <name>`/`sleep` loop (`pgrep -f` matches the waiter's own command line and your prompt-bearing agent process, so the loop never exits) and never poll a hardcoded log/output path you did not capture from that same launch.
600
605
  3. **Refine or pivot.** If coverage shows you missed the target path, adjust inputs to hit the right branch. If you reached it with no violation, try variations before moving to a new hypothesis.
601
606
  4. **Repeat systematically.** Work through the hypotheses in the orchestrator's directive.
602
607
 
@@ -608,7 +613,7 @@ states:
608
613
 
609
614
  - **approved**: An exploitable condition fired — sanitizer error, controlled crash, invariant violation, or direct evidence the hypothesized bug triggered. Document the reproduction input, coverage proof, and severity.
610
615
  - **rejected**: The harness is insufficient — can't reach the target, missing protocol feature, broken infrastructure.
611
- - **blocked**: Hypothesis did not fire despite a completed search. You may only return `blocked` if (a) coverage shows the target code was executed, AND (b) you swept the input range relevant to the hypothesis — fuzzing for value-dependent claims (sizes, offsets, indices, counters), bounded enumeration for discrete claims (protocol states, flag combinations, auth paths). "I read the code and it looks mitigated" is not `blocked` — it's `rejected` with a request to redesign the harness. If neither fuzzing nor enumeration applies to this hypothesis, name the reason explicitly in your findings doc.
616
+ - **blocked**: Hypothesis did not fire despite a completed search. You may only return `blocked` if (a) coverage shows the target code was executed, AND (b) you swept the input range relevant to the hypothesis — fuzzing for value-dependent claims (sizes, offsets, indices, counters), bounded enumeration for discrete claims (protocol states, flag combinations, auth paths). "I read the code and it looks mitigated" is not `blocked` — it's `rejected` with a request to redesign the harness. If neither fuzzing nor enumeration applies to this hypothesis, name the reason explicitly in your findings doc. **Invariant-only mitigations are not `blocked`.** If you attribute non-impact to an invariant your harness had to disable to reach the site — a lock/serialization or single-threaded dispatch contract, an upstream guard, a runtime/allocator check, or an earlier sanitizer abort that prevents reaching a downstream sink — you may return `blocked` only if you ALSO ran the trigger under a configuration where that invariant is present and showed the effect absorbed there. Disabling the invariant to reach the site and then citing it as the mitigation is circular; if you cannot build an invariant-present config that still reaches the site, return `blocked` only for the part you exercised and name the un-exercised invariant explicitly.
612
617
 
613
618
  Write per-hypothesis findings to `.workflow/discoveries/findings.<id>.md` where `<id>` is the orchestrator's hypothesis ID — e.g. `findings.h1.md`, `findings.h2.md`. Do not collapse hypotheses into one file. Append after each experiment (hypothesis, input, harness command, result) before starting the next one — do not batch writes to the end, so a crash or rate-limit preserves the round's work. If a hypothesis's findings file already exists from a prior run, read it first and skip experiments whose results are already recorded; do not re-read other hypotheses' files unless the directive scopes you to them.
614
619
  inputs:
@@ -34,6 +34,28 @@ RUN mkdir -p /workspace /etc/ironcurtain /run/ironcurtain && \
34
34
 
35
35
  RUN npm install -g node-gyp
36
36
 
37
+ # Rust toolchain (rustc, cargo) + cargo-fuzz for the vuln-discovery workflow.
38
+ # devcontainers/universal does NOT ship rustup (verified by an x86_64 build
39
+ # failing with `rustup: not found`), so install it explicitly rather than
40
+ # assuming it is present — same self-contained approach as
41
+ # Dockerfile.base.arm64. Containers run with --network=none, so the toolchain
42
+ # and the nightly channel that cargo-fuzz invokes (libFuzzer's unstable -Z
43
+ # flags) must be baked in. Installed system-wide under /opt/rust so the
44
+ # codespace user inherits it via ENV.
45
+ ENV RUSTUP_HOME=/opt/rust/rustup
46
+ ENV CARGO_HOME=/opt/rust/cargo
47
+ ENV PATH=/opt/rust/cargo/bin:$PATH
48
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
49
+ sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal \
50
+ --component rust-src && \
51
+ rustup toolchain install nightly --profile minimal --component rust-src && \
52
+ cargo install cargo-fuzz && \
53
+ rm -rf /opt/rust/cargo/registry /opt/rust/cargo/git && \
54
+ chmod -R a+rX /opt/rust && \
55
+ chown -R codespace:codespace /opt/rust/rustup /opt/rust/cargo && \
56
+ printf 'export RUSTUP_HOME=/opt/rust/rustup\nexport CARGO_HOME=/opt/rust/cargo\nexport PATH=/opt/rust/cargo/bin:$PATH\n' \
57
+ > /etc/profile.d/rust.sh
58
+
37
59
  # Playwright + Chromium: pre-install so containers work with --network=none.
38
60
  # Install system deps as root, then browser to a shared path.
39
61
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
@@ -60,6 +60,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends python3-pip &&
60
60
 
61
61
  RUN npm install -g node-gyp
62
62
 
63
+ # Rust toolchain (rustc, cargo) + cargo-fuzz for the vuln-discovery workflow.
64
+ # Containers run with --network=none, so the toolchain and the nightly
65
+ # channel that cargo-fuzz invokes must be baked into the image. Installed
66
+ # system-wide under /opt/rust so the codespace user inherits it via ENV.
67
+ ENV RUSTUP_HOME=/opt/rust/rustup
68
+ ENV CARGO_HOME=/opt/rust/cargo
69
+ ENV PATH=/opt/rust/cargo/bin:$PATH
70
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
71
+ sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal \
72
+ --component rust-src && \
73
+ rustup toolchain install nightly --profile minimal --component rust-src && \
74
+ cargo install cargo-fuzz && \
75
+ rm -rf /opt/rust/cargo/registry /opt/rust/cargo/git && \
76
+ chmod -R a+rX /opt/rust && \
77
+ chown -R codespace:codespace /opt/rust/rustup /opt/rust/cargo && \
78
+ printf 'export RUSTUP_HOME=/opt/rust/rustup\nexport CARGO_HOME=/opt/rust/cargo\nexport PATH=/opt/rust/cargo/bin:$PATH\n' \
79
+ > /etc/profile.d/rust.sh
80
+
63
81
  # Playwright + Chromium: pre-install so containers work with --network=none.
64
82
  # Install system deps as root, then browser to a shared path.
65
83
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
@@ -5,7 +5,9 @@ USER root
5
5
  RUN npm install -g @anthropic-ai/claude-code
6
6
 
7
7
  # The entrypoint script copies runtime-generated config into the right
8
- # location before the agent is invoked via docker exec.
8
+ # location before the agent is invoked via docker exec. It sources the
9
+ # shared UID/GID remap helper, so that must be present too.
10
+ COPY entrypoint-uid-remap.sh /usr/local/bin/ironcurtain-uid-remap.sh
9
11
  COPY entrypoint-claude-code.sh /usr/local/bin/entrypoint.sh
10
12
  RUN chmod +x /usr/local/bin/entrypoint.sh
11
13
 
@@ -0,0 +1,17 @@
1
+ FROM ironcurtain-base:latest
2
+
3
+ # Install OpenAI Codex CLI (Node.js is already in the base image).
4
+ # Unpinned (latest), matching Dockerfile.claude-code's `npm install -g`.
5
+ USER root
6
+ RUN npm install -g @openai/codex
7
+
8
+ # The entrypoint sources the shared UID/GID remap helper, so that must be
9
+ # present in the image too.
10
+ COPY entrypoint-uid-remap.sh /usr/local/bin/ironcurtain-uid-remap.sh
11
+ COPY entrypoint-codex.sh /usr/local/bin/entrypoint.sh
12
+ RUN chmod +x /usr/local/bin/entrypoint.sh
13
+
14
+ USER codespace
15
+ RUN mkdir -p /home/codespace/.codex
16
+
17
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -23,7 +23,9 @@ RUN apt-get update && \
23
23
  apt-get autoremove -y && \
24
24
  rm -rf /var/lib/apt/lists/*
25
25
 
26
- # Entrypoint: bridges UDS proxy, writes Goose config, hands off to CMD
26
+ # Entrypoint: bridges UDS proxy, writes Goose config, hands off to CMD.
27
+ # It sources the shared UID/GID remap helper, so that must be present too.
28
+ COPY entrypoint-uid-remap.sh /usr/local/bin/ironcurtain-uid-remap.sh
27
29
  COPY entrypoint-goose.sh /usr/local/bin/entrypoint.sh
28
30
  RUN chmod +x /usr/local/bin/entrypoint.sh
29
31
 
@@ -4,51 +4,12 @@
4
4
  # Non-PTY mode: CMD is "sleep infinity" (agent commands arrive via docker exec).
5
5
  # PTY mode: CMD is the socat PTY command from buildPtyCommand().
6
6
 
7
- # Runtime UID remap (Linux only).
8
- #
9
- # Issue #232: when the host UID is not 1000, bind-mounted directories
10
- # (e.g., the conversation-state dir) appear inside the container owned
11
- # by the host UID, and the baked codespace user (UID 1000) cannot write
12
- # to them. The host-side launcher passes `--user 0:0` plus the host
13
- # UID/GID via IRONCURTAIN_AGENT_UID / IRONCURTAIN_AGENT_GID so this
14
- # block (running as root) can renumber the codespace account to match
15
- # the host before dropping privileges. On macOS, Docker Desktop's
16
- # VirtioFS translates UIDs transparently and the env vars are not set,
17
- # so this block is skipped.
18
- if [ "$(id -u)" = "0" ] && [ -n "$IRONCURTAIN_AGENT_UID" ] && [ -n "$IRONCURTAIN_AGENT_GID" ]; then
19
- if [ "$IRONCURTAIN_AGENT_UID" != "1000" ] || [ "$IRONCURTAIN_AGENT_GID" != "1000" ]; then
20
- # Renumber the codespace user/group to the host UID/GID, then fix
21
- # ownership on the baked home dir and workspace mount so the
22
- # remapped codespace user can read/write them. Bind-mounted
23
- # subdirectories (conversation state, sockets, orientation) keep
24
- # their host-side ownership, which now matches codespace.
25
- #
26
- # FAIL HARD on remap errors. Previously these commands ran without
27
- # return-code checks: if `usermod -u $HOST_UID codespace` collided
28
- # with an existing image user (plausible for system UIDs like 33
29
- # `www-data` or 100 `systemd-network`), the entrypoint would
30
- # silently continue, then `chown` and `runuser -u codespace`
31
- # would operate on the still-UID-1000 codespace — recreating the
32
- # original issue #232 bug with no diagnostic. Abort with an explicit
33
- # error instead so the operator sees what went wrong.
34
- groupmod -g "$IRONCURTAIN_AGENT_GID" codespace || {
35
- echo "[ironcurtain] groupmod failed: cannot remap codespace group to GID $IRONCURTAIN_AGENT_GID (already in use?)" >&2
36
- exit 1
37
- }
38
- usermod -u "$IRONCURTAIN_AGENT_UID" -g "$IRONCURTAIN_AGENT_GID" codespace || {
39
- echo "[ironcurtain] usermod failed: cannot remap codespace user to UID $IRONCURTAIN_AGENT_UID (already in use?)" >&2
40
- exit 1
41
- }
42
- chown -R "$IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" /home/codespace /workspace || {
43
- echo "[ironcurtain] chown failed: cannot reset ownership of /home/codespace and /workspace to $IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" >&2
44
- exit 1
45
- }
46
- fi
47
- # Re-exec the entrypoint as the (possibly remapped) codespace user.
48
- # The remainder of this script runs under codespace, so $HOME and
49
- # sudoers (which keys on the username) resolve correctly.
50
- exec runuser -u codespace -- "$0" "$@"
51
- fi
7
+ # Runtime UID/GID remap (Linux only). The shared helper renumbers the
8
+ # baked codespace user/group to match the host UID/GID (issue #232) and
9
+ # re-execs this entrypoint as codespace. Sourced so it shares this
10
+ # script's $0/$@; see the helper for the full rationale (issues #232 and
11
+ # #291). No-op on macOS and when already running as codespace.
12
+ . /usr/local/bin/ironcurtain-uid-remap.sh
52
13
 
53
14
  # Bridge UDS to local TCP so HTTPS_PROXY works
54
15
  MITM_SOCK="/run/ironcurtain/mitm-proxy.sock"
@@ -86,6 +47,10 @@ fi
86
47
  # Auth mode determines how Claude Code gets its API credentials:
87
48
  # - OAuth mode (CLAUDE_CODE_OAUTH_TOKEN set): Claude Code reads the token from
88
49
  # this env var directly -- no apiKeyHelper needed.
50
+ # - OpenRouter mode (ANTHROPIC_AUTH_TOKEN set, CLAUDE_CODE_OAUTH_TOKEN not):
51
+ # Claude Code reads the bearer token from ANTHROPIC_AUTH_TOKEN directly, so
52
+ # no apiKeyHelper is written (an apiKeyHelper echoing an empty
53
+ # IRONCURTAIN_API_KEY would compete with the bearer token).
89
54
  # - API key mode: apiKeyHelper echoes the fake key so Claude Code skips the
90
55
  # custom API key approval dialog entirely.
91
56
  # Always written (even on resume) because auth mode is runtime-specific.
@@ -108,6 +73,24 @@ if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
108
73
  }
109
74
  }
110
75
  EOSETTINGS
76
+ elif [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then
77
+ # OpenRouter mode: Claude Code reads the bearer token from ANTHROPIC_AUTH_TOKEN
78
+ # directly -- no apiKeyHelper (which would echo an empty IRONCURTAIN_API_KEY).
79
+ cat > "$HOME/.claude/settings.json" <<EOSETTINGS
80
+ {
81
+ "permissions": {
82
+ "allow": [],
83
+ "deny": [],
84
+ "additionalDirectories": [],
85
+ "defaultMode": "bypassPermissions"
86
+ },
87
+ "skipDangerousModePermissionPrompt": true,
88
+ "skipWebFetchPreflight": true,
89
+ "env": {
90
+ "HTTPS_PROXY": "${HTTPS_PROXY}"
91
+ }
92
+ }
93
+ EOSETTINGS
111
94
  else
112
95
  # API key mode: apiKeyHelper echoes the fake key at runtime.
113
96
  cat > "$HOME/.claude/settings.json" <<EOSETTINGS
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # IronCurtain entrypoint for Codex CLI containers.
3
+
4
+ # Runtime UID/GID remap (Linux only). Shared with the other agent
5
+ # entrypoints so the remap logic cannot drift (issues #232 and #291).
6
+ # Sourced so it shares this script's $0/$@; no-op on macOS and when
7
+ # already running as codespace.
8
+ . /usr/local/bin/ironcurtain-uid-remap.sh
9
+
10
+ MITM_SOCK="/run/ironcurtain/mitm-proxy.sock"
11
+ PROXY_PORT=18080
12
+ if [ -S "$MITM_SOCK" ]; then
13
+ socat TCP-LISTEN:$PROXY_PORT,fork,reuseaddr UNIX-CONNECT:$MITM_SOCK &
14
+ fi
15
+
16
+ CODEX_CONFIG_DIR="${CODEX_HOME:-$HOME/.codex}"
17
+ mkdir -p "$CODEX_CONFIG_DIR"
18
+ if [ -f /etc/ironcurtain/codex-config.toml ]; then
19
+ cp /etc/ironcurtain/codex-config.toml "$CODEX_CONFIG_DIR/config.toml"
20
+ fi
21
+ if [ -n "$IRONCURTAIN_CODEX_ACCESS_TOKEN" ] && [ -n "$IRONCURTAIN_CODEX_ID_TOKEN" ]; then
22
+ cat > "$CODEX_CONFIG_DIR/auth.json" <<EOJSON
23
+ {
24
+ "auth_mode": "chatgptAuthTokens",
25
+ "tokens": {
26
+ "id_token": "$IRONCURTAIN_CODEX_ID_TOKEN",
27
+ "access_token": "$IRONCURTAIN_CODEX_ACCESS_TOKEN",
28
+ "refresh_token": "",
29
+ "account_id": "${IRONCURTAIN_CODEX_ACCOUNT_ID:-ironcurtain-account}"
30
+ },
31
+ "last_refresh": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
32
+ }
33
+ EOJSON
34
+ chmod 600 "$CODEX_CONFIG_DIR/auth.json"
35
+ fi
36
+
37
+ if [ -f /etc/ironcurtain/system-prompt.txt ]; then
38
+ export IRONCURTAIN_SYSTEM_PROMPT
39
+ IRONCURTAIN_SYSTEM_PROMPT=$(cat /etc/ironcurtain/system-prompt.txt)
40
+ fi
41
+
42
+ exec "$@"
@@ -2,30 +2,11 @@
2
2
  # IronCurtain entrypoint for Goose containers.
3
3
  # Sets up proxy bridges, writes Goose config, then hands off to CMD.
4
4
 
5
- # Runtime UID remap (Linux only). See entrypoint-claude-code.sh for the
6
- # full rationale (issue #232). Skipped on macOS where Docker Desktop
7
- # translates UIDs and the env vars are not set.
8
- if [ "$(id -u)" = "0" ] && [ -n "$IRONCURTAIN_AGENT_UID" ] && [ -n "$IRONCURTAIN_AGENT_GID" ]; then
9
- if [ "$IRONCURTAIN_AGENT_UID" != "1000" ] || [ "$IRONCURTAIN_AGENT_GID" != "1000" ]; then
10
- # Fail hard on remap errors — see entrypoint-claude-code.sh for the
11
- # full rationale. Without explicit checks, a UID collision (host UID
12
- # already in use by a system user baked into the image) silently
13
- # recreates the original issue #232 bug.
14
- groupmod -g "$IRONCURTAIN_AGENT_GID" codespace || {
15
- echo "[ironcurtain] groupmod failed: cannot remap codespace group to GID $IRONCURTAIN_AGENT_GID (already in use?)" >&2
16
- exit 1
17
- }
18
- usermod -u "$IRONCURTAIN_AGENT_UID" -g "$IRONCURTAIN_AGENT_GID" codespace || {
19
- echo "[ironcurtain] usermod failed: cannot remap codespace user to UID $IRONCURTAIN_AGENT_UID (already in use?)" >&2
20
- exit 1
21
- }
22
- chown -R "$IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" /home/codespace /workspace || {
23
- echo "[ironcurtain] chown failed: cannot reset ownership of /home/codespace and /workspace to $IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" >&2
24
- exit 1
25
- }
26
- fi
27
- exec runuser -u codespace -- "$0" "$@"
28
- fi
5
+ # Runtime UID/GID remap (Linux only). Shared with the other agent
6
+ # entrypoints so the remap logic cannot drift (issues #232 and #291).
7
+ # Sourced so it shares this script's $0/$@; no-op on macOS and when
8
+ # already running as codespace.
9
+ . /usr/local/bin/ironcurtain-uid-remap.sh
29
10
 
30
11
  # 1. Bridge MITM proxy UDS to local TCP (same as Claude Code entrypoint)
31
12
  MITM_SOCK="/run/ironcurtain/mitm-proxy.sock"
@@ -0,0 +1,93 @@
1
+ # shellcheck shell=bash
2
+ # Shared runtime UID/GID remap block for IronCurtain agent entrypoints
3
+ # (Linux only). SOURCED — not executed — from each agent's
4
+ # /usr/local/bin/entrypoint.sh so the three adapters (claude-code, goose,
5
+ # codex) cannot drift apart. Issue #291 was caused by exactly that drift:
6
+ # only the goose entrypoint got the GID-collision fix in the field report.
7
+ #
8
+ # Sourcing contract:
9
+ # - This file is `source`d near the top of the parent entrypoint, so
10
+ # `$0` and `$@` resolve to the PARENT entrypoint's path and args. The
11
+ # `exec runuser -u codespace -- "$0" "$@"` below therefore re-execs the
12
+ # parent entrypoint (/usr/local/bin/entrypoint.sh) with its original
13
+ # args — which is what we want. `exec` replaces the current process even
14
+ # when reached from a sourced file.
15
+ # - After the re-exec, the parent entrypoint runs again and sources this
16
+ # file again, but `id -u` is no longer 0, so the whole block is a no-op
17
+ # the second time through and execution falls past it to the rest of the
18
+ # parent entrypoint (now running as codespace).
19
+ #
20
+ # Issue #232: when the host UID is not 1000, bind-mounted directories
21
+ # (e.g., the conversation-state dir) appear inside the container owned by
22
+ # the host UID, and the baked codespace user (UID 1000) cannot write to
23
+ # them. The host-side launcher passes `--user 0:0` plus the host UID/GID via
24
+ # IRONCURTAIN_AGENT_UID / IRONCURTAIN_AGENT_GID so this block (running as
25
+ # root) can renumber the codespace account to match the host before
26
+ # dropping privileges. On macOS, Docker Desktop's VirtioFS translates UIDs
27
+ # transparently and the env vars are not set, so this block is skipped.
28
+ if [ "$(id -u)" = "0" ] && [ -n "$IRONCURTAIN_AGENT_UID" ] && [ -n "$IRONCURTAIN_AGENT_GID" ]; then
29
+ if [ "$IRONCURTAIN_AGENT_UID" != "1000" ] || [ "$IRONCURTAIN_AGENT_GID" != "1000" ]; then
30
+ # Renumber the codespace user/group to the host UID/GID, then fix
31
+ # ownership on the baked home dir and workspace mount so the remapped
32
+ # codespace user can read/write them. Bind-mounted subdirectories
33
+ # (conversation state, sockets, orientation) keep their host-side
34
+ # ownership, which now matches codespace.
35
+ #
36
+ # GID and UID collisions are handled asymmetrically because they mean
37
+ # different things:
38
+ #
39
+ # - A GID collision is BENIGN. It just means the host's primary group
40
+ # already exists inside the image (issue #291: the universal
41
+ # devcontainer image bakes `users:x:100`, and on nixos/WSL and many
42
+ # Linux desktops the invoking user's primary group is GID 100).
43
+ # Sharing a group is ordinary Unix, so we point codespace's PRIMARY
44
+ # group at the existing group via `usermod -g` (which REQUIRES the
45
+ # target GID to already exist). Only when the GID is free do we
46
+ # renumber codespace's own group via `groupmod -g`. Previously this
47
+ # unconditionally ran `groupmod -g`, which fails with
48
+ # "GID already exists" and aborted startup.
49
+ #
50
+ # - A UID collision is DANGEROUS and must FAIL HARD. It means the host
51
+ # UID is already a distinct baked-in account (e.g. www-data=33).
52
+ # Silently continuing would leave codespace at UID 1000 and then
53
+ # `chown` / `runuser -u codespace` would operate on the wrong
54
+ # account — recreating the original issue #232 bug with no
55
+ # diagnostic. This is the regression guard from commit 2f463f3.
56
+ current_gid="$(id -g codespace)"
57
+ if [ "$current_gid" != "$IRONCURTAIN_AGENT_GID" ]; then
58
+ if getent group "$IRONCURTAIN_AGENT_GID" >/dev/null 2>&1; then
59
+ # GID already exists in the image (benign collision): reuse it as
60
+ # codespace's primary group instead of renumbering codespace's own
61
+ # group onto an occupied GID.
62
+ usermod -g "$IRONCURTAIN_AGENT_GID" codespace || {
63
+ echo "[ironcurtain] usermod failed: cannot set codespace primary group to existing GID $IRONCURTAIN_AGENT_GID" >&2
64
+ exit 1
65
+ }
66
+ else
67
+ # GID is free: renumber codespace's own group to it.
68
+ groupmod -g "$IRONCURTAIN_AGENT_GID" codespace || {
69
+ echo "[ironcurtain] groupmod failed: cannot remap codespace group to GID $IRONCURTAIN_AGENT_GID (already in use?)" >&2
70
+ exit 1
71
+ }
72
+ fi
73
+ fi
74
+
75
+ current_uid="$(id -u codespace)"
76
+ if [ "$current_uid" != "$IRONCURTAIN_AGENT_UID" ]; then
77
+ usermod -u "$IRONCURTAIN_AGENT_UID" -g "$IRONCURTAIN_AGENT_GID" codespace || {
78
+ echo "[ironcurtain] usermod failed: cannot remap codespace user to UID $IRONCURTAIN_AGENT_UID (already in use?)" >&2
79
+ exit 1
80
+ }
81
+ fi
82
+
83
+ chown -R "$IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" /home/codespace /workspace || {
84
+ echo "[ironcurtain] chown failed: cannot reset ownership of /home/codespace and /workspace to $IRONCURTAIN_AGENT_UID:$IRONCURTAIN_AGENT_GID" >&2
85
+ exit 1
86
+ }
87
+ fi
88
+ # Re-exec the entrypoint as the (possibly remapped) codespace user. The
89
+ # remainder of the parent entrypoint runs under codespace, so $HOME and
90
+ # sudoers (which keys on the username) resolve correctly. `$0`/`$@` are
91
+ # the parent entrypoint's because this file is sourced, not executed.
92
+ exec runuser -u codespace -- "$0" "$@"
93
+ fi