@provos/ironcurtain 0.10.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 (660) hide show
  1. package/README.md +68 -14
  2. package/dist/auth/oauth-provider.js +1 -4
  3. package/dist/auth/oauth-provider.js.map +1 -1
  4. package/dist/cli-help.d.ts +12 -0
  5. package/dist/cli-help.js +21 -0
  6. package/dist/cli-help.js.map +1 -1
  7. package/dist/cli.js +46 -6
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/agent-model-guard.d.ts +14 -0
  10. package/dist/config/agent-model-guard.js +24 -0
  11. package/dist/config/agent-model-guard.js.map +1 -0
  12. package/dist/config/config-command.js +277 -7
  13. package/dist/config/config-command.js.map +1 -1
  14. package/dist/config/first-start.js +62 -7
  15. package/dist/config/first-start.js.map +1 -1
  16. package/dist/config/generated/compiled-policy.json +528 -161
  17. package/dist/config/generated/test-scenarios.json +4810 -170
  18. package/dist/config/generated/tool-annotations.json +34 -47
  19. package/dist/config/index.d.ts +58 -0
  20. package/dist/config/index.js +97 -15
  21. package/dist/config/index.js.map +1 -1
  22. package/dist/config/mcp-servers.json +2 -2
  23. package/dist/config/model-provider.d.ts +9 -0
  24. package/dist/config/model-provider.js +13 -0
  25. package/dist/config/model-provider.js.map +1 -1
  26. package/dist/config/paths.d.ts +217 -0
  27. package/dist/config/paths.js +363 -14
  28. package/dist/config/paths.js.map +1 -1
  29. package/dist/config/types.d.ts +7 -1
  30. package/dist/config/user-config.d.ts +101 -6
  31. package/dist/config/user-config.js +145 -31
  32. package/dist/config/user-config.js.map +1 -1
  33. package/dist/config/validate-policy-dir.d.ts +10 -0
  34. package/dist/config/validate-policy-dir.js +59 -0
  35. package/dist/config/validate-policy-dir.js.map +1 -0
  36. package/dist/cron/job-commands.js +22 -2
  37. package/dist/cron/job-commands.js.map +1 -1
  38. package/dist/cron/types.d.ts +16 -2
  39. package/dist/cron/types.js +1 -3
  40. package/dist/cron/types.js.map +1 -1
  41. package/dist/daemon/daemon-command.js +30 -7
  42. package/dist/daemon/daemon-command.js.map +1 -1
  43. package/dist/daemon/ironcurtain-daemon.d.ts +62 -1
  44. package/dist/daemon/ironcurtain-daemon.js +231 -8
  45. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  46. package/dist/daemon-client/daemon-client.d.ts +123 -0
  47. package/dist/daemon-client/daemon-client.js +324 -0
  48. package/dist/daemon-client/daemon-client.js.map +1 -0
  49. package/dist/docker/adapters/claude-code.js +194 -20
  50. package/dist/docker/adapters/claude-code.js.map +1 -1
  51. package/dist/docker/adapters/codex.d.ts +9 -0
  52. package/dist/docker/adapters/codex.js +297 -0
  53. package/dist/docker/adapters/codex.js.map +1 -0
  54. package/dist/docker/adapters/goose.d.ts +12 -11
  55. package/dist/docker/adapters/goose.js +80 -27
  56. package/dist/docker/adapters/goose.js.map +1 -1
  57. package/dist/docker/agent-adapter.d.ts +183 -5
  58. package/dist/docker/agent-adapter.js +16 -0
  59. package/dist/docker/agent-adapter.js.map +1 -1
  60. package/dist/docker/agent-registry.js +5 -0
  61. package/dist/docker/agent-registry.js.map +1 -1
  62. package/dist/docker/apple-container-manager.d.ts +49 -0
  63. package/dist/docker/apple-container-manager.js +464 -0
  64. package/dist/docker/apple-container-manager.js.map +1 -0
  65. package/dist/docker/code-mode-proxy.d.ts +38 -0
  66. package/dist/docker/code-mode-proxy.js +6 -1
  67. package/dist/docker/code-mode-proxy.js.map +1 -1
  68. package/dist/docker/container-lifecycle.d.ts +2 -2
  69. package/dist/docker/container-lifecycle.js.map +1 -1
  70. package/dist/docker/container-runtime.d.ts +29 -0
  71. package/dist/docker/container-runtime.js +76 -0
  72. package/dist/docker/container-runtime.js.map +1 -0
  73. package/dist/docker/docker-agent-session.d.ts +113 -62
  74. package/dist/docker/docker-agent-session.js +271 -294
  75. package/dist/docker/docker-agent-session.js.map +1 -1
  76. package/dist/docker/docker-infrastructure.d.ts +428 -26
  77. package/dist/docker/docker-infrastructure.js +1005 -70
  78. package/dist/docker/docker-infrastructure.js.map +1 -1
  79. package/dist/docker/docker-manager.d.ts +58 -3
  80. package/dist/docker/docker-manager.js +378 -33
  81. package/dist/docker/docker-manager.js.map +1 -1
  82. package/dist/docker/docker-probe.d.ts +45 -0
  83. package/dist/docker/docker-probe.js +86 -0
  84. package/dist/docker/docker-probe.js.map +1 -0
  85. package/dist/docker/docker-progress-sink.d.ts +43 -0
  86. package/dist/docker/docker-progress-sink.js +299 -0
  87. package/dist/docker/docker-progress-sink.js.map +1 -0
  88. package/dist/docker/mitm-proxy.d.ts +135 -1
  89. package/dist/docker/mitm-proxy.js +611 -25
  90. package/dist/docker/mitm-proxy.js.map +1 -1
  91. package/dist/docker/network-topology.d.ts +81 -0
  92. package/dist/docker/network-topology.js +160 -0
  93. package/dist/docker/network-topology.js.map +1 -0
  94. package/dist/docker/oauth-credentials.d.ts +59 -3
  95. package/dist/docker/oauth-credentials.js +181 -14
  96. package/dist/docker/oauth-credentials.js.map +1 -1
  97. package/dist/docker/oauth-token-manager.js +2 -2
  98. package/dist/docker/oauth-token-manager.js.map +1 -1
  99. package/dist/docker/orientation.d.ts +1 -1
  100. package/dist/docker/package-types.d.ts +1 -1
  101. package/dist/docker/provider-config.d.ts +78 -3
  102. package/dist/docker/provider-config.js +295 -31
  103. package/dist/docker/provider-config.js.map +1 -1
  104. package/dist/docker/provision-lock.d.ts +46 -0
  105. package/dist/docker/provision-lock.js +152 -0
  106. package/dist/docker/provision-lock.js.map +1 -0
  107. package/dist/docker/proxy-tools.d.ts +0 -2
  108. package/dist/docker/proxy-tools.js +2 -7
  109. package/dist/docker/proxy-tools.js.map +1 -1
  110. package/dist/docker/pty-session.d.ts +64 -1
  111. package/dist/docker/pty-session.js +306 -104
  112. package/dist/docker/pty-session.js.map +1 -1
  113. package/dist/docker/pty-types.d.ts +0 -2
  114. package/dist/docker/pty-types.js +0 -2
  115. package/dist/docker/pty-types.js.map +1 -1
  116. package/dist/docker/registry-proxy.d.ts +46 -0
  117. package/dist/docker/registry-proxy.js +255 -1
  118. package/dist/docker/registry-proxy.js.map +1 -1
  119. package/dist/docker/resource-limits.d.ts +134 -0
  120. package/dist/docker/resource-limits.js +203 -0
  121. package/dist/docker/resource-limits.js.map +1 -0
  122. package/dist/docker/spawn-with-idle-timeout.d.ts +39 -0
  123. package/dist/docker/spawn-with-idle-timeout.js +152 -0
  124. package/dist/docker/spawn-with-idle-timeout.js.map +1 -0
  125. package/dist/docker/sse-extractor.d.ts +51 -0
  126. package/dist/docker/sse-extractor.js +303 -0
  127. package/dist/docker/sse-extractor.js.map +1 -0
  128. package/dist/docker/token-stream-bus.d.ts +83 -0
  129. package/dist/docker/token-stream-bus.js +98 -0
  130. package/dist/docker/token-stream-bus.js.map +1 -0
  131. package/dist/docker/token-stream-types.d.ts +71 -0
  132. package/dist/docker/token-stream-types.js +8 -0
  133. package/dist/docker/token-stream-types.js.map +1 -0
  134. package/dist/docker/trajectory-capture.d.ts +79 -0
  135. package/dist/docker/trajectory-capture.js +521 -0
  136. package/dist/docker/trajectory-capture.js.map +1 -0
  137. package/dist/docker/trajectory-reassembler.d.ts +286 -0
  138. package/dist/docker/trajectory-reassembler.js +990 -0
  139. package/dist/docker/trajectory-reassembler.js.map +1 -0
  140. package/dist/docker/trajectory-tap.d.ts +100 -0
  141. package/dist/docker/trajectory-tap.js +410 -0
  142. package/dist/docker/trajectory-tap.js.map +1 -0
  143. package/dist/docker/trajectory-types.d.ts +187 -0
  144. package/dist/docker/trajectory-types.js +43 -0
  145. package/dist/docker/trajectory-types.js.map +1 -0
  146. package/dist/docker/types.d.ts +105 -12
  147. package/dist/doctor/checks.d.ts +171 -0
  148. package/dist/doctor/checks.js +598 -0
  149. package/dist/doctor/checks.js.map +1 -0
  150. package/dist/doctor/doctor-command.d.ts +29 -0
  151. package/dist/doctor/doctor-command.js +190 -0
  152. package/dist/doctor/doctor-command.js.map +1 -0
  153. package/dist/doctor/mcp-liveness.d.ts +28 -0
  154. package/dist/doctor/mcp-liveness.js +92 -0
  155. package/dist/doctor/mcp-liveness.js.map +1 -0
  156. package/dist/doctor/oauth-checks.d.ts +23 -0
  157. package/dist/doctor/oauth-checks.js +157 -0
  158. package/dist/doctor/oauth-checks.js.map +1 -0
  159. package/dist/doctor/output.d.ts +26 -0
  160. package/dist/doctor/output.js +82 -0
  161. package/dist/doctor/output.js.map +1 -0
  162. package/dist/escalation/listener-command.js +0 -3
  163. package/dist/escalation/listener-command.js.map +1 -1
  164. package/dist/event-bus/typed-event-bus.d.ts +26 -0
  165. package/dist/event-bus/typed-event-bus.js +35 -0
  166. package/dist/event-bus/typed-event-bus.js.map +1 -0
  167. package/dist/index.js +54 -21
  168. package/dist/index.js.map +1 -1
  169. package/dist/logger.js +31 -2
  170. package/dist/logger.js.map +1 -1
  171. package/dist/memory/auto-save.d.ts +18 -4
  172. package/dist/memory/auto-save.js +46 -6
  173. package/dist/memory/auto-save.js.map +1 -1
  174. package/dist/memory/memory-policy.d.ts +41 -0
  175. package/dist/memory/memory-policy.js +36 -0
  176. package/dist/memory/memory-policy.js.map +1 -0
  177. package/dist/mux/mux-app.d.ts +2 -0
  178. package/dist/mux/mux-app.js +1 -0
  179. package/dist/mux/mux-app.js.map +1 -1
  180. package/dist/mux/mux-command.d.ts +10 -1
  181. package/dist/mux/mux-command.js +74 -40
  182. package/dist/mux/mux-command.js.map +1 -1
  183. package/dist/mux/mux-renderer.js +2 -12
  184. package/dist/mux/mux-renderer.js.map +1 -1
  185. package/dist/mux/pty-bridge.d.ts +2 -0
  186. package/dist/mux/pty-bridge.js +3 -0
  187. package/dist/mux/pty-bridge.js.map +1 -1
  188. package/dist/mux/session-scanner.js +1 -2
  189. package/dist/mux/session-scanner.js.map +1 -1
  190. package/dist/{pipeline → observability}/llm-logger.d.ts +8 -3
  191. package/dist/{pipeline → observability}/llm-logger.js +8 -3
  192. package/dist/observability/llm-logger.js.map +1 -0
  193. package/dist/observe/observe-command.d.ts +12 -0
  194. package/dist/observe/observe-command.js +285 -0
  195. package/dist/observe/observe-command.js.map +1 -0
  196. package/dist/observe/observe-renderer.d.ts +24 -0
  197. package/dist/observe/observe-renderer.js +87 -0
  198. package/dist/observe/observe-renderer.js.map +1 -0
  199. package/dist/observe/observe-tui-rain.d.ts +42 -0
  200. package/dist/observe/observe-tui-rain.js +531 -0
  201. package/dist/observe/observe-tui-rain.js.map +1 -0
  202. package/dist/observe/observe-tui-text-panel.d.ts +46 -0
  203. package/dist/observe/observe-tui-text-panel.js +525 -0
  204. package/dist/observe/observe-tui-text-panel.js.map +1 -0
  205. package/dist/observe/observe-tui-types.d.ts +270 -0
  206. package/dist/observe/observe-tui-types.js +200 -0
  207. package/dist/observe/observe-tui-types.js.map +1 -0
  208. package/dist/observe/observe-tui-word-scorer.d.ts +143 -0
  209. package/dist/observe/observe-tui-word-scorer.js +572 -0
  210. package/dist/observe/observe-tui-word-scorer.js.map +1 -0
  211. package/dist/observe/observe-tui.d.ts +59 -0
  212. package/dist/observe/observe-tui.js +592 -0
  213. package/dist/observe/observe-tui.js.map +1 -0
  214. package/dist/persona/broad-policy-validator.d.ts +95 -0
  215. package/dist/persona/broad-policy-validator.js +171 -0
  216. package/dist/persona/broad-policy-validator.js.map +1 -0
  217. package/dist/persona/compile-persona-policy.d.ts +54 -2
  218. package/dist/persona/compile-persona-policy.js +29 -4
  219. package/dist/persona/compile-persona-policy.js.map +1 -1
  220. package/dist/persona/event-bus-progress-reporter.d.ts +57 -0
  221. package/dist/persona/event-bus-progress-reporter.js +66 -0
  222. package/dist/persona/event-bus-progress-reporter.js.map +1 -0
  223. package/dist/persona/memory-gate.d.ts +19 -0
  224. package/dist/persona/memory-gate.js +33 -0
  225. package/dist/persona/memory-gate.js.map +1 -0
  226. package/dist/persona/persona-command.js +77 -72
  227. package/dist/persona/persona-command.js.map +1 -1
  228. package/dist/persona/persona-compile-orchestrator.d.ts +101 -0
  229. package/dist/persona/persona-compile-orchestrator.js +504 -0
  230. package/dist/persona/persona-compile-orchestrator.js.map +1 -0
  231. package/dist/persona/persona-service.d.ts +137 -0
  232. package/dist/persona/persona-service.js +263 -0
  233. package/dist/persona/persona-service.js.map +1 -0
  234. package/dist/persona/policy-mutation-audit.d.ts +109 -0
  235. package/dist/persona/policy-mutation-audit.js +179 -0
  236. package/dist/persona/policy-mutation-audit.js.map +1 -0
  237. package/dist/persona/resolve.d.ts +22 -0
  238. package/dist/persona/resolve.js +45 -2
  239. package/dist/persona/resolve.js.map +1 -1
  240. package/dist/persona/rule-delta.d.ts +30 -0
  241. package/dist/persona/rule-delta.js +82 -0
  242. package/dist/persona/rule-delta.js.map +1 -0
  243. package/dist/persona/types.d.ts +28 -0
  244. package/dist/pipeline/annotate.js +10 -2
  245. package/dist/pipeline/annotate.js.map +1 -1
  246. package/dist/pipeline/compile.js +14 -2
  247. package/dist/pipeline/compile.js.map +1 -1
  248. package/dist/pipeline/constitution-compiler.d.ts +25 -14
  249. package/dist/pipeline/constitution-compiler.js +107 -38
  250. package/dist/pipeline/constitution-compiler.js.map +1 -1
  251. package/dist/pipeline/dynamic-list-types.d.ts +3 -6
  252. package/dist/pipeline/dynamic-list-types.js +3 -16
  253. package/dist/pipeline/dynamic-list-types.js.map +1 -1
  254. package/dist/pipeline/pipeline-runner.d.ts +63 -4
  255. package/dist/pipeline/pipeline-runner.js +242 -94
  256. package/dist/pipeline/pipeline-runner.js.map +1 -1
  257. package/dist/pipeline/pipeline-shared.d.ts +33 -4
  258. package/dist/pipeline/pipeline-shared.js +79 -19
  259. package/dist/pipeline/pipeline-shared.js.map +1 -1
  260. package/dist/pipeline/policy-verifier.js +16 -3
  261. package/dist/pipeline/policy-verifier.js.map +1 -1
  262. package/dist/pipeline/proxy-mcp-connections.js +17 -2
  263. package/dist/pipeline/proxy-mcp-connections.js.map +1 -1
  264. package/dist/pipeline/refresh-lists.js +3 -5
  265. package/dist/pipeline/refresh-lists.js.map +1 -1
  266. package/dist/pipeline/scenario-generator.d.ts +4 -8
  267. package/dist/pipeline/scenario-generator.js +13 -46
  268. package/dist/pipeline/scenario-generator.js.map +1 -1
  269. package/dist/sandbox/index.d.ts +29 -0
  270. package/dist/sandbox/index.js +351 -119
  271. package/dist/sandbox/index.js.map +1 -1
  272. package/dist/sandbox/ironcurtain-protocol.d.ts +52 -0
  273. package/dist/sandbox/ironcurtain-protocol.js +185 -0
  274. package/dist/sandbox/ironcurtain-protocol.js.map +1 -0
  275. package/dist/session/agent-session.d.ts +6 -0
  276. package/dist/session/agent-session.js +16 -5
  277. package/dist/session/agent-session.js.map +1 -1
  278. package/dist/session/cli-transport.js +0 -6
  279. package/dist/session/cli-transport.js.map +1 -1
  280. package/dist/session/index.d.ts +35 -14
  281. package/dist/session/index.js +333 -130
  282. package/dist/session/index.js.map +1 -1
  283. package/dist/session/preflight.d.ts +21 -13
  284. package/dist/session/preflight.js +227 -88
  285. package/dist/session/preflight.js.map +1 -1
  286. package/dist/session/prompts.d.ts +2 -5
  287. package/dist/session/prompts.js.map +1 -1
  288. package/dist/session/session-manager.d.ts +61 -6
  289. package/dist/session/session-manager.js +72 -10
  290. package/dist/session/session-manager.js.map +1 -1
  291. package/dist/session/session-metadata.d.ts +21 -5
  292. package/dist/session/session-metadata.js +34 -12
  293. package/dist/session/session-metadata.js.map +1 -1
  294. package/dist/session/types.d.ts +320 -5
  295. package/dist/session/types.js +57 -1
  296. package/dist/session/types.js.map +1 -1
  297. package/dist/signal/bot-command.js +6 -1
  298. package/dist/signal/bot-command.js.map +1 -1
  299. package/dist/signal/setup-signal.js +13 -21
  300. package/dist/signal/setup-signal.js.map +1 -1
  301. package/dist/signal/signal-bot-daemon.d.ts +16 -1
  302. package/dist/signal/signal-bot-daemon.js +39 -27
  303. package/dist/signal/signal-bot-daemon.js.map +1 -1
  304. package/dist/signal/signal-container.d.ts +3 -3
  305. package/dist/signal/signal-container.js +9 -3
  306. package/dist/signal/signal-container.js.map +1 -1
  307. package/dist/skills/discovery.d.ts +77 -0
  308. package/dist/skills/discovery.js +206 -0
  309. package/dist/skills/discovery.js.map +1 -0
  310. package/dist/skills/index.d.ts +4 -0
  311. package/dist/skills/index.js +3 -0
  312. package/dist/skills/index.js.map +1 -0
  313. package/dist/skills/staging.d.ts +28 -0
  314. package/dist/skills/staging.js +68 -0
  315. package/dist/skills/staging.js.map +1 -0
  316. package/dist/skills/types.d.ts +46 -0
  317. package/dist/skills/types.js +2 -0
  318. package/dist/skills/types.js.map +1 -0
  319. package/dist/trusted-process/approval-whitelist.d.ts +0 -2
  320. package/dist/trusted-process/approval-whitelist.js +17 -17
  321. package/dist/trusted-process/approval-whitelist.js.map +1 -1
  322. package/dist/trusted-process/async-mutex.d.ts +32 -0
  323. package/dist/trusted-process/async-mutex.js +50 -0
  324. package/dist/trusted-process/async-mutex.js.map +1 -0
  325. package/dist/trusted-process/audit-log.d.ts +38 -1
  326. package/dist/trusted-process/audit-log.js +82 -13
  327. package/dist/trusted-process/audit-log.js.map +1 -1
  328. package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
  329. package/dist/trusted-process/call-circuit-breaker.js +23 -3
  330. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  331. package/dist/trusted-process/container-command.d.ts +35 -0
  332. package/dist/trusted-process/container-command.js +49 -0
  333. package/dist/trusted-process/container-command.js.map +1 -0
  334. package/dist/trusted-process/control-server.d.ts +101 -0
  335. package/dist/trusted-process/control-server.js +282 -0
  336. package/dist/trusted-process/control-server.js.map +1 -0
  337. package/dist/trusted-process/domain-utils.d.ts +6 -6
  338. package/dist/trusted-process/domain-utils.js +7 -23
  339. package/dist/trusted-process/domain-utils.js.map +1 -1
  340. package/dist/trusted-process/error-prefixes.d.ts +16 -0
  341. package/dist/trusted-process/error-prefixes.js +17 -0
  342. package/dist/trusted-process/error-prefixes.js.map +1 -0
  343. package/dist/trusted-process/index.d.ts +40 -23
  344. package/dist/trusted-process/index.js +131 -225
  345. package/dist/trusted-process/index.js.map +1 -1
  346. package/dist/trusted-process/list-matcher.d.ts +17 -0
  347. package/dist/trusted-process/list-matcher.js +31 -0
  348. package/dist/trusted-process/list-matcher.js.map +1 -0
  349. package/dist/trusted-process/mcp-client-manager.d.ts +35 -6
  350. package/dist/trusted-process/mcp-client-manager.js +40 -45
  351. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  352. package/dist/trusted-process/mcp-proxy-server.d.ts +31 -120
  353. package/dist/trusted-process/mcp-proxy-server.js +201 -660
  354. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  355. package/dist/trusted-process/policy-engine.d.ts +16 -4
  356. package/dist/trusted-process/policy-engine.js +103 -47
  357. package/dist/trusted-process/policy-engine.js.map +1 -1
  358. package/dist/trusted-process/policy-roots.d.ts +6 -0
  359. package/dist/trusted-process/policy-roots.js +20 -0
  360. package/dist/trusted-process/policy-roots.js.map +1 -1
  361. package/dist/trusted-process/sandbox-integration.d.ts +12 -0
  362. package/dist/trusted-process/sandbox-integration.js +18 -0
  363. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  364. package/dist/trusted-process/tcp-server-transport.d.ts +12 -1
  365. package/dist/trusted-process/tcp-server-transport.js +9 -1
  366. package/dist/trusted-process/tcp-server-transport.js.map +1 -1
  367. package/dist/trusted-process/tool-call-coordinator.d.ts +283 -0
  368. package/dist/trusted-process/tool-call-coordinator.js +592 -0
  369. package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
  370. package/dist/trusted-process/tool-call-pipeline.d.ts +213 -0
  371. package/dist/trusted-process/tool-call-pipeline.js +832 -0
  372. package/dist/trusted-process/tool-call-pipeline.js.map +1 -0
  373. package/dist/trusted-process/tool-description-hints.d.ts +1 -1
  374. package/dist/types/argument-roles.d.ts +1 -1
  375. package/dist/types/argument-roles.js +14 -1
  376. package/dist/types/argument-roles.js.map +1 -1
  377. package/dist/types/audit.d.ts +9 -0
  378. package/dist/{session → types}/errors.d.ts +8 -2
  379. package/dist/{session → types}/errors.js +8 -2
  380. package/dist/types/errors.js.map +1 -0
  381. package/dist/types/reserved-servers.d.ts +11 -0
  382. package/dist/types/reserved-servers.js +12 -0
  383. package/dist/types/reserved-servers.js.map +1 -0
  384. package/dist/types/server-listing.d.ts +14 -0
  385. package/dist/types/server-listing.js +2 -0
  386. package/dist/types/server-listing.js.map +1 -0
  387. package/dist/types/slug.d.ts +2 -0
  388. package/dist/types/slug.js +2 -0
  389. package/dist/types/slug.js.map +1 -1
  390. package/dist/types/url-normalize.d.ts +16 -0
  391. package/dist/types/url-normalize.js +33 -0
  392. package/dist/types/url-normalize.js.map +1 -0
  393. package/dist/util/atomic-write.d.ts +31 -0
  394. package/dist/util/atomic-write.js +43 -0
  395. package/dist/util/atomic-write.js.map +1 -0
  396. package/dist/utils/error-message.d.ts +8 -0
  397. package/dist/utils/error-message.js +11 -0
  398. package/dist/utils/error-message.js.map +1 -0
  399. package/dist/utils/exec-error.d.ts +19 -0
  400. package/dist/utils/exec-error.js +16 -0
  401. package/dist/utils/exec-error.js.map +1 -0
  402. package/dist/utils/is-plain-object.d.ts +7 -0
  403. package/dist/utils/is-plain-object.js +12 -0
  404. package/dist/utils/is-plain-object.js.map +1 -0
  405. package/dist/utils/preflight-checks.d.ts +16 -0
  406. package/dist/utils/preflight-checks.js +178 -0
  407. package/dist/utils/preflight-checks.js.map +1 -0
  408. package/dist/web-ui/__tests__/json-rpc-dispatch.test.d.ts +7 -0
  409. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +725 -0
  410. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -0
  411. package/dist/web-ui/conn-registry.d.ts +14 -0
  412. package/dist/web-ui/conn-registry.js +25 -0
  413. package/dist/web-ui/conn-registry.js.map +1 -0
  414. package/dist/web-ui/dispatch/escalation-dispatch.d.ts +7 -0
  415. package/dist/web-ui/dispatch/escalation-dispatch.js +52 -0
  416. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -0
  417. package/dist/web-ui/dispatch/job-dispatch.d.ts +8 -0
  418. package/dist/web-ui/dispatch/job-dispatch.js +83 -0
  419. package/dist/web-ui/dispatch/job-dispatch.js.map +1 -0
  420. package/dist/web-ui/dispatch/persona-dispatch.d.ts +50 -0
  421. package/dist/web-ui/dispatch/persona-dispatch.js +269 -0
  422. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -0
  423. package/dist/web-ui/dispatch/session-dispatch.d.ts +9 -0
  424. package/dist/web-ui/dispatch/session-dispatch.js +219 -0
  425. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -0
  426. package/dist/web-ui/dispatch/token-stream-dispatch.d.ts +15 -0
  427. package/dist/web-ui/dispatch/token-stream-dispatch.js +55 -0
  428. package/dist/web-ui/dispatch/token-stream-dispatch.js.map +1 -0
  429. package/dist/web-ui/dispatch/types.d.ts +54 -0
  430. package/dist/web-ui/dispatch/types.js +72 -0
  431. package/dist/web-ui/dispatch/types.js.map +1 -0
  432. package/dist/web-ui/dispatch/workflow-dispatch.d.ts +103 -0
  433. package/dist/web-ui/dispatch/workflow-dispatch.js +1036 -0
  434. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -0
  435. package/dist/web-ui/json-rpc-dispatch.d.ts +12 -0
  436. package/dist/web-ui/json-rpc-dispatch.js +34 -0
  437. package/dist/web-ui/json-rpc-dispatch.js.map +1 -0
  438. package/dist/web-ui/state-graph.d.ts +13 -0
  439. package/dist/web-ui/state-graph.js +82 -0
  440. package/dist/web-ui/state-graph.js.map +1 -0
  441. package/dist/web-ui/token-stream-bridge.d.ts +76 -0
  442. package/dist/web-ui/token-stream-bridge.js +265 -0
  443. package/dist/web-ui/token-stream-bridge.js.map +1 -0
  444. package/dist/web-ui/web-event-bus.d.ts +176 -0
  445. package/dist/web-ui/web-event-bus.js +19 -0
  446. package/dist/web-ui/web-event-bus.js.map +1 -0
  447. package/dist/web-ui/web-session-transport.d.ts +31 -0
  448. package/dist/web-ui/web-session-transport.js +109 -0
  449. package/dist/web-ui/web-session-transport.js.map +1 -0
  450. package/dist/web-ui/web-ui-server.d.ts +90 -0
  451. package/dist/web-ui/web-ui-server.js +477 -0
  452. package/dist/web-ui/web-ui-server.js.map +1 -0
  453. package/dist/web-ui/web-ui-types.d.ts +418 -0
  454. package/dist/web-ui/web-ui-types.js +32 -0
  455. package/dist/web-ui/web-ui-types.js.map +1 -0
  456. package/dist/web-ui/ws-utils.d.ts +5 -0
  457. package/dist/web-ui/ws-utils.js +12 -0
  458. package/dist/web-ui/ws-utils.js.map +1 -0
  459. package/dist/web-ui-static/assets/index-BD5U6ILh.css +1 -0
  460. package/dist/web-ui-static/assets/index-FMsHWEfK.js +79 -0
  461. package/dist/web-ui-static/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  462. package/dist/web-ui-static/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  463. package/dist/web-ui-static/index.html +14 -0
  464. package/dist/workflow/artifacts.d.ts +59 -0
  465. package/dist/workflow/artifacts.js +188 -0
  466. package/dist/workflow/artifacts.js.map +1 -0
  467. package/dist/workflow/checkpoint-selection.d.ts +34 -0
  468. package/dist/workflow/checkpoint-selection.js +48 -0
  469. package/dist/workflow/checkpoint-selection.js.map +1 -0
  470. package/dist/workflow/checkpoint.d.ts +35 -0
  471. package/dist/workflow/checkpoint.js +84 -0
  472. package/dist/workflow/checkpoint.js.map +1 -0
  473. package/dist/workflow/cli-shared.d.ts +62 -0
  474. package/dist/workflow/cli-shared.js +117 -0
  475. package/dist/workflow/cli-shared.js.map +1 -0
  476. package/dist/workflow/cli-support.d.ts +92 -0
  477. package/dist/workflow/cli-support.js +421 -0
  478. package/dist/workflow/cli-support.js.map +1 -0
  479. package/dist/workflow/container-snapshots.d.ts +29 -0
  480. package/dist/workflow/container-snapshots.js +137 -0
  481. package/dist/workflow/container-snapshots.js.map +1 -0
  482. package/dist/workflow/daemon-gate-commands.d.ts +21 -0
  483. package/dist/workflow/daemon-gate-commands.js +599 -0
  484. package/dist/workflow/daemon-gate-commands.js.map +1 -0
  485. package/dist/workflow/definition-loader.d.ts +32 -0
  486. package/dist/workflow/definition-loader.js +56 -0
  487. package/dist/workflow/definition-loader.js.map +1 -0
  488. package/dist/workflow/discovery.d.ts +107 -0
  489. package/dist/workflow/discovery.js +263 -0
  490. package/dist/workflow/discovery.js.map +1 -0
  491. package/dist/workflow/errors.d.ts +66 -0
  492. package/dist/workflow/errors.js +49 -0
  493. package/dist/workflow/errors.js.map +1 -0
  494. package/dist/workflow/guards.d.ts +8 -0
  495. package/dist/workflow/guards.js +38 -0
  496. package/dist/workflow/guards.js.map +1 -0
  497. package/dist/workflow/lane-template.d.ts +89 -0
  498. package/dist/workflow/lane-template.js +141 -0
  499. package/dist/workflow/lane-template.js.map +1 -0
  500. package/dist/workflow/lint-integration.d.ts +64 -0
  501. package/dist/workflow/lint-integration.js +56 -0
  502. package/dist/workflow/lint-integration.js.map +1 -0
  503. package/dist/workflow/lint.d.ts +72 -0
  504. package/dist/workflow/lint.js +593 -0
  505. package/dist/workflow/lint.js.map +1 -0
  506. package/dist/workflow/machine-builder.d.ts +178 -0
  507. package/dist/workflow/machine-builder.js +678 -0
  508. package/dist/workflow/machine-builder.js.map +1 -0
  509. package/dist/workflow/message-log.d.ts +99 -0
  510. package/dist/workflow/message-log.js +40 -0
  511. package/dist/workflow/message-log.js.map +1 -0
  512. package/dist/workflow/orchestrator.d.ts +954 -0
  513. package/dist/workflow/orchestrator.js +3594 -0
  514. package/dist/workflow/orchestrator.js.map +1 -0
  515. package/dist/workflow/prompt-builder.d.ts +39 -0
  516. package/dist/workflow/prompt-builder.js +243 -0
  517. package/dist/workflow/prompt-builder.js.map +1 -0
  518. package/dist/workflow/run-state-command.d.ts +9 -0
  519. package/dist/workflow/run-state-command.js +414 -0
  520. package/dist/workflow/run-state-command.js.map +1 -0
  521. package/dist/workflow/run-state-debug-capture.d.ts +40 -0
  522. package/dist/workflow/run-state-debug-capture.js +101 -0
  523. package/dist/workflow/run-state-debug-capture.js.map +1 -0
  524. package/dist/workflow/status-parser.d.ts +68 -0
  525. package/dist/workflow/status-parser.js +274 -0
  526. package/dist/workflow/status-parser.js.map +1 -0
  527. package/dist/workflow/types.d.ts +598 -0
  528. package/dist/workflow/types.js +85 -0
  529. package/dist/workflow/types.js.map +1 -0
  530. package/dist/workflow/validate.d.ts +80 -0
  531. package/dist/workflow/validate.js +788 -0
  532. package/dist/workflow/validate.js.map +1 -0
  533. package/dist/workflow/workflow-command.d.ts +9 -0
  534. package/dist/workflow/workflow-command.js +670 -0
  535. package/dist/workflow/workflow-command.js.map +1 -0
  536. package/dist/workflow/workflow-discovery.d.ts +27 -0
  537. package/dist/workflow/workflow-discovery.js +121 -0
  538. package/dist/workflow/workflow-discovery.js.map +1 -0
  539. package/dist/workflow/workflow-manager.d.ts +156 -0
  540. package/dist/workflow/workflow-manager.js +359 -0
  541. package/dist/workflow/workflow-manager.js.map +1 -0
  542. package/dist/workflow/workflows/design-and-code/README.md +50 -0
  543. package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -0
  544. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  545. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  546. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  547. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  548. package/dist/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  549. package/dist/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  550. package/dist/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  551. package/dist/workflow/workflows/evolve/scripts/LICENSE +201 -0
  552. package/dist/workflow/workflows/evolve/scripts/README.md +314 -0
  553. package/dist/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  554. package/dist/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  555. package/dist/workflow/workflows/evolve/scripts/evolve-db +16 -0
  556. package/dist/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  557. package/dist/workflow/workflows/evolve/scripts/evolve-files +16 -0
  558. package/dist/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  559. package/dist/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  560. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  561. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  562. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  563. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  564. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  565. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  566. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  567. package/dist/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  568. package/dist/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  569. package/dist/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  570. package/dist/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  571. package/dist/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  572. package/dist/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  573. package/dist/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  574. package/dist/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  575. package/dist/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  576. package/dist/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  577. package/dist/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  578. package/dist/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  579. package/dist/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  580. package/dist/workflow/workflows/evolve/workflow.yaml +646 -0
  581. package/dist/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  582. package/dist/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  583. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  584. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  585. package/dist/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  586. package/dist/workflow/workflows/test-email-summary/workflow.yaml +90 -0
  587. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +206 -0
  588. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +167 -0
  589. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +189 -0
  590. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +925 -0
  591. package/dist/workflow/worktree.d.ts +43 -0
  592. package/dist/workflow/worktree.js +106 -0
  593. package/dist/workflow/worktree.js.map +1 -0
  594. package/docker/Dockerfile.base +31 -0
  595. package/docker/Dockerfile.base.arm64 +27 -0
  596. package/docker/Dockerfile.claude-code +3 -1
  597. package/docker/Dockerfile.codex +17 -0
  598. package/docker/Dockerfile.goose +3 -1
  599. package/docker/entrypoint-claude-code.sh +7 -0
  600. package/docker/entrypoint-codex.sh +42 -0
  601. package/docker/entrypoint-goose.sh +6 -0
  602. package/docker/entrypoint-uid-remap.sh +93 -0
  603. package/package.json +27 -11
  604. package/packages/web-ui/README.md +113 -0
  605. package/packages/web-ui/docs/workflow-state-machine.png +0 -0
  606. package/src/config/generated/compiled-policy.json +528 -161
  607. package/src/config/generated/test-scenarios.json +4810 -170
  608. package/src/config/generated/tool-annotations.json +34 -47
  609. package/src/config/mcp-servers.json +2 -2
  610. package/src/workflow/workflows/design-and-code/README.md +50 -0
  611. package/src/workflow/workflows/design-and-code/workflow.yaml +169 -0
  612. package/src/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  613. package/src/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  614. package/src/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  615. package/src/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  616. package/src/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  617. package/src/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  618. package/src/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  619. package/src/workflow/workflows/evolve/scripts/LICENSE +201 -0
  620. package/src/workflow/workflows/evolve/scripts/README.md +314 -0
  621. package/src/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  622. package/src/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  623. package/src/workflow/workflows/evolve/scripts/evolve-db +16 -0
  624. package/src/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  625. package/src/workflow/workflows/evolve/scripts/evolve-files +16 -0
  626. package/src/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  627. package/src/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  628. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  629. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  630. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  631. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  632. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  633. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  634. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  635. package/src/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  636. package/src/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  637. package/src/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  638. package/src/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  639. package/src/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  640. package/src/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  641. package/src/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  642. package/src/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  643. package/src/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  644. package/src/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  645. package/src/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  646. package/src/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  647. package/src/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  648. package/src/workflow/workflows/evolve/workflow.yaml +646 -0
  649. package/src/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  650. package/src/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  651. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  652. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  653. package/src/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  654. package/src/workflow/workflows/test-email-summary/workflow.yaml +90 -0
  655. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +206 -0
  656. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +167 -0
  657. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +189 -0
  658. package/src/workflow/workflows/vuln-discovery/workflow.yaml +925 -0
  659. package/dist/pipeline/llm-logger.js.map +0 -1
  660. package/dist/session/errors.js.map +0 -1
@@ -19,10 +19,14 @@ import * as net from 'node:net';
19
19
  import { existsSync, unlinkSync } from 'node:fs';
20
20
  import forge from 'node-forge';
21
21
  import { randomSerialNumber } from './ca.js';
22
- import { isEndpointAllowed, shouldRewriteBody, } from './provider-config.js';
22
+ import { isCapturableEndpoint, isEndpointAllowed, shouldRewriteBody, } from './provider-config.js';
23
23
  import { DENY_ALL_VALIDATOR } from './package-validator.js';
24
24
  import { validateDomain } from './proxy-tools.js';
25
+ import { PassThrough } from 'node:stream';
26
+ import { getTokenStreamBus } from './token-stream-bus.js';
27
+ import { SseExtractorTransform } from './sse-extractor.js';
25
28
  import * as logger from '../logger.js';
29
+ import { beginCaptureExchange, createResponseCaptureInlet } from './trajectory-tap.js';
26
30
  /** Connection reset errors are routine during proxy shutdown or client disconnect. */
27
31
  function isConnectionReset(err) {
28
32
  return err.code === 'ECONNRESET' || err.code === 'EPIPE';
@@ -30,6 +34,47 @@ function isConnectionReset(err) {
30
34
  const MAX_REWRITE_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
31
35
  /** Max Content-Length for which we buffer solely for 401 retry (1 MB). */
32
36
  const MAX_RETRY_BODY_BYTES = 1 * 1024 * 1024;
37
+ /**
38
+ * Max bytes buffered from a non-streaming JSON response body for token stream
39
+ * extraction. The response is still streamed to the client unchanged; only
40
+ * the extraction side stops capturing once the cap is hit. This protects the
41
+ * proxy from unbounded memory growth on very large completions.
42
+ */
43
+ export const MAX_JSON_RESPONSE_CAPTURE_BYTES = 2 * 1024 * 1024; // 2 MB
44
+ /**
45
+ * Create a size-bounded capture for a JSON response body.
46
+ *
47
+ * Up to `maxBytes` of data is accumulated into an internal chunk array.
48
+ * Once that threshold is crossed, any already-buffered chunks are released
49
+ * (so large responses do not pin memory) and further chunks are ignored.
50
+ * The caller is still responsible for piping the stream through to the
51
+ * client -- this helper only manages the extraction-side buffer.
52
+ */
53
+ export function createBoundedJsonResponseCapture(maxBytes = MAX_JSON_RESPONSE_CAPTURE_BYTES) {
54
+ let chunks = [];
55
+ let capturedBytes = 0;
56
+ return {
57
+ onData(chunk) {
58
+ if (capturedBytes > maxBytes)
59
+ return;
60
+ capturedBytes += chunk.length;
61
+ if (capturedBytes > maxBytes) {
62
+ chunks = [];
63
+ return;
64
+ }
65
+ chunks.push(chunk);
66
+ },
67
+ onEnd(onComplete) {
68
+ if (capturedBytes > maxBytes) {
69
+ onComplete(null);
70
+ return;
71
+ }
72
+ const body = Buffer.concat(chunks);
73
+ chunks = [];
74
+ onComplete(body);
75
+ },
76
+ };
77
+ }
33
78
  /**
34
79
  * Buffers the entire request body into a single Buffer.
35
80
  * Rejects if the body exceeds maxBytes.
@@ -52,17 +97,262 @@ function bufferRequestBody(req, maxBytes) {
52
97
  req.on('error', reject);
53
98
  });
54
99
  }
100
+ /**
101
+ * Maps a target hostname to the SSE provider for token stream parsing.
102
+ * Only Anthropic and OpenAI hosts are recognized; everything else
103
+ * falls back to 'unknown'.
104
+ */
105
+ export function resolveSseProvider(hostname) {
106
+ if (hostname === 'api.anthropic.com' || hostname === 'platform.claude.com') {
107
+ return 'anthropic';
108
+ }
109
+ if (hostname === 'api.openai.com') {
110
+ return 'openai';
111
+ }
112
+ return 'unknown';
113
+ }
114
+ /** Maximum characters for tool_result content before truncation. */
115
+ const MAX_TOOL_RESULT_CONTENT_LEN = 500;
116
+ /** Truncate tool result content to MAX_TOOL_RESULT_CONTENT_LEN, appending ellipsis if needed. */
117
+ function truncateToolResult(text) {
118
+ return text.length > MAX_TOOL_RESULT_CONTENT_LEN ? text.slice(0, MAX_TOOL_RESULT_CONTENT_LEN) + '\u2026' : text;
119
+ }
120
+ /** Returns true for LLM messages endpoints that carry multi-turn request bodies. */
121
+ function isLlmMessagesEndpoint(path) {
122
+ const p = path.split('?')[0];
123
+ return p === '/v1/messages' || p === '/v1/chat/completions';
124
+ }
125
+ /**
126
+ * Extract tool_result blocks from a parsed Anthropic/OpenAI request body
127
+ * and push them as TokenStreamEvents to the bus.
128
+ *
129
+ * Anthropic format: messages[].role === 'user', content[].type === 'tool_result'
130
+ * OpenAI format: messages[].role === 'tool', with tool_call_id
131
+ *
132
+ * Only extracts from the trailing user/tool messages to avoid re-emitting
133
+ * historical tool results (the full conversation history is sent with every request).
134
+ */
135
+ function extractTrailingToolMessages(messages) {
136
+ // Walk backward from the end to find the contiguous block of
137
+ // user (with tool_result) and tool messages at the tail.
138
+ const trailing = [];
139
+ for (let i = messages.length - 1; i >= 0; i--) {
140
+ const msg = messages[i];
141
+ if (typeof msg !== 'object' || msg === null)
142
+ break;
143
+ const role = msg['role'];
144
+ if (role === 'user' || role === 'tool') {
145
+ trailing.push(msg);
146
+ }
147
+ else {
148
+ break;
149
+ }
150
+ }
151
+ trailing.reverse();
152
+ return trailing;
153
+ }
154
+ export function extractToolResults(parsed, sessionId) {
155
+ const messages = parsed['messages'];
156
+ if (!Array.isArray(messages))
157
+ return;
158
+ // Fetch the singleton bus lazily so `resetTokenStreamBus()` between tests
159
+ // is honored at call time (not import time).
160
+ const bus = getTokenStreamBus();
161
+ const now = Date.now();
162
+ // Only extract from the LAST user/tool messages to avoid re-emitting
163
+ // historical tool results from earlier turns (the full conversation
164
+ // history is sent with every API request).
165
+ const lastMessages = extractTrailingToolMessages(messages);
166
+ for (const msg of lastMessages) {
167
+ if (typeof msg !== 'object' || msg === null)
168
+ continue;
169
+ const record = msg;
170
+ // Anthropic format: role=user, content array with tool_result blocks
171
+ if (record['role'] === 'user') {
172
+ const content = record['content'];
173
+ if (!Array.isArray(content))
174
+ continue;
175
+ for (const block of content) {
176
+ if (typeof block !== 'object' || block === null)
177
+ continue;
178
+ const b = block;
179
+ if (b['type'] !== 'tool_result')
180
+ continue;
181
+ const toolUseId = typeof b['tool_use_id'] === 'string' ? b['tool_use_id'] : '';
182
+ const isError = b['is_error'] === true;
183
+ const rawContent = b['content'];
184
+ let text = '';
185
+ if (typeof rawContent === 'string') {
186
+ text = rawContent;
187
+ }
188
+ else if (Array.isArray(rawContent)) {
189
+ // Content can be an array of {type: 'text', text: '...'} blocks
190
+ const parts = [];
191
+ for (const part of rawContent) {
192
+ if (typeof part === 'object' && part !== null && part['type'] === 'text') {
193
+ const t = part['text'];
194
+ if (typeof t === 'string')
195
+ parts.push(t);
196
+ }
197
+ }
198
+ text = parts.join('\n');
199
+ }
200
+ text = truncateToolResult(text);
201
+ bus.push(sessionId, {
202
+ kind: 'tool_result',
203
+ toolUseId,
204
+ toolName: '',
205
+ content: text,
206
+ isError,
207
+ timestamp: now,
208
+ });
209
+ }
210
+ continue;
211
+ }
212
+ // OpenAI format: role=tool, content is a string
213
+ if (record['role'] === 'tool') {
214
+ const toolCallId = typeof record['tool_call_id'] === 'string' ? record['tool_call_id'] : '';
215
+ let text = typeof record['content'] === 'string' ? record['content'] : '';
216
+ const isError = false; // OpenAI doesn't have an is_error field
217
+ text = truncateToolResult(text);
218
+ bus.push(sessionId, {
219
+ kind: 'tool_result',
220
+ toolUseId: toolCallId,
221
+ toolName: '',
222
+ content: text,
223
+ isError,
224
+ timestamp: now,
225
+ });
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Extract token stream events from a non-streaming JSON response body.
231
+ * Used when the upstream returns application/json instead of text/event-stream.
232
+ *
233
+ * Emits message_start (model), text_delta (content), and message_end (usage).
234
+ */
235
+ export function extractFromJsonResponse(body, sessionId) {
236
+ let parsed;
237
+ try {
238
+ parsed = JSON.parse(body.toString());
239
+ }
240
+ catch {
241
+ return; // Not valid JSON -- nothing to extract
242
+ }
243
+ // Fetch the singleton bus lazily so `resetTokenStreamBus()` between tests
244
+ // is honored at call time (not import time).
245
+ const bus = getTokenStreamBus();
246
+ const now = Date.now();
247
+ // Anthropic format: { model, content: [{type: 'text', text: '...'}], usage: {input_tokens, output_tokens}, stop_reason }
248
+ const model = typeof parsed['model'] === 'string' ? parsed['model'] : null;
249
+ if (model) {
250
+ bus.push(sessionId, { kind: 'message_start', model, timestamp: now });
251
+ }
252
+ // Extract text from content blocks (Anthropic) or choices (OpenAI)
253
+ const content = parsed['content'];
254
+ if (Array.isArray(content)) {
255
+ // Anthropic: content is an array of blocks
256
+ for (const block of content) {
257
+ if (typeof block === 'object' && block !== null) {
258
+ const b = block;
259
+ if (b['type'] === 'text' && typeof b['text'] === 'string') {
260
+ bus.push(sessionId, { kind: 'text_delta', text: b['text'], timestamp: now });
261
+ }
262
+ }
263
+ }
264
+ }
265
+ const choices = parsed['choices'];
266
+ if (Array.isArray(choices)) {
267
+ // OpenAI: choices[].message.content
268
+ for (const choice of choices) {
269
+ if (typeof choice === 'object' && choice !== null) {
270
+ const c = choice;
271
+ const message = c['message'];
272
+ if (typeof message === 'object' && message !== null) {
273
+ const m = message;
274
+ if (typeof m['content'] === 'string') {
275
+ bus.push(sessionId, { kind: 'text_delta', text: m['content'], timestamp: now });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ // Usage and stop reason
282
+ const usage = parsed['usage'];
283
+ const stopReason = typeof parsed['stop_reason'] === 'string' ? parsed['stop_reason'] : 'stop';
284
+ let inputTokens = 0;
285
+ let outputTokens = 0;
286
+ if (typeof usage === 'object' && usage !== null) {
287
+ const u = usage;
288
+ // Anthropic: input_tokens/output_tokens; OpenAI: prompt_tokens/completion_tokens
289
+ inputTokens =
290
+ typeof u['input_tokens'] === 'number'
291
+ ? u['input_tokens']
292
+ : typeof u['prompt_tokens'] === 'number'
293
+ ? u['prompt_tokens']
294
+ : 0;
295
+ outputTokens =
296
+ typeof u['output_tokens'] === 'number'
297
+ ? u['output_tokens']
298
+ : typeof u['completion_tokens'] === 'number'
299
+ ? u['completion_tokens']
300
+ : 0;
301
+ }
302
+ bus.push(sessionId, {
303
+ kind: 'message_end',
304
+ stopReason,
305
+ inputTokens,
306
+ outputTokens,
307
+ timestamp: now,
308
+ });
309
+ }
55
310
  export function createMitmProxy(options) {
311
+ // Token stream extractor installation is gated on `tokenSessionId`.
312
+ // This is a mutable per-proxy value that callers flip around each
313
+ // active agent (see `setTokenSessionId` on the returned handle). When
314
+ // `undefined`, all extractor-install sites skip `bus.push()` entirely
315
+ // — no sentinel value is published. The module-scoped singleton bus is
316
+ // fetched lazily at each push site so `resetTokenStreamBus()` between
317
+ // tests is honored.
318
+ let tokenSessionId = options.initialTokenSessionId;
319
+ /**
320
+ * Capture-attribution session ID. Decoupled from `tokenSessionId` so
321
+ * a future extractor-related flip cannot reattribute in-flight
322
+ * captured exchanges. See §11 in the capture design.
323
+ */
324
+ let captureSessionId;
325
+ /** Persona stamped onto subsequent captured ExchangeRecords. */
326
+ let capturePersona;
327
+ const agentKind = options.agentKind;
328
+ const captureWriter = options.capture;
329
+ const captureRecordedAgentName = options.recordedAgentName;
330
+ const captureWorkflowRunId = options.workflowRunId;
331
+ const captureBundleId = options.bundleId;
56
332
  // Parse CA cert and key from PEM
57
333
  const caCert = forge.pki.certificateFromPem(options.ca.certPem);
58
334
  const caKey = forge.pki.privateKeyFromPem(options.ca.keyPem);
335
+ // Hostnames are case-insensitive (RFC 1035 §2.3.3, RFC 7230 §5.4). Normalize
336
+ // to lowercase at every storage and lookup site so the allowlist cannot be
337
+ // bypassed by varying the case (e.g. `CONNECT API.TEST.COM:443` while the
338
+ // provider is registered as `api.test.com`).
339
+ function normalizeHost(hostname) {
340
+ return hostname.toLowerCase();
341
+ }
59
342
  // Build host → provider lookup
60
343
  const providersByHost = new Map();
61
344
  for (const mapping of options.providers) {
62
- providersByHost.set(mapping.config.host, mapping);
345
+ providersByHost.set(normalizeHost(mapping.config.host), mapping);
63
346
  }
64
347
  // Dynamically added passthrough hosts (no key swap, no endpoint filtering)
65
348
  const passthroughHosts = new Set();
349
+ // Escape hatch: when IRONCURTAIN_MITM_ALLOW_ALL_HOSTS is set to "1" or "true",
350
+ // any host that is NOT a configured provider or registry is treated as a
351
+ // passthrough tunnel. Known providers/registries still flow through the
352
+ // normal MITM path (TLS termination, credential swap, endpoint filtering) —
353
+ // this flag only widens the catch-all for unknown destinations. Defeats
354
+ // egress filtering for unknown hosts; only use in trusted environments.
355
+ const allowAllHosts = process.env.IRONCURTAIN_MITM_ALLOW_ALL_HOSTS === '1' || process.env.IRONCURTAIN_MITM_ALLOW_ALL_HOSTS === 'true';
66
356
  // Certificate cache: hostname → { ctx, expiresAt }
67
357
  const certCache = new Map();
68
358
  // Renew leaf certs 1 hour before they expire
@@ -104,14 +394,23 @@ export function createMitmProxy(options) {
104
394
  const registriesByHost = new Map();
105
395
  if (options.registries) {
106
396
  for (const reg of options.registries) {
107
- registriesByHost.set(reg.host, reg);
397
+ registriesByHost.set(normalizeHost(reg.host), reg);
108
398
  if (reg.mirrorHosts) {
109
399
  for (const mirror of reg.mirrorHosts) {
110
- registriesByHost.set(mirror, reg);
400
+ registriesByHost.set(normalizeHost(mirror), reg);
111
401
  }
112
402
  }
113
403
  }
114
404
  }
405
+ // True when a host is statically known to the proxy. Gates the wildcard
406
+ // escape hatch so known providers/registries always follow their dedicated
407
+ // MITM/registry path, never the passthrough tunnel. Accepts the hostname
408
+ // as given (case may vary on the wire); normalization happens here so all
409
+ // callers can trust the result regardless of input case.
410
+ function isKnownStaticHost(hostname) {
411
+ const h = normalizeHost(hostname);
412
+ return providersByHost.has(h) || registriesByHost.has(h);
413
+ }
115
414
  // AllowedVersionCache for tarball backstop
116
415
  const allowedVersionCache = new Map();
117
416
  const activeClientSockets = new Set();
@@ -130,6 +429,42 @@ export function createMitmProxy(options) {
130
429
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
131
430
  }
132
431
  });
432
+ // Fast-reject WebSocket upgrades on TLS-intercepted provider hosts.
433
+ //
434
+ // Codex CLI ≥ 0.142 talks to its inference endpoint over
435
+ // `wss://chatgpt.com/backend-api/codex/responses` (HTTP `GET` with
436
+ // `Upgrade: websocket`) and only falls back to the legacy `POST` + SSE
437
+ // path when the WS connection fails at the TRANSPORT level. Without this
438
+ // listener Node delivers the upgrade request to the `'request'` handler
439
+ // below, which forwards it via `https.request()`; the upstream's `101
440
+ // Switching Protocols` is then dropped on the floor (Node fires only
441
+ // `'close'` on a client request with no `'upgrade'` listener), and the
442
+ // agent's WS client sits on an open socket until its ~15s connect
443
+ // timeout, retrying ~7× (~112s) before falling back.
444
+ //
445
+ // We deliberately DESTROY the socket rather than replying with an HTTP
446
+ // 4xx/5xx: tungstenite surfaces a non-101 HTTP response as a fatal
447
+ // handshake error to the Codex turn (observed: `turn.failed` with the
448
+ // body text), whereas a connection-level failure (reset / EOF before
449
+ // handshake) is treated as a retryable transport error and triggers the
450
+ // POST/SSE fallback — the same path the timeout used to reach, just in
451
+ // milliseconds instead of ~15s per attempt.
452
+ //
453
+ // This is intentionally a fast-fail, NOT a bridge: bridging WS for
454
+ // provider hosts would let inference traffic bypass the SSE trajectory
455
+ // capture / token-stream extraction paths. A proper key-swapping WS
456
+ // bridge (with WS-frame reassembly for capture) is a follow-up; until
457
+ // then the agent's HTTP fallback is the supported transport.
458
+ innerServer.on('upgrade', (req, socket) => {
459
+ const meta = socketMetadata.get(req.socket);
460
+ const targetHost = meta?.host ?? 'unknown';
461
+ logger.info(`[mitm-proxy] REJECTED ${req.headers.upgrade ?? 'upgrade'} ${targetHost}${req.url ?? ''} ` +
462
+ '(WebSocket not bridged for MITM provider hosts; agent should fall back to HTTP)');
463
+ socket.on('error', () => {
464
+ /* swallow — socket is being torn down */
465
+ });
466
+ socket.destroy();
467
+ });
133
468
  // Lazy-loaded registry handler to avoid circular imports.
134
469
  // Cached after first import so subsequent calls are synchronous.
135
470
  let registryHandlerModule;
@@ -193,6 +528,45 @@ export function createMitmProxy(options) {
193
528
  }
194
529
  const provider = meta.provider;
195
530
  const { method, url: path, headers } = clientReq;
531
+ // Per-handler capture-attribution snapshot. Reading once at handler
532
+ // entry keeps every captured byte tied to the session active when
533
+ // the request arrived, even if `setCaptureSessionId(...)` flips
534
+ // mid-exchange. The snapshot is per-closure so HTTP keep-alive
535
+ // concurrent exchanges on the same TLS socket get independent
536
+ // attribution (§11 parallel-keep-alive correctness).
537
+ const sidAtCapture = captureSessionId;
538
+ let captureHandle;
539
+ // Capture-endpoint gate: only training-relevant completion endpoints are
540
+ // recorded. Host-shared housekeeping traffic (registry pagination,
541
+ // telemetry batches, settings/eval pings) is excluded so it never
542
+ // pollutes the corpus. Strictly subtractive — when capture is off or the
543
+ // endpoint isn't capturable, `captureHandle` stays undefined and
544
+ // `attachCaptureBranch` is a no-op; forwarding, key swap, and the
545
+ // 401-retry path are unaffected. The `captureWriter` short-circuit keeps
546
+ // a disabled proxy at zero per-request cost (no header clone, no glob
547
+ // match). See §3 integration point 4 of the capture design doc.
548
+ if (captureWriter && sidAtCapture && isCapturableEndpoint(provider.config, method, path)) {
549
+ // Snapshot the original (pre-key-swap) headers for the capture
550
+ // record. `modifiedHeaders` (below) gets the real key injected by
551
+ // `validateAndSwapApiKey` — we must capture from `headers`, not
552
+ // `modifiedHeaders`. The writer applies a second redaction pass
553
+ // defensively.
554
+ const captureRequestHeaders = { ...headers };
555
+ captureHandle = beginCaptureExchange({
556
+ writer: captureWriter,
557
+ sessionId: sidAtCapture,
558
+ persona: capturePersona,
559
+ workflowRunId: captureWorkflowRunId,
560
+ bundleId: captureBundleId,
561
+ recordedAgentName: captureRecordedAgentName,
562
+ host: targetHost,
563
+ path: String(path),
564
+ method: String(method),
565
+ requestHeaders: captureRequestHeaders,
566
+ requestContentEncoding: clientReq.headers['content-encoding']?.toLowerCase(),
567
+ requestStartedAt: Date.now(),
568
+ });
569
+ }
196
570
  // 1. Endpoint filtering
197
571
  if (!isEndpointAllowed(provider.config, method, path)) {
198
572
  logger.info(`[mitm-proxy] BLOCKED ${method} ${targetHost}${path}`);
@@ -219,11 +593,14 @@ export function createMitmProxy(options) {
219
593
  const needsRewrite = shouldRewriteBody(provider.config, method, path);
220
594
  // 401 retry only makes sense when we swapped our own managed credential
221
595
  const canRetryAuth = keyResult.swapped && !!provider.tokenManager;
222
- // Only buffer for retry when the body is small enough (known Content-Length
223
- // under 1MB). Large or chunked bodies stream through without retry support
224
- // to avoid memory overhead and 413 rejections on large payloads.
596
+ // Only buffer for retry when the body is absent or small enough (known
597
+ // Content-Length under 1MB). Large or chunked bodies stream through without
598
+ // retry support to avoid memory overhead and 413 rejections on large
599
+ // payloads. Bodyless requests (notably Codex's model-list GET) still need
600
+ // the OAuth 401 refresh path.
225
601
  const contentLength = parseInt(clientReq.headers['content-length'] ?? '', 10);
226
- const retryBufferOk = canRetryAuth && Number.isFinite(contentLength) && contentLength <= MAX_RETRY_BODY_BYTES;
602
+ const hasNoRequestBody = clientReq.headers['content-length'] === undefined && clientReq.headers['transfer-encoding'] === undefined;
603
+ const retryBufferOk = canRetryAuth && (hasNoRequestBody || (Number.isFinite(contentLength) && contentLength <= MAX_RETRY_BODY_BYTES));
227
604
  const needsBuffer = needsRewrite || retryBufferOk;
228
605
  /**
229
606
  * Sends the request upstream with optional body override and 401 retry.
@@ -294,7 +671,140 @@ export function createMitmProxy(options) {
294
671
  clientRes.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
295
672
  clientRes.flushHeaders();
296
673
  clientRes.socket?.setNoDelay(true);
297
- upstreamRes.pipe(clientRes);
674
+ const contentType = upstreamRes.headers['content-type'] ?? '';
675
+ // Pin this response's events to the id active when it started,
676
+ // so an orchestrator mid-flight flip can't split one stream.
677
+ const sidAtAttach = tokenSessionId;
678
+ // Trajectory-capture response branch. The 401 refresh-retry
679
+ // path above already returned before reaching here, so the
680
+ // captureTap is bound only to the SUCCESSFUL upstream
681
+ // response — never the drained 401 (§5 "401 refresh-retry
682
+ // semantics").
683
+ //
684
+ // The capture branch is FANNED OUT from the upstream response
685
+ // (it does NOT sit in-series on the forwarding pipeline).
686
+ // Anthropic serves `/v1/messages` SSE with `content-encoding:
687
+ // gzip` by default; the forwarding path must continue to
688
+ // deliver the raw compressed bytes to the agent (its SDK
689
+ // decompresses transparently), while the capture path needs
690
+ // decompressed bytes so the reassembler can parse SSE events.
691
+ // See §3 and §6 invariant 6 of the design doc.
692
+ attachCaptureBranch(upstreamRes, captureHandle, sidAtCapture);
693
+ if (sidAtAttach && contentType.includes('text/event-stream')) {
694
+ const tokenBus = getTokenStreamBus();
695
+ const sseProvider = resolveSseProvider(targetHost);
696
+ const extractor = new SseExtractorTransform(sseProvider, (event) => {
697
+ tokenBus.push(sidAtAttach, event);
698
+ });
699
+ upstreamRes.pipe(extractor).pipe(clientRes);
700
+ }
701
+ else if (sidAtAttach && isLlmMessagesEndpoint(path) && contentType.includes('application/json')) {
702
+ // Non-streaming JSON response: buffer for extraction while piping to client.
703
+ // The passthrough stream always forwards the full response to the client.
704
+ // Capture is bounded at MAX_JSON_RESPONSE_CAPTURE_BYTES: once exceeded,
705
+ // we stop accumulating chunks (and skip extraction at end-of-stream),
706
+ // but the passthrough listener stays attached so the stream completes
707
+ // normally and the client still sees every byte.
708
+ const passthrough = new PassThrough();
709
+ const capture = createBoundedJsonResponseCapture();
710
+ passthrough.on('data', (chunk) => capture.onData(chunk));
711
+ passthrough.on('end', () => {
712
+ capture.onEnd((body) => {
713
+ if (body === null)
714
+ return; // overflowed -- skip extraction
715
+ try {
716
+ extractFromJsonResponse(body, sidAtAttach);
717
+ }
718
+ catch {
719
+ // Extraction errors must never affect the forwarding path
720
+ }
721
+ });
722
+ });
723
+ upstreamRes.pipe(passthrough).pipe(clientRes);
724
+ }
725
+ else {
726
+ upstreamRes.pipe(clientRes);
727
+ }
728
+ /**
729
+ * Attach the trajectory capture branch to `upstreamRes` via a
730
+ * fan-out tee. Bytes the agent receives are unaffected. When
731
+ * `content-encoding` is supported we decompress before feeding
732
+ * the captureTap; unsupported encodings poison the session
733
+ * with `unsupported-encoding`.
734
+ */
735
+ function attachCaptureBranch(upstream, handle, sessionId) {
736
+ if (!handle || !sessionId)
737
+ return;
738
+ const captureTap = handle.attachResponse({
739
+ statusCode: upstream.statusCode ?? 502,
740
+ headers: upstream.headers,
741
+ });
742
+ const inlet = createResponseCaptureInlet({
743
+ captureTap,
744
+ contentEncoding: upstream.headers['content-encoding'],
745
+ captureHandle: handle,
746
+ onPoison: (reason) => {
747
+ try {
748
+ captureWriter?.markSessionPoisoned(sessionId, reason);
749
+ }
750
+ catch {
751
+ /* swallow — capture must never disturb forwarding */
752
+ }
753
+ },
754
+ });
755
+ // Tee upstream bytes into the capture inlet via data
756
+ // listeners. This pattern coexists with the sibling
757
+ // `upstreamRes.pipe(...)` for forwarding because Node
758
+ // broadcasts `data` events to all listeners, and pipe is
759
+ // implemented on top of the same event. Errors / aborts on
760
+ // the upstream stream are propagated to the inlet so the
761
+ // captureTap can settle cleanly (or surface a poison).
762
+ upstream.on('data', (chunk) => {
763
+ if (!inlet.writableEnded && !inlet.destroyed) {
764
+ inlet.write(chunk);
765
+ }
766
+ });
767
+ upstream.on('end', () => {
768
+ if (!inlet.writableEnded && !inlet.destroyed) {
769
+ inlet.end();
770
+ }
771
+ });
772
+ upstream.on('error', () => {
773
+ // On upstream 'error'/'aborted' a terminal event may already
774
+ // be buffered in a decompressor inlet. Prefer a graceful
775
+ // `inlet.end()` flush (NOT destroy) so zlib `_flush` emits the
776
+ // buffered tail and the tap's `canFinalize()` can recover a
777
+ // complete-but-aborted stream; `canFinalize()` is the primary
778
+ // recovery mechanism (codex is uncompressed, so the inlet is
779
+ // the captureTap itself and `end()` simply triggers finalize).
780
+ // Genuine decompressor corruption surfaces as a zlib 'error'
781
+ // on the inlet, which poisons `reassembly-failure` via
782
+ // `createResponseCaptureInlet`'s own handler. Reserve
783
+ // `inlet.destroy()` for the case where the inlet cannot be
784
+ // ended (already errored).
785
+ if (!inlet.writableEnded && !inlet.destroyed) {
786
+ inlet.end();
787
+ }
788
+ });
789
+ upstream.on('close', () => {
790
+ // If `end` already fired, the inlet has been ended. If we
791
+ // got here without `end` and without an `error`, this is a
792
+ // graceful close-after-data (e.g. the client disconnected
793
+ // after the completion). Call `inlet.end()` — NOT
794
+ // `inlet.destroy()` — so a decompressor inlet runs zlib
795
+ // `_flush`, emits its buffered tail + `end` to the
796
+ // captureTap, and the reassembler sees its terminal event.
797
+ // `inlet.destroy(err)` is reserved for genuine upstream
798
+ // `error` (handled above), which would discard the
799
+ // decompressor's pending tail. The tap's `canFinalize()`
800
+ // lifecycle then writes a faithful record for a
801
+ // complete-but-socket-aborted stream, or poisons
802
+ // mid-stream-abort only if the terminal event never landed.
803
+ if (!inlet.writableEnded && !inlet.destroyed) {
804
+ inlet.end();
805
+ }
806
+ });
807
+ }
298
808
  });
299
809
  activeUpstreamRequests.add(upstreamReq);
300
810
  upstreamReq.on('close', () => activeUpstreamRequests.delete(upstreamReq));
@@ -324,7 +834,23 @@ export function createMitmProxy(options) {
324
834
  log(`[mitm-proxy] client request error: ${err.message}`);
325
835
  upstreamReq.destroy();
326
836
  });
327
- clientReq.pipe(upstreamReq);
837
+ // Trajectory-capture tee: when capture is active and we are NOT
838
+ // buffering for rewrite/retry, install a PassThrough between
839
+ // clientReq and upstreamReq so we can observe each request chunk
840
+ // on the way through. The tap's data listener never modifies
841
+ // bytes; the forwarding pipe stays line-rate. Adding a 'data'
842
+ // listener directly to clientReq would flip it to flowing mode
843
+ // and race the upstream pipe — the PassThrough avoids that.
844
+ if (captureHandle) {
845
+ const reqTap = new PassThrough();
846
+ const handle = captureHandle;
847
+ reqTap.on('data', (chunk) => handle.pushRequestChunk(chunk));
848
+ reqTap.on('end', () => handle.finishRequest());
849
+ clientReq.pipe(reqTap).pipe(upstreamReq);
850
+ }
851
+ else {
852
+ clientReq.pipe(upstreamReq);
853
+ }
328
854
  }
329
855
  }
330
856
  /**
@@ -370,17 +896,34 @@ export function createMitmProxy(options) {
370
896
  }
371
897
  return;
372
898
  }
899
+ // Trajectory-capture: feed the buffered raw body into the
900
+ // capture record. This is the pre-rewrite, pre-key-swap body —
901
+ // exactly what the agent emitted. See §3 / §6 invariant #2.
902
+ captureHandle?.setRequestBody(rawBody);
373
903
  let finalBody = rawBody;
904
+ // Extract tool_result events from the request body for token stream observation.
905
+ // Snapshot here (before rewrite) so a concurrent setTokenSessionId
906
+ // flip can't split this request's events across two ids.
907
+ const sidForToolResults = tokenSessionId;
908
+ if (sidForToolResults && isLlmMessagesEndpoint(path)) {
909
+ try {
910
+ const bodyParsed = JSON.parse(rawBody.toString());
911
+ extractToolResults(bodyParsed, sidForToolResults);
912
+ }
913
+ catch {
914
+ // Parse failure is fine -- the rewriter below will also attempt to parse
915
+ }
916
+ }
374
917
  if (needsRewrite) {
375
918
  const rewriter = provider.config.requestRewriter;
376
919
  const reqMethod = method;
377
920
  const reqPath = path;
378
921
  try {
379
922
  const parsed = JSON.parse(rawBody.toString());
380
- const result = rewriter(parsed, { method: reqMethod, path: reqPath });
923
+ const result = rewriter(parsed, { method: reqMethod, path: reqPath, agentKind });
381
924
  if (result) {
382
925
  finalBody = Buffer.from(JSON.stringify(result.modified));
383
- logger.info(`[mitm-proxy] POST ${targetHost}${path} - stripped server-side tools: ${result.stripped.join(', ')}`);
926
+ logger.info(`[mitm-proxy] POST ${targetHost}${path} - stripped tools: ${result.stripped.join(', ')}`);
384
927
  }
385
928
  }
386
929
  catch {
@@ -505,11 +1048,29 @@ export function createMitmProxy(options) {
505
1048
  clientReq.pipe(upstreamReq);
506
1049
  }
507
1050
  // Outer server - UDS listener, handles CONNECT and plain HTTP proxy requests
508
- const outerServer = http.createServer((req, res) => {
509
- // Handle plain HTTP proxy requests for passthrough domains.
1051
+ const outerServer = http.createServer();
1052
+ // Connection-source guard (tcp-hostonly topology): destroy sockets from
1053
+ // outside the allowed subnet before any HTTP parsing happens. Covers
1054
+ // plain requests, CONNECT, and WebSocket upgrades alike since all ride
1055
+ // on the same TCP connections.
1056
+ if (options.allowRemoteAddress) {
1057
+ const allowRemoteAddress = options.allowRemoteAddress;
1058
+ outerServer.on('connection', (socket) => {
1059
+ if (!allowRemoteAddress(socket.remoteAddress)) {
1060
+ socket.destroy();
1061
+ }
1062
+ });
1063
+ }
1064
+ outerServer.on('request', (req, res) => {
1065
+ // Handle plain HTTP proxy requests for passthrough and Debian registry domains.
510
1066
  // HTTP proxy clients send absolute URLs: "GET http://host:port/path".
1067
+ // Only Debian registries are included here because apt uses plain HTTP
1068
+ // by default. npm/PyPI use HTTPS CONNECT and go through the registry-proxy
1069
+ // validation path instead.
511
1070
  const parsed = req.url ? tryParseProxyUrl(req.url) : null;
512
- if (parsed && passthroughHosts.has(parsed.hostname)) {
1071
+ const isDebianRegistry = parsed ? registriesByHost.get(parsed.hostname)?.type === 'debian' : false;
1072
+ const isWildcardEligible = !!parsed && allowAllHosts && !isKnownStaticHost(parsed.hostname);
1073
+ if (parsed && (passthroughHosts.has(parsed.hostname) || isDebianRegistry || isWildcardEligible)) {
513
1074
  forwardPlainHttpPassthrough(req, res, parsed.hostname, parsed.port, parsed.path);
514
1075
  return;
515
1076
  }
@@ -526,7 +1087,12 @@ export function createMitmProxy(options) {
526
1087
  outerServer.on('connect', (req, clientSocket, head) => {
527
1088
  const url = req.url ?? '';
528
1089
  const colonIndex = url.lastIndexOf(':');
529
- const host = colonIndex > 0 ? url.substring(0, colonIndex) : url;
1090
+ const rawHost = colonIndex > 0 ? url.substring(0, colonIndex) : url;
1091
+ // Normalize so the allowlist is case-insensitive (RFC 1035 §2.3.3).
1092
+ // Critical when the wildcard escape hatch is enabled — without this,
1093
+ // `CONNECT API.TEST.COM:443` would miss the provider map and be treated
1094
+ // as an unknown host, opening a raw tunnel that bypasses MITM.
1095
+ const host = normalizeHost(rawHost);
530
1096
  const port = colonIndex > 0 ? parseInt(url.substring(colonIndex + 1), 10) : 443;
531
1097
  const connId = ++connectionId;
532
1098
  // Handle client socket errors early to prevent uncaught 'error' events
@@ -541,14 +1107,17 @@ export function createMitmProxy(options) {
541
1107
  // 1. Check allowlist (providers, registries, and dynamic passthrough)
542
1108
  const provider = providersByHost.get(host);
543
1109
  const registry = registriesByHost.get(host);
544
- const isPassthrough = !provider && !registry && passthroughHosts.has(host);
1110
+ const isDynamicPassthrough = passthroughHosts.has(host);
1111
+ const isWildcardEligible = allowAllHosts && !isKnownStaticHost(host);
1112
+ const isPassthrough = !provider && !registry && (isDynamicPassthrough || isWildcardEligible);
545
1113
  if (!provider && !registry && !isPassthrough) {
546
1114
  logger.info(`[mitm-proxy] #${connId} DENIED CONNECT ${host}:${port}`);
547
1115
  clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
548
1116
  clientSocket.destroy();
549
1117
  return;
550
1118
  }
551
- const connType = provider ? 'provider' : registry ? 'registry' : 'passthrough';
1119
+ const passthroughKind = isDynamicPassthrough ? 'passthrough' : 'passthrough-wildcard';
1120
+ const connType = provider ? 'provider' : registry ? 'registry' : passthroughKind;
552
1121
  // 2. For passthrough domains, create a raw TCP tunnel (no MITM).
553
1122
  // The proxy just pipes bytes bidirectionally — this supports both
554
1123
  // plain HTTP and WebSocket connections through CONNECT.
@@ -644,7 +1213,8 @@ export function createMitmProxy(options) {
644
1213
  // of routing through the normal 'request' handler.
645
1214
  outerServer.on('upgrade', (req, clientSocket, head) => {
646
1215
  const parsed = req.url ? tryParseProxyUrl(req.url) : null;
647
- if (!parsed || !passthroughHosts.has(parsed.hostname)) {
1216
+ const isWildcardEligible = !!parsed && allowAllHosts && !isKnownStaticHost(parsed.hostname);
1217
+ if (!parsed || (!passthroughHosts.has(parsed.hostname) && !isWildcardEligible)) {
648
1218
  clientSocket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
649
1219
  return;
650
1220
  }
@@ -824,17 +1394,18 @@ export function createMitmProxy(options) {
824
1394
  const hostController = {
825
1395
  addHost(domain) {
826
1396
  validateDomain(domain);
827
- if (providersByHost.has(domain))
1397
+ const h = normalizeHost(domain);
1398
+ if (providersByHost.has(h))
828
1399
  return false;
829
- if (passthroughHosts.has(domain))
1400
+ if (passthroughHosts.has(h))
830
1401
  return false;
831
- passthroughHosts.add(domain);
1402
+ passthroughHosts.add(h);
832
1403
  // Pre-warm cert cache for the new domain
833
- getOrCreateSecureContext(domain);
1404
+ getOrCreateSecureContext(h);
834
1405
  return true;
835
1406
  },
836
1407
  removeHost(domain) {
837
- return passthroughHosts.delete(domain);
1408
+ return passthroughHosts.delete(normalizeHost(domain));
838
1409
  },
839
1410
  listHosts() {
840
1411
  return {
@@ -936,10 +1507,25 @@ export function createMitmProxy(options) {
936
1507
  }
937
1508
  return {
938
1509
  hosts: hostController,
1510
+ setTokenSessionId(id) {
1511
+ tokenSessionId = id;
1512
+ },
1513
+ setCaptureSessionId(id) {
1514
+ captureSessionId = id;
1515
+ },
1516
+ setCapturePersona(persona) {
1517
+ capturePersona = persona;
1518
+ },
939
1519
  async start() {
1520
+ // Emit the egress-filter downgrade notice once per actual listening
1521
+ // proxy. Construction-time logging would fire even for proxies that
1522
+ // are created and discarded (tests, aborted setup).
1523
+ if (allowAllHosts) {
1524
+ logger.warn('[mitm-proxy] IRONCURTAIN_MITM_ALLOW_ALL_HOSTS is set — unknown hosts will be tunneled (egress filtering DISABLED for unknown hosts)');
1525
+ }
940
1526
  // Pre-warm cert cache for all configured providers and registries
941
1527
  for (const mapping of options.providers) {
942
- getOrCreateSecureContext(mapping.config.host);
1528
+ getOrCreateSecureContext(normalizeHost(mapping.config.host));
943
1529
  }
944
1530
  for (const host of registriesByHost.keys()) {
945
1531
  getOrCreateSecureContext(host);