@provos/ironcurtain 0.9.1 → 0.11.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 (502) hide show
  1. package/README.md +69 -10
  2. package/dist/auth/oauth-provider.js +1 -4
  3. package/dist/auth/oauth-provider.js.map +1 -1
  4. package/dist/cli.js +40 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config/config-command.js +174 -10
  7. package/dist/config/config-command.js.map +1 -1
  8. package/dist/config/first-start.js +61 -0
  9. package/dist/config/first-start.js.map +1 -1
  10. package/dist/config/generated/compiled-policy.json +528 -161
  11. package/dist/config/generated/test-scenarios.json +4810 -170
  12. package/dist/config/generated/tool-annotations.json +34 -47
  13. package/dist/config/index.d.ts +58 -0
  14. package/dist/config/index.js +97 -15
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/mcp-servers.json +2 -2
  17. package/dist/config/model-provider.d.ts +8 -2
  18. package/dist/config/model-provider.js +52 -9
  19. package/dist/config/model-provider.js.map +1 -1
  20. package/dist/config/paths.d.ts +181 -0
  21. package/dist/config/paths.js +326 -23
  22. package/dist/config/paths.js.map +1 -1
  23. package/dist/config/types.d.ts +7 -1
  24. package/dist/config/user-config.d.ts +61 -4
  25. package/dist/config/user-config.js +108 -21
  26. package/dist/config/user-config.js.map +1 -1
  27. package/dist/config/validate-policy-dir.d.ts +10 -0
  28. package/dist/config/validate-policy-dir.js +59 -0
  29. package/dist/config/validate-policy-dir.js.map +1 -0
  30. package/dist/cron/compile-task-policy.js +1 -0
  31. package/dist/cron/compile-task-policy.js.map +1 -1
  32. package/dist/cron/job-commands.js +22 -2
  33. package/dist/cron/job-commands.js.map +1 -1
  34. package/dist/cron/types.d.ts +16 -2
  35. package/dist/cron/types.js +1 -3
  36. package/dist/cron/types.js.map +1 -1
  37. package/dist/daemon/daemon-command.js +16 -2
  38. package/dist/daemon/daemon-command.js.map +1 -1
  39. package/dist/daemon/ironcurtain-daemon.d.ts +44 -1
  40. package/dist/daemon/ironcurtain-daemon.js +210 -8
  41. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  42. package/dist/docker/adapters/claude-code.d.ts +2 -1
  43. package/dist/docker/adapters/claude-code.js +320 -131
  44. package/dist/docker/adapters/claude-code.js.map +1 -1
  45. package/dist/docker/adapters/goose.d.ts +12 -11
  46. package/dist/docker/adapters/goose.js +80 -27
  47. package/dist/docker/adapters/goose.js.map +1 -1
  48. package/dist/docker/agent-adapter.d.ts +178 -4
  49. package/dist/docker/agent-adapter.js +14 -0
  50. package/dist/docker/agent-adapter.js.map +1 -1
  51. package/dist/docker/agent-registry.js +3 -2
  52. package/dist/docker/agent-registry.js.map +1 -1
  53. package/dist/docker/code-mode-proxy.d.ts +27 -0
  54. package/dist/docker/code-mode-proxy.js +3 -0
  55. package/dist/docker/code-mode-proxy.js.map +1 -1
  56. package/dist/docker/container-lifecycle.d.ts +15 -0
  57. package/dist/docker/container-lifecycle.js +32 -0
  58. package/dist/docker/container-lifecycle.js.map +1 -0
  59. package/dist/docker/docker-agent-session.d.ts +113 -62
  60. package/dist/docker/docker-agent-session.js +241 -300
  61. package/dist/docker/docker-agent-session.js.map +1 -1
  62. package/dist/docker/docker-infrastructure.d.ts +256 -21
  63. package/dist/docker/docker-infrastructure.js +585 -30
  64. package/dist/docker/docker-infrastructure.js.map +1 -1
  65. package/dist/docker/docker-manager.d.ts +36 -1
  66. package/dist/docker/docker-manager.js +168 -26
  67. package/dist/docker/docker-manager.js.map +1 -1
  68. package/dist/docker/docker-progress-sink.d.ts +43 -0
  69. package/dist/docker/docker-progress-sink.js +299 -0
  70. package/dist/docker/docker-progress-sink.js.map +1 -0
  71. package/dist/docker/mitm-proxy.d.ts +90 -1
  72. package/dist/docker/mitm-proxy.js +720 -40
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/oauth-credentials.d.ts +46 -3
  75. package/dist/docker/oauth-credentials.js +56 -14
  76. package/dist/docker/oauth-credentials.js.map +1 -1
  77. package/dist/docker/oauth-token-manager.js +2 -2
  78. package/dist/docker/oauth-token-manager.js.map +1 -1
  79. package/dist/docker/orientation.d.ts +1 -1
  80. package/dist/docker/provider-config.d.ts +81 -2
  81. package/dist/docker/provider-config.js +222 -28
  82. package/dist/docker/provider-config.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +0 -2
  84. package/dist/docker/proxy-tools.js +2 -7
  85. package/dist/docker/proxy-tools.js.map +1 -1
  86. package/dist/docker/pty-session.d.ts +49 -1
  87. package/dist/docker/pty-session.js +212 -90
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +0 -2
  90. package/dist/docker/pty-types.js +0 -2
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/resource-limits.d.ts +125 -0
  93. package/dist/docker/resource-limits.js +200 -0
  94. package/dist/docker/resource-limits.js.map +1 -0
  95. package/dist/docker/spawn-with-idle-timeout.d.ts +39 -0
  96. package/dist/docker/spawn-with-idle-timeout.js +152 -0
  97. package/dist/docker/spawn-with-idle-timeout.js.map +1 -0
  98. package/dist/docker/sse-extractor.d.ts +51 -0
  99. package/dist/docker/sse-extractor.js +303 -0
  100. package/dist/docker/sse-extractor.js.map +1 -0
  101. package/dist/docker/token-stream-bus.d.ts +83 -0
  102. package/dist/docker/token-stream-bus.js +98 -0
  103. package/dist/docker/token-stream-bus.js.map +1 -0
  104. package/dist/docker/token-stream-types.d.ts +71 -0
  105. package/dist/docker/token-stream-types.js +8 -0
  106. package/dist/docker/token-stream-types.js.map +1 -0
  107. package/dist/docker/types.d.ts +61 -6
  108. package/dist/doctor/checks.d.ts +138 -0
  109. package/dist/doctor/checks.js +511 -0
  110. package/dist/doctor/checks.js.map +1 -0
  111. package/dist/doctor/doctor-command.d.ts +29 -0
  112. package/dist/doctor/doctor-command.js +160 -0
  113. package/dist/doctor/doctor-command.js.map +1 -0
  114. package/dist/doctor/mcp-liveness.d.ts +28 -0
  115. package/dist/doctor/mcp-liveness.js +84 -0
  116. package/dist/doctor/mcp-liveness.js.map +1 -0
  117. package/dist/doctor/oauth-checks.d.ts +23 -0
  118. package/dist/doctor/oauth-checks.js +157 -0
  119. package/dist/doctor/oauth-checks.js.map +1 -0
  120. package/dist/doctor/output.d.ts +26 -0
  121. package/dist/doctor/output.js +82 -0
  122. package/dist/doctor/output.js.map +1 -0
  123. package/dist/escalation/listener-command.js +0 -3
  124. package/dist/escalation/listener-command.js.map +1 -1
  125. package/dist/event-bus/typed-event-bus.d.ts +26 -0
  126. package/dist/event-bus/typed-event-bus.js +35 -0
  127. package/dist/event-bus/typed-event-bus.js.map +1 -0
  128. package/dist/index.js +32 -9
  129. package/dist/index.js.map +1 -1
  130. package/dist/logger.js +31 -2
  131. package/dist/logger.js.map +1 -1
  132. package/dist/memory/auto-save.d.ts +18 -4
  133. package/dist/memory/auto-save.js +46 -6
  134. package/dist/memory/auto-save.js.map +1 -1
  135. package/dist/memory/memory-policy.d.ts +41 -0
  136. package/dist/memory/memory-policy.js +36 -0
  137. package/dist/memory/memory-policy.js.map +1 -0
  138. package/dist/mux/mux-app.d.ts +2 -0
  139. package/dist/mux/mux-app.js +30 -34
  140. package/dist/mux/mux-app.js.map +1 -1
  141. package/dist/mux/mux-command.d.ts +10 -1
  142. package/dist/mux/mux-command.js +62 -35
  143. package/dist/mux/mux-command.js.map +1 -1
  144. package/dist/mux/mux-renderer.js +22 -17
  145. package/dist/mux/mux-renderer.js.map +1 -1
  146. package/dist/mux/pty-bridge.d.ts +2 -0
  147. package/dist/mux/pty-bridge.js +3 -0
  148. package/dist/mux/pty-bridge.js.map +1 -1
  149. package/dist/mux/session-scanner.d.ts +7 -0
  150. package/dist/mux/session-scanner.js +21 -2
  151. package/dist/mux/session-scanner.js.map +1 -1
  152. package/dist/mux/types.d.ts +0 -4
  153. package/dist/mux/types.js.map +1 -1
  154. package/dist/{pipeline → observability}/llm-logger.d.ts +9 -3
  155. package/dist/{pipeline → observability}/llm-logger.js +16 -4
  156. package/dist/observability/llm-logger.js.map +1 -0
  157. package/dist/observe/observe-command.d.ts +12 -0
  158. package/dist/observe/observe-command.js +298 -0
  159. package/dist/observe/observe-command.js.map +1 -0
  160. package/dist/observe/observe-renderer.d.ts +24 -0
  161. package/dist/observe/observe-renderer.js +87 -0
  162. package/dist/observe/observe-renderer.js.map +1 -0
  163. package/dist/observe/observe-tui-rain.d.ts +42 -0
  164. package/dist/observe/observe-tui-rain.js +531 -0
  165. package/dist/observe/observe-tui-rain.js.map +1 -0
  166. package/dist/observe/observe-tui-text-panel.d.ts +46 -0
  167. package/dist/observe/observe-tui-text-panel.js +525 -0
  168. package/dist/observe/observe-tui-text-panel.js.map +1 -0
  169. package/dist/observe/observe-tui-types.d.ts +270 -0
  170. package/dist/observe/observe-tui-types.js +200 -0
  171. package/dist/observe/observe-tui-types.js.map +1 -0
  172. package/dist/observe/observe-tui-word-scorer.d.ts +143 -0
  173. package/dist/observe/observe-tui-word-scorer.js +572 -0
  174. package/dist/observe/observe-tui-word-scorer.js.map +1 -0
  175. package/dist/observe/observe-tui.d.ts +59 -0
  176. package/dist/observe/observe-tui.js +592 -0
  177. package/dist/observe/observe-tui.js.map +1 -0
  178. package/dist/persona/compile-persona-policy.js +1 -0
  179. package/dist/persona/compile-persona-policy.js.map +1 -1
  180. package/dist/persona/memory-gate.d.ts +19 -0
  181. package/dist/persona/memory-gate.js +33 -0
  182. package/dist/persona/memory-gate.js.map +1 -0
  183. package/dist/persona/persona-command.js +41 -6
  184. package/dist/persona/persona-command.js.map +1 -1
  185. package/dist/persona/resolve.d.ts +22 -0
  186. package/dist/persona/resolve.js +45 -2
  187. package/dist/persona/resolve.js.map +1 -1
  188. package/dist/persona/types.d.ts +16 -0
  189. package/dist/pipeline/annotate.d.ts +7 -1
  190. package/dist/pipeline/annotate.js +92 -2
  191. package/dist/pipeline/annotate.js.map +1 -1
  192. package/dist/pipeline/compile.js +2 -0
  193. package/dist/pipeline/compile.js.map +1 -1
  194. package/dist/pipeline/constitution-compiler.d.ts +25 -14
  195. package/dist/pipeline/constitution-compiler.js +107 -38
  196. package/dist/pipeline/constitution-compiler.js.map +1 -1
  197. package/dist/pipeline/dynamic-list-types.d.ts +3 -6
  198. package/dist/pipeline/dynamic-list-types.js +3 -16
  199. package/dist/pipeline/dynamic-list-types.js.map +1 -1
  200. package/dist/pipeline/parallel-progress.d.ts +59 -0
  201. package/dist/pipeline/parallel-progress.js +263 -0
  202. package/dist/pipeline/parallel-progress.js.map +1 -0
  203. package/dist/pipeline/pipeline-runner.d.ts +49 -8
  204. package/dist/pipeline/pipeline-runner.js +419 -231
  205. package/dist/pipeline/pipeline-runner.js.map +1 -1
  206. package/dist/pipeline/pipeline-shared.d.ts +59 -1
  207. package/dist/pipeline/pipeline-shared.js +127 -12
  208. package/dist/pipeline/pipeline-shared.js.map +1 -1
  209. package/dist/pipeline/policy-verifier.js +16 -3
  210. package/dist/pipeline/policy-verifier.js.map +1 -1
  211. package/dist/pipeline/proxy-mcp-connections.js +17 -2
  212. package/dist/pipeline/proxy-mcp-connections.js.map +1 -1
  213. package/dist/pipeline/scenario-generator.d.ts +4 -8
  214. package/dist/pipeline/scenario-generator.js +13 -46
  215. package/dist/pipeline/scenario-generator.js.map +1 -1
  216. package/dist/pipeline/server-prefilter.d.ts +31 -0
  217. package/dist/pipeline/server-prefilter.js +115 -0
  218. package/dist/pipeline/server-prefilter.js.map +1 -0
  219. package/dist/pipeline/types.d.ts +5 -0
  220. package/dist/sandbox/index.d.ts +14 -0
  221. package/dist/sandbox/index.js +335 -116
  222. package/dist/sandbox/index.js.map +1 -1
  223. package/dist/sandbox/ironcurtain-protocol.d.ts +52 -0
  224. package/dist/sandbox/ironcurtain-protocol.js +185 -0
  225. package/dist/sandbox/ironcurtain-protocol.js.map +1 -0
  226. package/dist/session/agent-session.d.ts +6 -0
  227. package/dist/session/agent-session.js +16 -5
  228. package/dist/session/agent-session.js.map +1 -1
  229. package/dist/session/cli-transport.js +0 -6
  230. package/dist/session/cli-transport.js.map +1 -1
  231. package/dist/session/index.d.ts +35 -14
  232. package/dist/session/index.js +324 -130
  233. package/dist/session/index.js.map +1 -1
  234. package/dist/session/preflight.d.ts +50 -13
  235. package/dist/session/preflight.js +252 -80
  236. package/dist/session/preflight.js.map +1 -1
  237. package/dist/session/prompts.d.ts +2 -5
  238. package/dist/session/prompts.js.map +1 -1
  239. package/dist/session/session-manager.d.ts +61 -6
  240. package/dist/session/session-manager.js +72 -10
  241. package/dist/session/session-manager.js.map +1 -1
  242. package/dist/session/session-metadata.d.ts +21 -5
  243. package/dist/session/session-metadata.js +34 -12
  244. package/dist/session/session-metadata.js.map +1 -1
  245. package/dist/session/types.d.ts +311 -5
  246. package/dist/session/types.js +57 -1
  247. package/dist/session/types.js.map +1 -1
  248. package/dist/signal/setup-signal.js +13 -21
  249. package/dist/signal/setup-signal.js.map +1 -1
  250. package/dist/signal/signal-bot-daemon.d.ts +16 -1
  251. package/dist/signal/signal-bot-daemon.js +39 -27
  252. package/dist/signal/signal-bot-daemon.js.map +1 -1
  253. package/dist/signal/signal-container.js +7 -1
  254. package/dist/signal/signal-container.js.map +1 -1
  255. package/dist/skills/discovery.d.ts +77 -0
  256. package/dist/skills/discovery.js +206 -0
  257. package/dist/skills/discovery.js.map +1 -0
  258. package/dist/skills/index.d.ts +4 -0
  259. package/dist/skills/index.js +3 -0
  260. package/dist/skills/index.js.map +1 -0
  261. package/dist/skills/staging.d.ts +28 -0
  262. package/dist/skills/staging.js +68 -0
  263. package/dist/skills/staging.js.map +1 -0
  264. package/dist/skills/types.d.ts +46 -0
  265. package/dist/skills/types.js +2 -0
  266. package/dist/skills/types.js.map +1 -0
  267. package/dist/trusted-process/approval-whitelist.d.ts +0 -2
  268. package/dist/trusted-process/approval-whitelist.js +17 -17
  269. package/dist/trusted-process/approval-whitelist.js.map +1 -1
  270. package/dist/trusted-process/async-mutex.d.ts +32 -0
  271. package/dist/trusted-process/async-mutex.js +50 -0
  272. package/dist/trusted-process/async-mutex.js.map +1 -0
  273. package/dist/trusted-process/audit-log.d.ts +38 -1
  274. package/dist/trusted-process/audit-log.js +82 -13
  275. package/dist/trusted-process/audit-log.js.map +1 -1
  276. package/dist/trusted-process/call-circuit-breaker.js +2 -1
  277. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  278. package/dist/trusted-process/control-server.d.ts +101 -0
  279. package/dist/trusted-process/control-server.js +282 -0
  280. package/dist/trusted-process/control-server.js.map +1 -0
  281. package/dist/trusted-process/domain-utils.d.ts +6 -6
  282. package/dist/trusted-process/domain-utils.js +7 -23
  283. package/dist/trusted-process/domain-utils.js.map +1 -1
  284. package/dist/trusted-process/error-prefixes.d.ts +16 -0
  285. package/dist/trusted-process/error-prefixes.js +17 -0
  286. package/dist/trusted-process/error-prefixes.js.map +1 -0
  287. package/dist/trusted-process/index.d.ts +40 -23
  288. package/dist/trusted-process/index.js +131 -225
  289. package/dist/trusted-process/index.js.map +1 -1
  290. package/dist/trusted-process/list-matcher.d.ts +17 -0
  291. package/dist/trusted-process/list-matcher.js +31 -0
  292. package/dist/trusted-process/list-matcher.js.map +1 -0
  293. package/dist/trusted-process/mcp-client-manager.d.ts +35 -6
  294. package/dist/trusted-process/mcp-client-manager.js +40 -45
  295. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  296. package/dist/trusted-process/mcp-proxy-server.d.ts +31 -120
  297. package/dist/trusted-process/mcp-proxy-server.js +166 -657
  298. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  299. package/dist/trusted-process/policy-engine.d.ts +16 -4
  300. package/dist/trusted-process/policy-engine.js +90 -45
  301. package/dist/trusted-process/policy-engine.js.map +1 -1
  302. package/dist/trusted-process/policy-roots.d.ts +6 -0
  303. package/dist/trusted-process/policy-roots.js +20 -0
  304. package/dist/trusted-process/policy-roots.js.map +1 -1
  305. package/dist/trusted-process/sandbox-integration.d.ts +12 -0
  306. package/dist/trusted-process/sandbox-integration.js +26 -0
  307. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  308. package/dist/trusted-process/tool-call-coordinator.d.ts +269 -0
  309. package/dist/trusted-process/tool-call-coordinator.js +508 -0
  310. package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
  311. package/dist/trusted-process/tool-call-pipeline.d.ts +205 -0
  312. package/dist/trusted-process/tool-call-pipeline.js +798 -0
  313. package/dist/trusted-process/tool-call-pipeline.js.map +1 -0
  314. package/dist/trusted-process/tool-description-hints.d.ts +1 -1
  315. package/dist/types/argument-roles.d.ts +1 -1
  316. package/dist/types/argument-roles.js +14 -1
  317. package/dist/types/argument-roles.js.map +1 -1
  318. package/dist/types/audit.d.ts +9 -0
  319. package/dist/{session → types}/errors.d.ts +8 -2
  320. package/dist/{session → types}/errors.js +8 -2
  321. package/dist/types/errors.js.map +1 -0
  322. package/dist/types/reserved-servers.d.ts +11 -0
  323. package/dist/types/reserved-servers.js +12 -0
  324. package/dist/types/reserved-servers.js.map +1 -0
  325. package/dist/types/server-listing.d.ts +14 -0
  326. package/dist/types/server-listing.js +2 -0
  327. package/dist/types/server-listing.js.map +1 -0
  328. package/dist/types/slug.d.ts +2 -0
  329. package/dist/types/slug.js +2 -0
  330. package/dist/types/slug.js.map +1 -1
  331. package/dist/types/url-normalize.d.ts +16 -0
  332. package/dist/types/url-normalize.js +33 -0
  333. package/dist/types/url-normalize.js.map +1 -0
  334. package/dist/utils/error-message.d.ts +8 -0
  335. package/dist/utils/error-message.js +11 -0
  336. package/dist/utils/error-message.js.map +1 -0
  337. package/dist/utils/exec-error.d.ts +19 -0
  338. package/dist/utils/exec-error.js +16 -0
  339. package/dist/utils/exec-error.js.map +1 -0
  340. package/dist/utils/is-plain-object.d.ts +7 -0
  341. package/dist/utils/is-plain-object.js +12 -0
  342. package/dist/utils/is-plain-object.js.map +1 -0
  343. package/dist/utils/preflight-checks.d.ts +16 -0
  344. package/dist/utils/preflight-checks.js +178 -0
  345. package/dist/utils/preflight-checks.js.map +1 -0
  346. package/dist/web-ui/__tests__/json-rpc-dispatch.test.d.ts +7 -0
  347. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +725 -0
  348. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -0
  349. package/dist/web-ui/dispatch/escalation-dispatch.d.ts +7 -0
  350. package/dist/web-ui/dispatch/escalation-dispatch.js +52 -0
  351. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -0
  352. package/dist/web-ui/dispatch/job-dispatch.d.ts +8 -0
  353. package/dist/web-ui/dispatch/job-dispatch.js +83 -0
  354. package/dist/web-ui/dispatch/job-dispatch.js.map +1 -0
  355. package/dist/web-ui/dispatch/persona-dispatch.d.ts +6 -0
  356. package/dist/web-ui/dispatch/persona-dispatch.js +116 -0
  357. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -0
  358. package/dist/web-ui/dispatch/session-dispatch.d.ts +9 -0
  359. package/dist/web-ui/dispatch/session-dispatch.js +201 -0
  360. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -0
  361. package/dist/web-ui/dispatch/token-stream-dispatch.d.ts +15 -0
  362. package/dist/web-ui/dispatch/token-stream-dispatch.js +55 -0
  363. package/dist/web-ui/dispatch/token-stream-dispatch.js.map +1 -0
  364. package/dist/web-ui/dispatch/types.d.ts +31 -0
  365. package/dist/web-ui/dispatch/types.js +68 -0
  366. package/dist/web-ui/dispatch/types.js.map +1 -0
  367. package/dist/web-ui/dispatch/workflow-dispatch.d.ts +103 -0
  368. package/dist/web-ui/dispatch/workflow-dispatch.js +925 -0
  369. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -0
  370. package/dist/web-ui/json-rpc-dispatch.d.ts +12 -0
  371. package/dist/web-ui/json-rpc-dispatch.js +34 -0
  372. package/dist/web-ui/json-rpc-dispatch.js.map +1 -0
  373. package/dist/web-ui/state-graph.d.ts +13 -0
  374. package/dist/web-ui/state-graph.js +82 -0
  375. package/dist/web-ui/state-graph.js.map +1 -0
  376. package/dist/web-ui/token-stream-bridge.d.ts +76 -0
  377. package/dist/web-ui/token-stream-bridge.js +265 -0
  378. package/dist/web-ui/token-stream-bridge.js.map +1 -0
  379. package/dist/web-ui/web-event-bus.d.ts +152 -0
  380. package/dist/web-ui/web-event-bus.js +19 -0
  381. package/dist/web-ui/web-event-bus.js.map +1 -0
  382. package/dist/web-ui/web-session-transport.d.ts +31 -0
  383. package/dist/web-ui/web-session-transport.js +109 -0
  384. package/dist/web-ui/web-session-transport.js.map +1 -0
  385. package/dist/web-ui/web-ui-server.d.ts +76 -0
  386. package/dist/web-ui/web-ui-server.js +461 -0
  387. package/dist/web-ui/web-ui-server.js.map +1 -0
  388. package/dist/web-ui/web-ui-types.d.ts +290 -0
  389. package/dist/web-ui/web-ui-types.js +32 -0
  390. package/dist/web-ui/web-ui-types.js.map +1 -0
  391. package/dist/web-ui/ws-utils.d.ts +5 -0
  392. package/dist/web-ui/ws-utils.js +12 -0
  393. package/dist/web-ui/ws-utils.js.map +1 -0
  394. package/dist/web-ui-static/assets/index-BFO5z1js.js +76 -0
  395. package/dist/web-ui-static/assets/index-CCUwrkIL.css +1 -0
  396. package/dist/web-ui-static/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  397. package/dist/web-ui-static/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  398. package/dist/web-ui-static/index.html +14 -0
  399. package/dist/workflow/artifacts.d.ts +59 -0
  400. package/dist/workflow/artifacts.js +188 -0
  401. package/dist/workflow/artifacts.js.map +1 -0
  402. package/dist/workflow/checkpoint-selection.d.ts +34 -0
  403. package/dist/workflow/checkpoint-selection.js +48 -0
  404. package/dist/workflow/checkpoint-selection.js.map +1 -0
  405. package/dist/workflow/checkpoint.d.ts +35 -0
  406. package/dist/workflow/checkpoint.js +84 -0
  407. package/dist/workflow/checkpoint.js.map +1 -0
  408. package/dist/workflow/cli-support.d.ts +92 -0
  409. package/dist/workflow/cli-support.js +423 -0
  410. package/dist/workflow/cli-support.js.map +1 -0
  411. package/dist/workflow/definition-loader.d.ts +32 -0
  412. package/dist/workflow/definition-loader.js +56 -0
  413. package/dist/workflow/definition-loader.js.map +1 -0
  414. package/dist/workflow/discovery.d.ts +78 -0
  415. package/dist/workflow/discovery.js +203 -0
  416. package/dist/workflow/discovery.js.map +1 -0
  417. package/dist/workflow/errors.d.ts +66 -0
  418. package/dist/workflow/errors.js +49 -0
  419. package/dist/workflow/errors.js.map +1 -0
  420. package/dist/workflow/guards.d.ts +8 -0
  421. package/dist/workflow/guards.js +38 -0
  422. package/dist/workflow/guards.js.map +1 -0
  423. package/dist/workflow/lint-integration.d.ts +64 -0
  424. package/dist/workflow/lint-integration.js +56 -0
  425. package/dist/workflow/lint-integration.js.map +1 -0
  426. package/dist/workflow/lint.d.ts +66 -0
  427. package/dist/workflow/lint.js +410 -0
  428. package/dist/workflow/lint.js.map +1 -0
  429. package/dist/workflow/machine-builder.d.ts +90 -0
  430. package/dist/workflow/machine-builder.js +485 -0
  431. package/dist/workflow/machine-builder.js.map +1 -0
  432. package/dist/workflow/message-log.d.ts +85 -0
  433. package/dist/workflow/message-log.js +40 -0
  434. package/dist/workflow/message-log.js.map +1 -0
  435. package/dist/workflow/orchestrator.d.ts +450 -0
  436. package/dist/workflow/orchestrator.js +1941 -0
  437. package/dist/workflow/orchestrator.js.map +1 -0
  438. package/dist/workflow/prompt-builder.d.ts +39 -0
  439. package/dist/workflow/prompt-builder.js +242 -0
  440. package/dist/workflow/prompt-builder.js.map +1 -0
  441. package/dist/workflow/run-state-command.d.ts +9 -0
  442. package/dist/workflow/run-state-command.js +414 -0
  443. package/dist/workflow/run-state-command.js.map +1 -0
  444. package/dist/workflow/run-state-debug-capture.d.ts +40 -0
  445. package/dist/workflow/run-state-debug-capture.js +101 -0
  446. package/dist/workflow/run-state-debug-capture.js.map +1 -0
  447. package/dist/workflow/status-parser.d.ts +68 -0
  448. package/dist/workflow/status-parser.js +259 -0
  449. package/dist/workflow/status-parser.js.map +1 -0
  450. package/dist/workflow/types.d.ts +498 -0
  451. package/dist/workflow/types.js +83 -0
  452. package/dist/workflow/types.js.map +1 -0
  453. package/dist/workflow/validate.d.ts +58 -0
  454. package/dist/workflow/validate.js +499 -0
  455. package/dist/workflow/validate.js.map +1 -0
  456. package/dist/workflow/workflow-command.d.ts +26 -0
  457. package/dist/workflow/workflow-command.js +648 -0
  458. package/dist/workflow/workflow-command.js.map +1 -0
  459. package/dist/workflow/workflow-discovery.d.ts +25 -0
  460. package/dist/workflow/workflow-discovery.js +100 -0
  461. package/dist/workflow/workflow-discovery.js.map +1 -0
  462. package/dist/workflow/workflow-manager.d.ts +122 -0
  463. package/dist/workflow/workflow-manager.js +308 -0
  464. package/dist/workflow/workflow-manager.js.map +1 -0
  465. package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -0
  466. package/dist/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  467. package/dist/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  468. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  469. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  470. package/dist/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  471. package/dist/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  472. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  473. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  474. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  475. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  476. package/dist/workflow/worktree.d.ts +43 -0
  477. package/dist/workflow/worktree.js +106 -0
  478. package/dist/workflow/worktree.js.map +1 -0
  479. package/docker/Dockerfile.base +12 -1
  480. package/docker/Dockerfile.base.arm64 +17 -1
  481. package/docker/entrypoint-claude-code.sh +46 -0
  482. package/docker/entrypoint-goose.sh +25 -0
  483. package/package.json +22 -8
  484. package/packages/web-ui/README.md +113 -0
  485. package/packages/web-ui/docs/workflow-state-machine.png +0 -0
  486. package/src/config/generated/compiled-policy.json +528 -161
  487. package/src/config/generated/test-scenarios.json +4810 -170
  488. package/src/config/generated/tool-annotations.json +34 -47
  489. package/src/config/mcp-servers.json +2 -2
  490. package/src/workflow/workflows/design-and-code/workflow.yaml +169 -0
  491. package/src/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  492. package/src/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  493. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  494. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  495. package/src/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  496. package/src/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  497. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  498. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  499. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  500. package/src/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  501. package/dist/pipeline/llm-logger.js.map +0 -1
  502. package/dist/session/errors.js.map +0 -1
@@ -1,65 +1,53 @@
1
1
  /**
2
- * MCP Proxy Server -- The trusted process running as a standalone MCP server.
2
+ * MCP Proxy Server -- Standalone MCP subprocess for Code Mode.
3
3
  *
4
- * Code Mode spawns this as a child process via stdio transport. It acts
5
- * as the security boundary between the sandbox and real MCP servers:
4
+ * Runs as a child process spawned by MCPClientManager. Acts as a pure
5
+ * MCP pass-through relay: connects to real MCP backend servers and
6
+ * forwards tool calls without any policy evaluation. The coordinator
7
+ * in the parent process owns the full security pipeline.
6
8
  *
7
- * 1. Connects to real MCP servers as a client
8
- * 2. Exposes their tools with passthrough schemas
9
- * 3. Evaluates every tool call against the policy engine
10
- * 4. Forwards allowed calls to real servers, denies or escalates others
11
- * 5. Logs every request and decision to the append-only audit log
9
+ * Responsibilities:
10
+ * - MCP transport setup (stdio/UDS/TCP)
11
+ * - Backend MCP server connection (StdioClientTransport)
12
+ * - OAuth credential injection and TokenFileRefresher
13
+ * - Sandbox-runtime wrapping (srt)
14
+ * - ListTools handler (raw pass-through)
15
+ * - CallTool handler (pure forward to backend)
16
+ * - Virtual proxy tools (MITM_CONTROL_ADDR)
17
+ * - Tool description hints injection
18
+ * - Roots bridging: on `notifications/roots/list_changed` from the
19
+ * coordinator, fetch fresh roots via `server.listRoots()`, update
20
+ * the shared `relayRoots` array, and notify each backend client so
21
+ * they re-query their `roots/list` handlers with the new set.
12
22
  *
13
23
  * Configuration via environment variables:
14
- * AUDIT_LOG_PATH -- path to the audit log file
15
24
  * MCP_SERVERS_CONFIG -- JSON string of MCP server configs to proxy
16
25
  * GENERATED_DIR -- path to the generated artifacts directory
17
26
  * PROTECTED_PATHS -- JSON array of protected paths
18
- * ALLOWED_DIRECTORY -- (optional) sandbox directory for structural containment check
19
- * ESCALATION_DIR -- (optional) directory for file-based escalation IPC
27
+ * ALLOWED_DIRECTORY -- (optional) sandbox directory
20
28
  * SESSION_LOG_PATH -- (optional) path for capturing child process stderr
21
29
  * SANDBOX_POLICY -- (optional) "enforce" | "warn" (default: "warn")
22
- * AUDIT_REDACTION -- (optional) "true" to redact PII/credentials in audit log entries
23
- * SERVER_FILTER -- (optional) when set, only connect to this single server name
24
- * AUTO_APPROVE_ENABLED -- (optional) "true" to enable auto-approval of escalations
25
- * AUTO_APPROVE_MODEL_ID -- (optional) qualified model ID for auto-approve LLM
26
- * AUTO_APPROVE_API_KEY -- (optional) API key for the auto-approve model's provider
27
- * AUTO_APPROVE_LLM_LOG_PATH -- (optional) path to JSONL file for auto-approve LLM logging
30
+ * SERVER_FILTER -- (optional) only connect to this single server name
28
31
  */
29
32
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
30
33
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
31
34
  import { UdsServerTransport } from './uds-server-transport.js';
32
35
  import { TcpServerTransport } from './tcp-server-transport.js';
33
- import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
36
+ import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
34
37
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
35
38
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
36
- import { appendFileSync, existsSync, mkdtempSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
37
- import { atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
39
+ import { appendFileSync, mkdtempSync, writeFileSync, writeSync } from 'node:fs';
38
40
  import { join, resolve } from 'node:path';
39
41
  import { fileURLToPath } from 'node:url';
40
42
  import { tmpdir, homedir } from 'node:os';
41
- import { v4 as uuidv4 } from 'uuid';
42
- import { loadGeneratedPolicy, extractServerDomainAllowlists, getPackageGeneratedDir } from '../config/index.js';
43
- import { PolicyEngine, extractAnnotatedPaths } from './policy-engine.js';
44
- import { getPathRoles, expandTilde } from '../types/argument-roles.js';
45
- import { AuditLog } from './audit-log.js';
46
- import { prepareToolArgs, rewriteResultContent } from './path-utils.js';
47
- import { CONTAINER_WORKSPACE_DIR } from '../docker/agent-adapter.js';
48
- import { extractPolicyRoots, toMcpRoots, directoryForPath } from './policy-roots.js';
49
- import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, annotateSandboxViolation, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
50
- import { ROOTS_REFRESH_TIMEOUT_MS } from './mcp-client-manager.js';
51
- import { CallCircuitBreaker } from './call-circuit-breaker.js';
52
- import { autoApprove, extractArgsForAutoApprove, readUserContext } from './auto-approver.js';
53
- import { createLanguageModelFromEnv } from '../config/model-provider.js';
54
- import { wrapLanguageModel } from 'ai';
55
- import { createLlmLoggingMiddleware } from '../pipeline/llm-logger.js';
43
+ import { loadGeneratedPolicy, getPackageGeneratedDir } from '../config/index.js';
44
+ import { expandTilde } from '../types/argument-roles.js';
45
+ import { extractPolicyRoots, toMcpRoots } from './policy-roots.js';
46
+ import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
56
47
  import { extractMcpErrorMessage } from './mcp-error-utils.js';
57
- import { updateServerContext, formatServerContext } from './server-context.js';
58
48
  import { permissiveJsonSchemaValidator } from './permissive-output-validator.js';
59
49
  import { VERSION } from '../version.js';
60
- import { buildTrustedServerSet } from '../memory/memory-annotations.js';
61
50
  import { loadToolDescriptionHints, applyToolDescriptionHints } from './tool-description-hints.js';
62
- import { createApprovalWhitelist, extractWhitelistCandidates, } from './approval-whitelist.js';
63
51
  import { getProviderForServer } from '../auth/oauth-registry.js';
64
52
  import { loadClientCredentials } from '../auth/oauth-provider.js';
65
53
  import { OAuthTokenProvider } from '../auth/oauth-token-provider.js';
@@ -67,68 +55,10 @@ import { loadOAuthToken } from '../auth/oauth-token-store.js';
67
55
  import { writeGWorkspaceCredentialFile } from './gworkspace-credentials.js';
68
56
  import { TokenFileRefresher } from './token-file-refresher.js';
69
57
  import { proxyAnnotations, proxyPolicyRules, proxyToolDefinitions, handleVirtualProxyTool, createControlApiClient, } from '../docker/proxy-tools.js';
70
- /**
71
- * Pending whitelist candidates keyed by escalation ID.
72
- * Populated when a request file is written with candidates;
73
- * consumed when the response comes back with a selection.
74
- * Module-level state — the proxy is a singleton process.
75
- */
76
- const pendingWhitelistCandidates = new Map();
77
- /**
78
- * Adds a root to a client's root list and waits for the server to
79
- * fetch the updated list. No-op if the root URI is already present.
80
- * Times out after ROOTS_REFRESH_TIMEOUT_MS if the server never requests
81
- * the updated list (e.g. servers that don't implement Roots protocol).
82
- *
83
- * Returns `'added'` when the server acknowledged the update (called
84
- * `roots/list`), `'timeout'` when the server didn't respond in time,
85
- * or `false` when the root was already present.
86
- */
87
- async function addRootToClient(state, root) {
88
- if (state.roots.some((r) => r.uri === root.uri))
89
- return false;
90
- state.roots.push(root);
91
- let timer;
92
- let outcome = 'timeout';
93
- const refreshed = new Promise((resolve) => {
94
- state.rootsRefreshed = () => {
95
- clearTimeout(timer);
96
- outcome = 'added';
97
- resolve();
98
- };
99
- });
100
- const timeout = new Promise((resolve) => {
101
- timer = setTimeout(() => {
102
- state.rootsRefreshed = undefined;
103
- resolve();
104
- }, ROOTS_REFRESH_TIMEOUT_MS);
105
- timer.unref();
106
- });
107
- await state.client.sendRootsListChanged();
108
- await Promise.race([refreshed, timeout]);
109
- return outcome;
110
- }
111
- /**
112
- * Delay before retrying a tool call that returned "access denied" immediately
113
- * after roots expansion. The filesystem server needs time to async-validate
114
- * (fs.realpath, fs.stat) the new roots after receiving the rootsListChanged
115
- * notification. Exported for test access.
116
- */
117
- export const ROOTS_RACE_RETRY_DELAY_MS = 200;
118
- /**
119
- * Checks whether an MCP tool call result looks like a roots-race "access denied"
120
- * error -- the filesystem server rejected the call because it hasn't finished
121
- * processing the updated roots yet.
122
- */
123
- function isRootsRaceError(result) {
124
- if (!('isError' in result) || !result.isError)
125
- return false;
126
- const text = extractTextFromContent(result.content);
127
- if (!text)
128
- return false;
129
- const lower = text.toLowerCase();
130
- return lower.includes('access denied') || lower.includes('outside allowed directories');
131
- }
58
+ import { buildToolMap, assertUniqueToolNames } from './tool-call-pipeline.js';
59
+ // ---------------------------------------------------------------------------
60
+ // Subprocess helpers
61
+ // ---------------------------------------------------------------------------
132
62
  /** Appends a timestamped line to the session log file. */
133
63
  function logToSessionFile(sessionLogPath, message) {
134
64
  const timestamp = new Date().toISOString();
@@ -157,15 +87,6 @@ function getMissingEnvVars(args) {
157
87
  }
158
88
  return missing;
159
89
  }
160
- /** Extracts concatenated text from MCP content blocks. */
161
- export function extractTextFromContent(content) {
162
- if (!Array.isArray(content))
163
- return undefined;
164
- const texts = content
165
- .filter((c) => c.type === 'text' && typeof c.text === 'string')
166
- .map((c) => c.text);
167
- return texts.length > 0 ? texts.join('\n') : undefined;
168
- }
169
90
  /**
170
91
  * Replaces known credential values in a string with `***REDACTED***`.
171
92
  * Prevents credential leakage in session log files.
@@ -179,163 +100,40 @@ function redactCredentials(text, credentials) {
179
100
  }
180
101
  return result;
181
102
  }
182
- const ESCALATION_POLL_INTERVAL_MS = 500;
183
- const DEFAULT_ESCALATION_TIMEOUT_SECONDS = 300;
184
- /** Auto-approve trusted input older than this is rejected as stale. */
185
- const TRUSTED_INPUT_STALENESS_MS = 120_000;
186
103
  /**
187
- * Returns true when a user context is safe to act on for auto-approval.
188
- *
189
- * For PTY sessions (isPtySession=true):
190
- * - source must be 'mux-trusted-input' (set by the trusted mux layer)
191
- * - timestamp must be present, valid, not in the future, and within
192
- * TRUSTED_INPUT_STALENESS_MS of `now`
193
- *
194
- * For non-PTY sessions:
195
- * - source is not checked (no mux layer in the path)
196
- * - a missing or invalid timestamp does not prevent trust; a valid stale
197
- * timestamp still prevents trust (belt-and-suspenders for future callers)
198
- *
199
- * @param context - The user context parsed from user-context.json
200
- * @param isPtySession - Whether this is a PTY interactive session
201
- * @param now - Current time in ms; defaults to Date.now() (injectable for tests)
104
+ * True when this proxy subprocess is in a viable serving state -- either
105
+ * virtual-only mode (no backends configured) or at least one configured
106
+ * backend connected. Returning false means the parent should treat this
107
+ * subprocess as failed; otherwise it would register zero tools for the
108
+ * server and trigger a misleading "stale annotations" warning in the
109
+ * coordinator's drift check.
202
110
  */
203
- export function isUserContextTrusted(context, isPtySession, now = Date.now()) {
204
- const sourceValid = !isPtySession || context.source === 'mux-trusted-input';
205
- if (!sourceValid)
206
- return false;
207
- let stale = isPtySession; // non-PTY sessions don't require timestamps
208
- if (context.timestamp !== undefined) {
209
- const tsMs = new Date(context.timestamp).getTime();
210
- if (!Number.isNaN(tsMs)) {
211
- const ageMs = now - tsMs;
212
- stale = ageMs > TRUSTED_INPUT_STALENESS_MS || ageMs < 0;
213
- }
214
- }
215
- return !stale;
216
- }
217
- /** Reads escalation timeout from env var, falling back to default. */
218
- function getEscalationTimeoutMs() {
219
- const envValue = process.env.ESCALATION_TIMEOUT_SECONDS;
220
- if (envValue) {
221
- const parsed = Number(envValue);
222
- if (Number.isFinite(parsed) && parsed > 0) {
223
- return parsed * 1000;
224
- }
225
- }
226
- return DEFAULT_ESCALATION_TIMEOUT_SECONDS * 1000;
227
- }
228
- /** Silently removes a file. Ignores errors (e.g. file already gone). */
229
- function tryUnlink(path) {
230
- try {
231
- unlinkSync(path);
232
- }
233
- catch {
234
- /* ignore */
235
- }
236
- }
237
- /**
238
- * Removes both escalation IPC files. Request is deleted first so the
239
- * session side sees "request gone + response exists" as normal
240
- * approval-in-progress rather than an expiry.
241
- */
242
- function cleanupEscalationFiles(requestPath, responsePath) {
243
- tryUnlink(requestPath);
244
- tryUnlink(responsePath);
111
+ export function proxyCanServe(serversConfig, connectedBackendCount) {
112
+ return Object.keys(serversConfig).length === 0 || connectedBackendCount > 0;
245
113
  }
246
114
  /**
247
- * Reads and parses the escalation response file if it exists.
248
- * Returns the response data, or undefined if the file is not present.
249
- */
250
- function readEscalationResponse(responsePath) {
251
- if (!existsSync(responsePath))
252
- return undefined;
253
- const raw = JSON.parse(readFileSync(responsePath, 'utf-8'));
254
- // Validate whitelistSelection: must be an integer if present, discard otherwise
255
- let whitelistSelection;
256
- if (typeof raw.whitelistSelection === 'number' && Number.isInteger(raw.whitelistSelection)) {
257
- whitelistSelection = raw.whitelistSelection;
258
- }
259
- return {
260
- decision: raw.decision,
261
- ...(whitelistSelection !== undefined ? { whitelistSelection } : {}),
262
- };
263
- }
264
- /**
265
- * Waits for a human decision via file-based IPC.
266
- *
267
- * Writes a request file to the escalation directory, then polls
268
- * for a response file. The session process (on the other side)
269
- * detects the request, surfaces it to the user, and writes the response.
115
+ * Writes a diagnostic line to stderr and (if configured) the proxy session log.
270
116
  *
271
- * Returns 'approved' or 'denied'. On timeout, returns 'denied'.
117
+ * Uses `fs.writeSync` so the bytes reach the OS before the function returns
118
+ * even when stderr is a pipe (parent-spawned subprocess). `process.stderr.write`
119
+ * is asynchronous in that case, which would let a follow-up `process.exit(1)`
120
+ * truncate the diagnostic before delivery.
272
121
  */
273
- async function waitForEscalationDecision(escalationDir, request) {
274
- const requestPath = resolve(escalationDir, `request-${request.escalationId}.json`);
275
- const responsePath = resolve(escalationDir, `response-${request.escalationId}.json`);
276
- atomicWriteJsonSync(requestPath, request);
277
- const deadline = Date.now() + getEscalationTimeoutMs();
278
- while (Date.now() < deadline) {
279
- const response = readEscalationResponse(responsePath);
280
- if (response) {
281
- cleanupEscalationFiles(requestPath, responsePath);
282
- return response;
283
- }
284
- await new Promise((r) => setTimeout(r, ESCALATION_POLL_INTERVAL_MS));
285
- }
286
- // Final check -- response may have arrived between last poll and deadline
287
- const lateResponse = readEscalationResponse(responsePath);
288
- cleanupEscalationFiles(requestPath, responsePath);
289
- return lateResponse ?? { decision: 'denied' };
122
+ function emitProxyDiagnostic(level, message, sessionLogPath) {
123
+ writeSync(2, `${level}: ${message}\n`);
124
+ if (sessionLogPath)
125
+ logToSessionFile(sessionLogPath, `[proxy] ${message}`);
290
126
  }
291
- /**
292
- * Creates the auto-approve LLM model from environment variables.
293
- * Returns null when auto-approve is not enabled or env vars are missing.
294
- * Wraps with LLM logging middleware when a log path is provided.
295
- */
296
- async function createAutoApproveModel(sessionLogPath) {
297
- if (process.env.AUTO_APPROVE_ENABLED !== 'true')
298
- return null;
299
- const modelId = process.env.AUTO_APPROVE_MODEL_ID;
300
- if (!modelId)
301
- return null;
302
- const apiKey = process.env.AUTO_APPROVE_API_KEY ?? '';
303
- try {
304
- const baseModel = await createLanguageModelFromEnv(modelId, apiKey);
305
- const llmLogPath = process.env.AUTO_APPROVE_LLM_LOG_PATH;
306
- if (sessionLogPath) {
307
- logToSessionFile(sessionLogPath, `Auto-approve model created: ${modelId}`);
308
- }
309
- if (!llmLogPath)
310
- return baseModel;
311
- return wrapLanguageModel({
312
- model: baseModel,
313
- middleware: createLlmLoggingMiddleware(llmLogPath, { stepName: 'auto-approve' }),
314
- });
315
- }
316
- catch (err) {
317
- // Model creation failure should not prevent the proxy from starting.
318
- // Auto-approve simply won't be available for this session.
319
- const message = err instanceof Error ? err.message : String(err);
320
- if (sessionLogPath) {
321
- logToSessionFile(sessionLogPath, `Auto-approve model creation failed for ${modelId}: ${message}`);
322
- }
323
- return null;
324
- }
325
- }
326
- // ── Extracted functions ────────────────────────────────────────────────
327
127
  /**
328
128
  * Reads and validates proxy environment variables.
329
129
  * Returns a typed config object or calls process.exit(1) on missing required vars.
330
130
  */
331
131
  export function parseProxyEnvConfig() {
332
- const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
333
132
  const serversConfigJson = process.env.MCP_SERVERS_CONFIG;
334
133
  const generatedDir = process.env.GENERATED_DIR;
335
134
  const protectedPathsJson = process.env.PROTECTED_PATHS ?? '[]';
336
135
  const sessionLogPath = process.env.SESSION_LOG_PATH;
337
136
  const allowedDirectory = process.env.ALLOWED_DIRECTORY;
338
- const escalationDir = process.env.ESCALATION_DIR;
339
137
  if (!serversConfigJson) {
340
138
  process.stderr.write('MCP_SERVERS_CONFIG environment variable is required\n');
341
139
  process.exit(1);
@@ -351,7 +149,6 @@ export function parseProxyEnvConfig() {
351
149
  : {};
352
150
  delete process.env.SERVER_CREDENTIALS;
353
151
  const sandboxPolicy = (process.env.SANDBOX_POLICY ?? 'warn');
354
- const auditRedaction = process.env.AUDIT_REDACTION === 'true';
355
152
  const allServersConfig = JSON.parse(serversConfigJson);
356
153
  const protectedPaths = JSON.parse(protectedPathsJson);
357
154
  // When SERVER_FILTER is set, only connect to that single backend server.
@@ -373,17 +170,15 @@ export function parseProxyEnvConfig() {
373
170
  // Otherwise they default to the same directory as compiled policy.
374
171
  const toolAnnotationsDir = process.env.TOOL_ANNOTATIONS_DIR ?? generatedDir;
375
172
  return {
376
- auditLogPath,
377
173
  serversConfig,
378
174
  generatedDir,
379
175
  toolAnnotationsDir,
380
176
  protectedPaths,
381
177
  sessionLogPath,
382
178
  allowedDirectory,
383
- escalationDir,
384
179
  serverCredentials,
385
180
  sandboxPolicy,
386
- auditRedaction,
181
+ serverFilter,
387
182
  };
388
183
  }
389
184
  /**
@@ -433,365 +228,6 @@ export function resolveServerSandboxConfigs(serversConfig, allowedDirectory, san
433
228
  }
434
229
  return { resolvedSandboxConfigs, settingsDir, serverCwdPaths };
435
230
  }
436
- /**
437
- * Validates that all argument keys exist in the tool's inputSchema.
438
- * Returns null if valid, or an error message listing unknown and valid keys.
439
- * Skips validation when the schema has no properties or allows additional properties.
440
- */
441
- export function validateToolArguments(args, inputSchema) {
442
- const properties = inputSchema.properties;
443
- if (!properties || typeof properties !== 'object')
444
- return null;
445
- if (inputSchema.additionalProperties)
446
- return null;
447
- const validKeys = new Set(Object.keys(properties));
448
- const unknownKeys = Object.keys(args).filter((k) => !validKeys.has(k));
449
- if (unknownKeys.length === 0)
450
- return null;
451
- const unknownList = unknownKeys.map((k) => `"${k}"`).join(', ');
452
- const validList = [...validKeys]
453
- .sort()
454
- .map((k) => `"${k}"`)
455
- .join(', ');
456
- return `Unknown argument(s): ${unknownList}. Valid parameters are: ${validList}`;
457
- }
458
- /** Builds a lookup map from tool name to ProxiedTool for routing. */
459
- export function buildToolMap(allTools) {
460
- const toolMap = new Map();
461
- for (const tool of allTools) {
462
- toolMap.set(tool.name, tool);
463
- }
464
- return toolMap;
465
- }
466
- /**
467
- * Creates an audit entry and logs it. Extracted so the audit shape
468
- * construction is testable independently.
469
- */
470
- export function buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, options) {
471
- return {
472
- timestamp: request.timestamp,
473
- requestId: request.requestId,
474
- serverName: request.serverName,
475
- toolName: request.toolName,
476
- arguments: argsForTransport,
477
- policyDecision,
478
- escalationResult: options.escalationResult,
479
- result,
480
- durationMs,
481
- sandboxed: options.sandboxed || undefined,
482
- autoApproved: options.autoApproved || undefined,
483
- whitelistApproved: options.whitelistApproved || undefined,
484
- whitelistPatternId: options.whitelistPatternId,
485
- };
486
- }
487
- /**
488
- * Handles a single tool call request: policy evaluation, escalation,
489
- * circuit breaker check, and forwarding to the real MCP server.
490
- *
491
- * This is the core security logic extracted from the CallTool handler
492
- * for independent unit testing.
493
- */
494
- export async function handleCallTool(toolName, rawArgs, deps) {
495
- const toolInfo = deps.toolMap.get(toolName);
496
- if (!toolInfo) {
497
- return {
498
- content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
499
- isError: true,
500
- };
501
- }
502
- // Annotation-driven normalization: split into transport vs policy args.
503
- // Trusted servers skip annotation lookup and prepareToolArgs — use raw args directly.
504
- const annotation = deps.policyEngine.getAnnotation(toolInfo.serverName, toolInfo.name, rawArgs);
505
- const isTrusted = !annotation && deps.policyEngine.isTrustedServer(toolInfo.serverName);
506
- if (!annotation && !isTrusted) {
507
- return {
508
- content: [
509
- {
510
- type: 'text',
511
- text: `Missing annotation for tool: ${toolInfo.serverName}__${toolInfo.name}. Re-run 'ironcurtain annotate-tools' to update.`,
512
- },
513
- ],
514
- isError: true,
515
- };
516
- }
517
- // Validate argument names against the tool's schema.
518
- // Skip for trusted servers (check directly, not via isTrusted which also requires missing annotation).
519
- if (!deps.policyEngine.isTrustedServer(toolInfo.serverName)) {
520
- const validationError = validateToolArguments(rawArgs, toolInfo.inputSchema);
521
- if (validationError) {
522
- deps.auditLog.log({
523
- timestamp: new Date().toISOString(),
524
- requestId: uuidv4(),
525
- serverName: toolInfo.serverName,
526
- toolName: toolInfo.name,
527
- arguments: rawArgs,
528
- policyDecision: { status: 'deny', rule: 'invalid-arguments', reason: validationError },
529
- result: { status: 'denied', error: validationError },
530
- durationMs: 0,
531
- });
532
- return {
533
- content: [{ type: 'text', text: validationError }],
534
- isError: true,
535
- };
536
- }
537
- }
538
- let argsForTransport;
539
- let argsForPolicy;
540
- if (isTrusted) {
541
- // Trusted server with no annotation: use raw args for both transport and policy
542
- argsForTransport = rawArgs;
543
- argsForPolicy = rawArgs;
544
- }
545
- else {
546
- // annotation is guaranteed non-null here: we returned early if both are falsy
547
- const resolvedAnnotation = annotation;
548
- // Enrich git tool args with the locally tracked working directory when
549
- // the agent omits the `path` argument. The git MCP server tracks its own
550
- // CWD (set via git_set_working_dir) and uses it implicitly, but the
551
- // policy engine needs the explicit path to resolve sandbox containment
552
- // and the default remote URL for domain-based policy rules.
553
- // The serverContextMap mirrors the git server's working directory from
554
- // successful git_set_working_dir / git_clone calls, avoiding an RPC.
555
- let effectiveRawArgs = rawArgs;
556
- const hasEffectivePath = 'path' in rawArgs && typeof rawArgs.path === 'string' && rawArgs.path.trim() !== '';
557
- if (toolInfo.serverName === 'git' && 'path' in resolvedAnnotation.args && !hasEffectivePath) {
558
- const gitWorkDir = deps.serverContextMap.get('git')?.workingDirectory;
559
- if (!gitWorkDir) {
560
- const errorMsg = 'Git server has no working directory set. Call git_set_working_dir first.';
561
- deps.auditLog.log(buildAuditEntry({
562
- requestId: uuidv4(),
563
- serverName: toolInfo.serverName,
564
- toolName: toolInfo.name,
565
- arguments: rawArgs,
566
- timestamp: new Date().toISOString(),
567
- }, rawArgs, { status: 'deny', rule: 'git-path-enrichment-failed', reason: errorMsg }, { status: 'denied', error: errorMsg }, 0, {}));
568
- return {
569
- content: [{ type: 'text', text: `Error: ${errorMsg}` }],
570
- isError: true,
571
- };
572
- }
573
- effectiveRawArgs = { ...rawArgs, path: gitWorkDir };
574
- }
575
- ({ argsForTransport, argsForPolicy } = prepareToolArgs(effectiveRawArgs, resolvedAnnotation, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR));
576
- }
577
- const request = {
578
- requestId: uuidv4(),
579
- serverName: toolInfo.serverName,
580
- toolName: toolInfo.name,
581
- arguments: argsForPolicy,
582
- timestamp: new Date().toISOString(),
583
- };
584
- const evaluation = deps.policyEngine.evaluate(request);
585
- const policyDecision = {
586
- status: evaluation.decision,
587
- rule: evaluation.rule,
588
- reason: evaluation.reason,
589
- };
590
- let escalationResult;
591
- let autoApproved = false;
592
- let whitelistApproved = false;
593
- let whitelistPatternId;
594
- let rootsExpanded = false;
595
- const serverSandboxConfig = deps.resolvedSandboxConfigs.get(toolInfo.serverName);
596
- const serverIsSandboxed = serverSandboxConfig?.sandboxed === true;
597
- function logAudit(result, durationMs, overrideEscalation) {
598
- const entry = buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, {
599
- escalationResult: overrideEscalation ?? escalationResult,
600
- sandboxed: serverIsSandboxed,
601
- autoApproved,
602
- whitelistApproved: whitelistApproved || undefined,
603
- whitelistPatternId,
604
- });
605
- deps.auditLog.log(entry);
606
- }
607
- if (evaluation.decision === 'escalate') {
608
- // Annotation is guaranteed non-null here: trusted servers always allow (never escalate),
609
- // and missing-annotation returns early above. Narrow the type for TypeScript.
610
- const resolvedAnnotation = annotation;
611
- const whitelistMatch = deps.whitelist.match(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation);
612
- if (whitelistMatch.matched) {
613
- whitelistApproved = true;
614
- whitelistPatternId = whitelistMatch.patternId;
615
- escalationResult = 'approved';
616
- policyDecision.status = 'allow';
617
- policyDecision.reason = `Whitelist-approved: ${whitelistMatch.pattern.description}`;
618
- }
619
- if (!whitelistApproved) {
620
- if (!deps.escalationDir) {
621
- logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
622
- return {
623
- content: [
624
- {
625
- type: 'text',
626
- text: `ESCALATION REQUIRED: ${evaluation.reason}. Action denied (no escalation handler).`,
627
- },
628
- ],
629
- isError: true,
630
- };
631
- }
632
- const escalationId = uuidv4();
633
- // Try auto-approve before falling through to human escalation
634
- if (deps.autoApproveModel) {
635
- const userContext = readUserContext(deps.escalationDir);
636
- if (userContext) {
637
- const isPtySession = process.env.IRONCURTAIN_PTY_SESSION === '1';
638
- if (isUserContextTrusted(userContext, isPtySession)) {
639
- const autoResult = await autoApprove({
640
- userMessage: userContext.userMessage,
641
- toolName: `${toolInfo.serverName}/${toolInfo.name}`,
642
- escalationReason: evaluation.reason,
643
- arguments: extractArgsForAutoApprove(argsForPolicy, annotation),
644
- }, deps.autoApproveModel);
645
- if (autoResult.decision === 'approve') {
646
- autoApproved = true;
647
- escalationResult = 'approved';
648
- policyDecision.status = 'allow';
649
- policyDecision.reason = `Auto-approved: ${autoResult.reasoning}`;
650
- }
651
- }
652
- }
653
- }
654
- if (!autoApproved) {
655
- // Extract whitelist candidates just before human escalation (avoids
656
- // wasted work when auto-approve succeeds).
657
- const { patterns: candidatePatterns, ipcs: candidateIpcs } = extractWhitelistCandidates(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation, evaluation.escalatedRoles, escalationId, evaluation.reason);
658
- if (candidatePatterns.length > 0) {
659
- // Store full patterns in proxy memory, keyed by escalation ID
660
- pendingWhitelistCandidates.set(escalationId, candidatePatterns);
661
- }
662
- try {
663
- const response = await waitForEscalationDecision(deps.escalationDir, {
664
- escalationId,
665
- serverName: request.serverName,
666
- toolName: request.toolName,
667
- arguments: argsForTransport,
668
- reason: evaluation.reason,
669
- context: formatServerContext(deps.serverContextMap, toolInfo.serverName),
670
- whitelistCandidates: candidateIpcs.length > 0 ? candidateIpcs : undefined,
671
- });
672
- if (response.decision === 'denied') {
673
- logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
674
- return {
675
- content: [{ type: 'text', text: `ESCALATION DENIED: ${evaluation.reason}` }],
676
- isError: true,
677
- };
678
- }
679
- escalationResult = 'approved';
680
- policyDecision.status = 'allow';
681
- policyDecision.reason = 'Approved by human during escalation';
682
- // Handle whitelist selection from the response
683
- if (response.whitelistSelection !== undefined) {
684
- const storedPatterns = pendingWhitelistCandidates.get(escalationId);
685
- if (storedPatterns &&
686
- response.whitelistSelection >= 0 &&
687
- response.whitelistSelection < storedPatterns.length) {
688
- const selectedPattern = storedPatterns[response.whitelistSelection];
689
- deps.whitelist.add(selectedPattern);
690
- }
691
- }
692
- }
693
- finally {
694
- // Issue 12: ensure cleanup even if waitForEscalationDecision throws
695
- pendingWhitelistCandidates.delete(escalationId);
696
- }
697
- }
698
- }
699
- // Expand roots for approved path arguments only (skip URLs, opaques)
700
- const state = deps.clientStates.get(toolInfo.serverName);
701
- if (state && annotation) {
702
- const pathValues = extractAnnotatedPaths(argsForTransport, annotation, getPathRoles());
703
- for (const p of pathValues) {
704
- const result = await addRootToClient(state, {
705
- uri: `file://${directoryForPath(p)}`,
706
- name: 'escalation-approved',
707
- });
708
- if (result === 'added')
709
- rootsExpanded = true;
710
- }
711
- }
712
- }
713
- if (evaluation.decision === 'deny') {
714
- logAudit({ status: 'denied', error: evaluation.reason }, 0);
715
- return {
716
- content: [{ type: 'text', text: `DENIED: ${evaluation.reason}` }],
717
- isError: true,
718
- };
719
- }
720
- // Circuit breaker: deny if the same tool+args is called too many times
721
- const cbVerdict = deps.circuitBreaker.check(toolInfo.name, argsForTransport);
722
- if (!cbVerdict.allowed) {
723
- logAudit({ status: 'denied', error: cbVerdict.reason }, 0);
724
- return {
725
- content: [{ type: 'text', text: cbVerdict.reason }],
726
- isError: true,
727
- };
728
- }
729
- // Virtual proxy tools: handle locally, no backend forwarding
730
- if (toolInfo.serverName === 'proxy' && deps.controlApiClient) {
731
- const startTime = Date.now();
732
- try {
733
- const result = await handleVirtualProxyTool(toolInfo.name, argsForTransport, deps.controlApiClient);
734
- logAudit({ status: 'success' }, Date.now() - startTime);
735
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
736
- }
737
- catch (err) {
738
- const errorMessage = err instanceof Error ? err.message : String(err);
739
- logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
740
- return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
741
- }
742
- }
743
- // Policy allows -- forward to the real MCP server with transport args
744
- const startTime = Date.now();
745
- try {
746
- const clientState = deps.clientStates.get(toolInfo.serverName);
747
- if (!clientState) {
748
- const err = `Internal error: no client connection for server "${toolInfo.serverName}"`;
749
- logAudit({ status: 'denied', error: err }, 0);
750
- return { content: [{ type: 'text', text: err }], isError: true };
751
- }
752
- const client = clientState.client;
753
- // CompatibilityCallToolResultSchema accepts the legacy `toolResult` response format
754
- // (protocol version 2024-10-07). Output schema validation is intentionally
755
- // bypassed by the permissiveJsonSchemaValidator injected at Client construction time.
756
- const callToolArgs = { name: toolInfo.name, arguments: argsForTransport };
757
- const callToolOpts = { timeout: getEscalationTimeoutMs() };
758
- let result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
759
- // Race condition mitigation: after roots expansion the filesystem server
760
- // has been notified but may not have finished async-validating the new
761
- // roots (fs.realpath, fs.stat). Retry once after a short delay.
762
- if (rootsExpanded && isRootsRaceError(result)) {
763
- const initialError = extractTextFromContent(result.content) ?? 'access denied (roots race)';
764
- logAudit({ status: 'error', error: `Roots race detected, retrying: ${initialError}` }, Date.now() - startTime);
765
- await new Promise((r) => setTimeout(r, ROOTS_RACE_RETRY_DELAY_MS));
766
- result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
767
- }
768
- // Reverse-rewrite host sandbox paths to container workspace paths in results
769
- const rewrittenContent = deps.allowedDirectory
770
- ? rewriteResultContent(result.content, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
771
- : result.content;
772
- if (result.isError) {
773
- const errorText = extractTextFromContent(rewrittenContent) ?? 'Unknown tool error';
774
- const errorMessage = annotateSandboxViolation(errorText, serverIsSandboxed);
775
- logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
776
- return { content: rewrittenContent, isError: true };
777
- }
778
- updateServerContext(deps.serverContextMap, toolInfo.serverName, toolInfo.name, argsForTransport);
779
- logAudit({ status: 'success' }, Date.now() - startTime);
780
- return { content: rewrittenContent };
781
- }
782
- catch (err) {
783
- const rawError = extractMcpErrorMessage(err);
784
- const errorMessage = annotateSandboxViolation(rawError, serverIsSandboxed);
785
- logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
786
- const errorContent = [{ type: 'text', text: `Error: ${errorMessage}` }];
787
- return {
788
- content: deps.allowedDirectory
789
- ? rewriteResultContent(errorContent, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
790
- : errorContent,
791
- isError: true,
792
- };
793
- }
794
- }
795
231
  /**
796
232
  * Selects and validates the proxy transport based on environment variables.
797
233
  * Returns 'tcp', 'uds', or 'stdio' along with the transport options.
@@ -814,27 +250,30 @@ export function selectTransportConfig() {
814
250
  // ── main() -- thin orchestrator ────────────────────────────────────────
815
251
  async function main() {
816
252
  const envConfig = parseProxyEnvConfig();
817
- const { auditLogPath, serversConfig, generatedDir, toolAnnotationsDir, protectedPaths, sessionLogPath, allowedDirectory, escalationDir, serverCredentials, sandboxPolicy, auditRedaction, } = envConfig;
818
- const { compiledPolicy, toolAnnotations, dynamicLists } = loadGeneratedPolicy({
253
+ const { serversConfig, generatedDir, toolAnnotationsDir, sessionLogPath, allowedDirectory, serverCredentials, sandboxPolicy, serverFilter, } = envConfig;
254
+ // Load compiled policy + annotations to derive MCP roots advertised
255
+ // to backend MCP servers at connection time (the backend still uses
256
+ // Roots to authorize filesystem access).
257
+ const { compiledPolicy, toolAnnotations } = loadGeneratedPolicy({
819
258
  policyDir: generatedDir,
820
259
  toolAnnotationsDir,
821
260
  fallbackDir: getPackageGeneratedDir(),
822
261
  });
823
- // Merge proxy tool annotations and policy rules into the loaded artifacts
824
- // so the PolicyEngine can evaluate proxy tool calls.
262
+ // Merge proxy tool annotations and policy rules so roots derivation
263
+ // includes any paths referenced by proxy policy rules.
825
264
  toolAnnotations.servers.proxy = {
826
265
  inputHash: 'hardcoded',
827
266
  tools: proxyAnnotations,
828
267
  };
829
268
  compiledPolicy.rules = [...proxyPolicyRules, ...compiledPolicy.rules];
830
- const serverDomainAllowlists = extractServerDomainAllowlists(serversConfig);
831
- const trustedServers = buildTrustedServerSet(serversConfig);
832
- const policyEngine = new PolicyEngine(compiledPolicy, toolAnnotations, protectedPaths, allowedDirectory, serverDomainAllowlists, dynamicLists, trustedServers);
833
- const auditLog = new AuditLog(auditLogPath, { redact: auditRedaction });
834
- const circuitBreaker = new CallCircuitBreaker();
835
- const autoApproveModel = await createAutoApproveModel(sessionLogPath);
836
269
  const policyRoots = extractPolicyRoots(compiledPolicy, allowedDirectory ?? '/tmp');
837
270
  const mcpRoots = toMcpRoots(policyRoots);
271
+ // Shared mutable roots array bridged to the coordinator. The proxy
272
+ // server's `notifications/roots/list_changed` handler (registered
273
+ // below) refreshes this in place via `server.listRoots()`, and each
274
+ // backend client's `roots/list` handler returns it. All backends
275
+ // observe the same authoritative set.
276
+ const relayRoots = [...mcpRoots];
838
277
  // ── Sandbox availability & config resolution ──────────────────────
839
278
  const { sandboxAvailable } = validateSandboxAvailability(sandboxPolicy, sessionLogPath, process.platform);
840
279
  const { resolvedSandboxConfigs, settingsDir, serverCwdPaths } = resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy);
@@ -849,10 +288,7 @@ async function main() {
849
288
  // that aren't set — the server will fail to start without them.
850
289
  const missingEnvVars = getMissingEnvVars(config.args);
851
290
  if (missingEnvVars.length > 0) {
852
- const warning = `Skipping MCP server "${serverName}": missing environment variable(s) ${missingEnvVars.join(', ')}`;
853
- process.stderr.write(`WARNING: ${warning}\n`);
854
- if (sessionLogPath)
855
- logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
291
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": missing environment variable(s) ${missingEnvVars.join(', ')}`, sessionLogPath);
856
292
  continue;
857
293
  }
858
294
  // ── OAuth token injection for servers backed by OAuth providers ──
@@ -861,18 +297,12 @@ async function main() {
861
297
  if (oauthProvider) {
862
298
  const clientCreds = loadClientCredentials(oauthProvider);
863
299
  if (!clientCreds) {
864
- const warning = `Skipping MCP server "${serverName}": no OAuth credentials. Run 'ironcurtain auth import ${oauthProvider.id} <file>'`;
865
- process.stderr.write(`WARNING: ${warning}\n`);
866
- if (sessionLogPath)
867
- logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
300
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": no OAuth credentials. Run 'ironcurtain auth import ${oauthProvider.id} <file>'`, sessionLogPath);
868
301
  continue;
869
302
  }
870
303
  const tokenProvider = new OAuthTokenProvider(oauthProvider, clientCreds);
871
304
  if (!tokenProvider.isAuthorized()) {
872
- const warning = `Skipping MCP server "${serverName}": not authorized. Run 'ironcurtain auth ${oauthProvider.id}'`;
873
- process.stderr.write(`WARNING: ${warning}\n`);
874
- if (sessionLogPath)
875
- logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
305
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": not authorized. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
876
306
  continue;
877
307
  }
878
308
  try {
@@ -928,10 +358,7 @@ async function main() {
928
358
  }
929
359
  catch (err) {
930
360
  const message = err instanceof Error ? err.message : String(err);
931
- const warning = `Skipping MCP server "${serverName}": OAuth token error: ${message}. Run 'ironcurtain auth ${oauthProvider.id}'`;
932
- process.stderr.write(`WARNING: ${warning}\n`);
933
- if (sessionLogPath)
934
- logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
361
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": OAuth token error: ${message}. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
935
362
  continue;
936
363
  }
937
364
  }
@@ -983,7 +410,11 @@ async function main() {
983
410
  capabilities: { roots: { listChanged: true } },
984
411
  jsonSchemaValidator: permissiveJsonSchemaValidator,
985
412
  });
986
- const state = { client, roots: [...mcpRoots] };
413
+ // All backend clients share the single `relayRoots` array so a
414
+ // coordinator-driven roots update (see the proxy server's
415
+ // `roots/list_changed` handler below) is visible to every backend
416
+ // on its next `roots/list` request.
417
+ const state = { client, roots: relayRoots };
987
418
  client.setRequestHandler(ListRootsRequestSchema, () => {
988
419
  if (state.rootsRefreshed) {
989
420
  state.rootsRefreshed();
@@ -1010,9 +441,21 @@ async function main() {
1010
441
  });
1011
442
  }
1012
443
  }
444
+ // Exit non-zero so the parent's `MCPClientManager.connect()` fails the
445
+ // handshake -- the existing try/catch in `connectBackendSubprocesses`
446
+ // skips the server without reaching `registerTools`.
447
+ if (!proxyCanServe(serversConfig, clientStates.size)) {
448
+ const serverNames = Object.keys(serversConfig).join(', ');
449
+ emitProxyDiagnostic('ERROR', `proxy subprocess for SERVER_FILTER="${serverFilter ?? '<unset>'}" has no connected backend (configured: ${serverNames}); exiting`, sessionLogPath);
450
+ for (const refresher of tokenRefreshers.values()) {
451
+ refresher.stop();
452
+ }
453
+ cleanupSettingsFiles(settingsDir);
454
+ process.exit(1);
455
+ }
1013
456
  // In virtual-only mode (SERVER_FILTER=proxy), register proxy tool definitions
1014
457
  // and create the control API client for communicating with the MITM proxy.
1015
- const isVirtualOnlyMode = process.env.SERVER_FILTER === 'proxy' && Object.keys(serversConfig).length === 0;
458
+ const isVirtualOnlyMode = serverFilter === 'proxy' && Object.keys(serversConfig).length === 0;
1016
459
  let controlApiClient = null;
1017
460
  if (isVirtualOnlyMode) {
1018
461
  for (const toolDef of proxyToolDefinitions) {
@@ -1030,6 +473,11 @@ async function main() {
1030
473
  }
1031
474
  controlApiClient = createControlApiClient(mitmControlAddr);
1032
475
  }
476
+ // Defense-in-depth: the relay's CallTool handler routes by bare
477
+ // tool name, so two backends exposing the same tool name would
478
+ // silently collide and route to the wrong server. Fail fast if we
479
+ // ever end up with such a collision in the loaded tool set.
480
+ assertUniqueToolNames(allTools);
1033
481
  const toolMap = buildToolMap(allTools);
1034
482
  const toolDescriptionHints = loadToolDescriptionHints();
1035
483
  const hintedTools = applyToolDescriptionHints(allTools, toolDescriptionHints);
@@ -1044,23 +492,85 @@ async function main() {
1044
492
  // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional use of low-level Server for raw JSON schema passthrough
1045
493
  const server = new Server({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { tools: {} } });
1046
494
  server.setRequestHandler(ListToolsRequestSchema, () => listToolsResponse);
1047
- const serverContextMap = new Map();
1048
- const callToolDeps = {
1049
- toolMap,
1050
- policyEngine,
1051
- auditLog,
1052
- circuitBreaker,
1053
- clientStates,
1054
- resolvedSandboxConfigs,
1055
- allowedDirectory,
1056
- escalationDir,
1057
- autoApproveModel,
1058
- serverContextMap,
1059
- whitelist: createApprovalWhitelist(),
1060
- controlApiClient,
1061
- };
495
+ // ── Roots bridging (coordinator → relay → backends) ───────────────
496
+ // The coordinator in the parent process is the authoritative source
497
+ // of MCP roots. On escalation approval it expands its roots and
498
+ // sends `notifications/roots/list_changed` to this relay. We fetch
499
+ // the fresh list from the coordinator via `server.listRoots()`,
500
+ // replace `relayRoots` in place, and propagate the notification to
501
+ // every backend client so they re-query via their `roots/list`
502
+ // handlers (registered above).
503
+ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
504
+ try {
505
+ const { roots: updatedRoots } = await server.listRoots();
506
+ // Replace contents in-place so all backend clientStates -- which
507
+ // share the same array reference as `relayRoots` -- see the new
508
+ // set on their next `roots/list` call. MCP `Root.name` is
509
+ // optional in the spec; fall back to the URI so our internal
510
+ // `name: string` contract holds.
511
+ relayRoots.length = 0;
512
+ for (const r of updatedRoots) {
513
+ relayRoots.push({ uri: r.uri, name: r.name ?? r.uri });
514
+ }
515
+ }
516
+ catch (err) {
517
+ if (sessionLogPath) {
518
+ const msg = err instanceof Error ? err.message : String(err);
519
+ logToSessionFile(sessionLogPath, `[proxy] Failed to fetch roots from coordinator: ${msg}`);
520
+ }
521
+ return;
522
+ }
523
+ await Promise.all([...clientStates.values()].map(async (state) => {
524
+ try {
525
+ await state.client.sendRootsListChanged();
526
+ }
527
+ catch (err) {
528
+ if (sessionLogPath) {
529
+ const msg = err instanceof Error ? err.message : String(err);
530
+ logToSessionFile(sessionLogPath, `[proxy] sendRootsListChanged to backend failed: ${msg}`);
531
+ }
532
+ }
533
+ }));
534
+ });
535
+ // Pass-through CallTool handler: forward to the backend client,
536
+ // return the raw result. No policy evaluation -- the coordinator
537
+ // in the parent process handles that.
1062
538
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
1063
- return handleCallTool(req.params.name, req.params.arguments ?? {}, callToolDeps);
539
+ const toolInfo = toolMap.get(req.params.name);
540
+ if (!toolInfo) {
541
+ return {
542
+ content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
543
+ isError: true,
544
+ };
545
+ }
546
+ // Virtual proxy tools stay local -- they speak to the MITM control
547
+ // socket via the ControlApiClient and never reach a backend MCP server.
548
+ const rawArgs = req.params.arguments ?? {};
549
+ if (toolInfo.serverName === 'proxy' && controlApiClient) {
550
+ try {
551
+ const result = await handleVirtualProxyTool(toolInfo.name, rawArgs, controlApiClient);
552
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
553
+ }
554
+ catch (err) {
555
+ const errorMessage = err instanceof Error ? err.message : String(err);
556
+ return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
557
+ }
558
+ }
559
+ const clientState = clientStates.get(toolInfo.serverName);
560
+ if (!clientState) {
561
+ return {
562
+ content: [{ type: 'text', text: `Internal error: no client connection for server "${toolInfo.serverName}"` }],
563
+ isError: true,
564
+ };
565
+ }
566
+ try {
567
+ const result = await clientState.client.callTool({ name: toolInfo.name, arguments: rawArgs }, CompatibilityCallToolResultSchema);
568
+ return result.isError ? { content: result.content, isError: true } : { content: result.content };
569
+ }
570
+ catch (err) {
571
+ const errorMessage = extractMcpErrorMessage(err);
572
+ return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
573
+ }
1064
574
  });
1065
575
  // ── Transport selection ───────────────────────────────────────────
1066
576
  const transportConfig = selectTransportConfig();
@@ -1105,7 +615,6 @@ async function main() {
1105
615
  catch {
1106
616
  /* ignore */
1107
617
  }
1108
- await auditLog.close();
1109
618
  process.exit(0);
1110
619
  }
1111
620
  process.on('SIGINT', () => {