@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
@@ -1,65 +1,54 @@
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 { resolveContainerSpawnCommand } from './container-command.js';
37
+ import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
34
38
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
35
39
  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';
40
+ import { appendFileSync, mkdtempSync, writeFileSync, writeSync } from 'node:fs';
38
41
  import { join, resolve } from 'node:path';
39
42
  import { fileURLToPath } from 'node:url';
40
43
  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';
44
+ import { loadGeneratedPolicy, getPackageGeneratedDir } from '../config/index.js';
45
+ import { expandTilde } from '../types/argument-roles.js';
46
+ import { extractPolicyRoots, toMcpRoots } from './policy-roots.js';
47
+ import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
56
48
  import { extractMcpErrorMessage } from './mcp-error-utils.js';
57
- import { updateServerContext, formatServerContext } from './server-context.js';
58
49
  import { permissiveJsonSchemaValidator } from './permissive-output-validator.js';
59
50
  import { VERSION } from '../version.js';
60
- import { buildTrustedServerSet } from '../memory/memory-annotations.js';
61
51
  import { loadToolDescriptionHints, applyToolDescriptionHints } from './tool-description-hints.js';
62
- import { createApprovalWhitelist, extractWhitelistCandidates, } from './approval-whitelist.js';
63
52
  import { getProviderForServer } from '../auth/oauth-registry.js';
64
53
  import { loadClientCredentials } from '../auth/oauth-provider.js';
65
54
  import { OAuthTokenProvider } from '../auth/oauth-token-provider.js';
@@ -67,68 +56,10 @@ import { loadOAuthToken } from '../auth/oauth-token-store.js';
67
56
  import { writeGWorkspaceCredentialFile } from './gworkspace-credentials.js';
68
57
  import { TokenFileRefresher } from './token-file-refresher.js';
69
58
  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
- }
59
+ import { buildToolMap, assertUniqueToolNames } from './tool-call-pipeline.js';
60
+ // ---------------------------------------------------------------------------
61
+ // Subprocess helpers
62
+ // ---------------------------------------------------------------------------
132
63
  /** Appends a timestamped line to the session log file. */
133
64
  function logToSessionFile(sessionLogPath, message) {
134
65
  const timestamp = new Date().toISOString();
@@ -157,15 +88,6 @@ function getMissingEnvVars(args) {
157
88
  }
158
89
  return missing;
159
90
  }
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
91
  /**
170
92
  * Replaces known credential values in a string with `***REDACTED***`.
171
93
  * Prevents credential leakage in session log files.
@@ -179,163 +101,40 @@ function redactCredentials(text, credentials) {
179
101
  }
180
102
  return result;
181
103
  }
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
- /**
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)
202
- */
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
104
  /**
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.
105
+ * True when this proxy subprocess is in a viable serving state -- either
106
+ * virtual-only mode (no backends configured) or at least one configured
107
+ * backend connected. Returning false means the parent should treat this
108
+ * subprocess as failed; otherwise it would register zero tools for the
109
+ * server and trigger a misleading "stale annotations" warning in the
110
+ * coordinator's drift check.
241
111
  */
242
- function cleanupEscalationFiles(requestPath, responsePath) {
243
- tryUnlink(requestPath);
244
- tryUnlink(responsePath);
112
+ export function proxyCanServe(serversConfig, connectedBackendCount) {
113
+ return Object.keys(serversConfig).length === 0 || connectedBackendCount > 0;
245
114
  }
246
115
  /**
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.
116
+ * Writes a diagnostic line to stderr and (if configured) the proxy session log.
270
117
  *
271
- * Returns 'approved' or 'denied'. On timeout, returns 'denied'.
118
+ * Uses `fs.writeSync` so the bytes reach the OS before the function returns
119
+ * even when stderr is a pipe (parent-spawned subprocess). `process.stderr.write`
120
+ * is asynchronous in that case, which would let a follow-up `process.exit(1)`
121
+ * truncate the diagnostic before delivery.
272
122
  */
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' };
123
+ function emitProxyDiagnostic(level, message, sessionLogPath) {
124
+ writeSync(2, `${level}: ${message}\n`);
125
+ if (sessionLogPath)
126
+ logToSessionFile(sessionLogPath, `[proxy] ${message}`);
290
127
  }
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
128
  /**
328
129
  * Reads and validates proxy environment variables.
329
130
  * Returns a typed config object or calls process.exit(1) on missing required vars.
330
131
  */
331
132
  export function parseProxyEnvConfig() {
332
- const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
333
133
  const serversConfigJson = process.env.MCP_SERVERS_CONFIG;
334
134
  const generatedDir = process.env.GENERATED_DIR;
335
135
  const protectedPathsJson = process.env.PROTECTED_PATHS ?? '[]';
336
136
  const sessionLogPath = process.env.SESSION_LOG_PATH;
337
137
  const allowedDirectory = process.env.ALLOWED_DIRECTORY;
338
- const escalationDir = process.env.ESCALATION_DIR;
339
138
  if (!serversConfigJson) {
340
139
  process.stderr.write('MCP_SERVERS_CONFIG environment variable is required\n');
341
140
  process.exit(1);
@@ -351,7 +150,6 @@ export function parseProxyEnvConfig() {
351
150
  : {};
352
151
  delete process.env.SERVER_CREDENTIALS;
353
152
  const sandboxPolicy = (process.env.SANDBOX_POLICY ?? 'warn');
354
- const auditRedaction = process.env.AUDIT_REDACTION === 'true';
355
153
  const allServersConfig = JSON.parse(serversConfigJson);
356
154
  const protectedPaths = JSON.parse(protectedPathsJson);
357
155
  // When SERVER_FILTER is set, only connect to that single backend server.
@@ -373,17 +171,15 @@ export function parseProxyEnvConfig() {
373
171
  // Otherwise they default to the same directory as compiled policy.
374
172
  const toolAnnotationsDir = process.env.TOOL_ANNOTATIONS_DIR ?? generatedDir;
375
173
  return {
376
- auditLogPath,
377
174
  serversConfig,
378
175
  generatedDir,
379
176
  toolAnnotationsDir,
380
177
  protectedPaths,
381
178
  sessionLogPath,
382
179
  allowedDirectory,
383
- escalationDir,
384
180
  serverCredentials,
385
181
  sandboxPolicy,
386
- auditRedaction,
182
+ serverFilter,
387
183
  };
388
184
  }
389
185
  /**
@@ -433,365 +229,6 @@ export function resolveServerSandboxConfigs(serversConfig, allowedDirectory, san
433
229
  }
434
230
  return { resolvedSandboxConfigs, settingsDir, serverCwdPaths };
435
231
  }
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
232
  /**
796
233
  * Selects and validates the proxy transport based on environment variables.
797
234
  * Returns 'tcp', 'uds', or 'stdio' along with the transport options.
@@ -814,27 +251,30 @@ export function selectTransportConfig() {
814
251
  // ── main() -- thin orchestrator ────────────────────────────────────────
815
252
  async function main() {
816
253
  const envConfig = parseProxyEnvConfig();
817
- const { auditLogPath, serversConfig, generatedDir, toolAnnotationsDir, protectedPaths, sessionLogPath, allowedDirectory, escalationDir, serverCredentials, sandboxPolicy, auditRedaction, } = envConfig;
818
- const { compiledPolicy, toolAnnotations, dynamicLists } = loadGeneratedPolicy({
254
+ const { serversConfig, generatedDir, toolAnnotationsDir, sessionLogPath, allowedDirectory, serverCredentials, sandboxPolicy, serverFilter, } = envConfig;
255
+ // Load compiled policy + annotations to derive MCP roots advertised
256
+ // to backend MCP servers at connection time (the backend still uses
257
+ // Roots to authorize filesystem access).
258
+ const { compiledPolicy, toolAnnotations } = loadGeneratedPolicy({
819
259
  policyDir: generatedDir,
820
260
  toolAnnotationsDir,
821
261
  fallbackDir: getPackageGeneratedDir(),
822
262
  });
823
- // Merge proxy tool annotations and policy rules into the loaded artifacts
824
- // so the PolicyEngine can evaluate proxy tool calls.
263
+ // Merge proxy tool annotations and policy rules so roots derivation
264
+ // includes any paths referenced by proxy policy rules.
825
265
  toolAnnotations.servers.proxy = {
826
266
  inputHash: 'hardcoded',
827
267
  tools: proxyAnnotations,
828
268
  };
829
269
  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
270
  const policyRoots = extractPolicyRoots(compiledPolicy, allowedDirectory ?? '/tmp');
837
271
  const mcpRoots = toMcpRoots(policyRoots);
272
+ // Shared mutable roots array bridged to the coordinator. The proxy
273
+ // server's `notifications/roots/list_changed` handler (registered
274
+ // below) refreshes this in place via `server.listRoots()`, and each
275
+ // backend client's `roots/list` handler returns it. All backends
276
+ // observe the same authoritative set.
277
+ const relayRoots = [...mcpRoots];
838
278
  // ── Sandbox availability & config resolution ──────────────────────
839
279
  const { sandboxAvailable } = validateSandboxAvailability(sandboxPolicy, sessionLogPath, process.platform);
840
280
  const { resolvedSandboxConfigs, settingsDir, serverCwdPaths } = resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy);
@@ -849,10 +289,7 @@ async function main() {
849
289
  // that aren't set — the server will fail to start without them.
850
290
  const missingEnvVars = getMissingEnvVars(config.args);
851
291
  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}`);
292
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": missing environment variable(s) ${missingEnvVars.join(', ')}`, sessionLogPath);
856
293
  continue;
857
294
  }
858
295
  // ── OAuth token injection for servers backed by OAuth providers ──
@@ -861,18 +298,12 @@ async function main() {
861
298
  if (oauthProvider) {
862
299
  const clientCreds = loadClientCredentials(oauthProvider);
863
300
  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}`);
301
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": no OAuth credentials. Run 'ironcurtain auth import ${oauthProvider.id} <file>'`, sessionLogPath);
868
302
  continue;
869
303
  }
870
304
  const tokenProvider = new OAuthTokenProvider(oauthProvider, clientCreds);
871
305
  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}`);
306
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": not authorized. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
876
307
  continue;
877
308
  }
878
309
  try {
@@ -928,10 +359,7 @@ async function main() {
928
359
  }
929
360
  catch (err) {
930
361
  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}`);
362
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": OAuth token error: ${message}. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
935
363
  continue;
936
364
  }
937
365
  }
@@ -946,7 +374,13 @@ async function main() {
946
374
  rewriteServerSettings(settingsPath, { allowRead: dynamicNodePaths });
947
375
  }
948
376
  }
949
- const wrapped = wrapServerCommand(serverName, config.command, config.args, resolved, settingsDir);
377
+ // Docker-based servers (e.g. github) run under the Apple `container`
378
+ // CLI on machines that have that runtime instead of Docker.
379
+ const spawnSpec = resolveContainerSpawnCommand(config.command, config.args);
380
+ if (spawnSpec.translated) {
381
+ emitProxyDiagnostic('WARNING', `MCP server "${serverName}": docker not found; running via the Apple container CLI instead`, sessionLogPath);
382
+ }
383
+ const wrapped = wrapServerCommand(serverName, spawnSpec.command, spawnSpec.args, resolved, settingsDir);
950
384
  const transport = new StdioClientTransport({
951
385
  command: wrapped.command,
952
386
  args: wrapped.args,
@@ -983,7 +417,11 @@ async function main() {
983
417
  capabilities: { roots: { listChanged: true } },
984
418
  jsonSchemaValidator: permissiveJsonSchemaValidator,
985
419
  });
986
- const state = { client, roots: [...mcpRoots] };
420
+ // All backend clients share the single `relayRoots` array so a
421
+ // coordinator-driven roots update (see the proxy server's
422
+ // `roots/list_changed` handler below) is visible to every backend
423
+ // on its next `roots/list` request.
424
+ const state = { client, roots: relayRoots };
987
425
  client.setRequestHandler(ListRootsRequestSchema, () => {
988
426
  if (state.rootsRefreshed) {
989
427
  state.rootsRefreshed();
@@ -995,9 +433,34 @@ async function main() {
995
433
  await client.connect(transport);
996
434
  }
997
435
  catch (err) {
436
+ // Per-server graceful degradation: an unconnectable backend (binary
437
+ // missing, container runtime down, server crash on startup) is
438
+ // skipped with a diagnostic, like the missing-env-var and OAuth
439
+ // paths above. When this leaves the relay with zero backends, the
440
+ // proxyCanServe guard below exits non-zero and the parent skips
441
+ // the relay — a startup failure never silently widens or narrows
442
+ // policy, since unregistered tools are denied by default.
998
443
  const cmd = `${wrapped.command} ${wrapped.args.join(' ')}`;
999
- const stderrSnippet = serverStderr ? `\nServer stderr: ${serverStderr.substring(0, 1000)}` : '';
1000
- throw new Error(`Failed to connect to MCP server "${serverName}" (${cmd}): ${err instanceof Error ? err.message : String(err)}${stderrSnippet}`, { cause: err });
444
+ // Redact credentials/OAuth env from the captured stderr before it
445
+ // reaches emitProxyDiagnostic (which persists to the session log),
446
+ // matching the redaction the streaming stderr handler applies above.
447
+ const redactedStderr = redactCredentials(redactCredentials(serverStderr, serverCredentials), oauthEnv);
448
+ const stderrSnippet = redactedStderr ? ` Server stderr: ${redactedStderr.substring(0, 1000)}` : '';
449
+ emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": failed to connect (${cmd}): ${err instanceof Error ? err.message : String(err)}${stderrSnippet}`, sessionLogPath);
450
+ // `client.connect` may have spawned the backend subprocess (via
451
+ // StdioClientTransport) before failing; close it so a half-started
452
+ // backend doesn't leak a child process for the relay's lifetime.
453
+ await client.close().catch(() => { });
454
+ // This server is being skipped; stop the OAuth refresher started for it
455
+ // above so it does not keep running force-refresh loops for a backend
456
+ // that never connected (shutdown-time cleanup would otherwise be the
457
+ // only thing that stops it).
458
+ const orphanedRefresher = tokenRefreshers.get(serverName);
459
+ if (orphanedRefresher) {
460
+ orphanedRefresher.stop();
461
+ tokenRefreshers.delete(serverName);
462
+ }
463
+ continue;
1001
464
  }
1002
465
  clientStates.set(serverName, state);
1003
466
  const result = await client.listTools();
@@ -1010,9 +473,21 @@ async function main() {
1010
473
  });
1011
474
  }
1012
475
  }
476
+ // Exit non-zero so the parent's `MCPClientManager.connect()` fails the
477
+ // handshake -- the existing try/catch in `connectBackendSubprocesses`
478
+ // skips the server without reaching `registerTools`.
479
+ if (!proxyCanServe(serversConfig, clientStates.size)) {
480
+ const serverNames = Object.keys(serversConfig).join(', ');
481
+ emitProxyDiagnostic('ERROR', `proxy subprocess for SERVER_FILTER="${serverFilter ?? '<unset>'}" has no connected backend (configured: ${serverNames}); exiting`, sessionLogPath);
482
+ for (const refresher of tokenRefreshers.values()) {
483
+ refresher.stop();
484
+ }
485
+ cleanupSettingsFiles(settingsDir);
486
+ process.exit(1);
487
+ }
1013
488
  // In virtual-only mode (SERVER_FILTER=proxy), register proxy tool definitions
1014
489
  // 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;
490
+ const isVirtualOnlyMode = serverFilter === 'proxy' && Object.keys(serversConfig).length === 0;
1016
491
  let controlApiClient = null;
1017
492
  if (isVirtualOnlyMode) {
1018
493
  for (const toolDef of proxyToolDefinitions) {
@@ -1030,6 +505,11 @@ async function main() {
1030
505
  }
1031
506
  controlApiClient = createControlApiClient(mitmControlAddr);
1032
507
  }
508
+ // Defense-in-depth: the relay's CallTool handler routes by bare
509
+ // tool name, so two backends exposing the same tool name would
510
+ // silently collide and route to the wrong server. Fail fast if we
511
+ // ever end up with such a collision in the loaded tool set.
512
+ assertUniqueToolNames(allTools);
1033
513
  const toolMap = buildToolMap(allTools);
1034
514
  const toolDescriptionHints = loadToolDescriptionHints();
1035
515
  const hintedTools = applyToolDescriptionHints(allTools, toolDescriptionHints);
@@ -1044,23 +524,85 @@ async function main() {
1044
524
  // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional use of low-level Server for raw JSON schema passthrough
1045
525
  const server = new Server({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { tools: {} } });
1046
526
  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
- };
527
+ // ── Roots bridging (coordinator → relay → backends) ───────────────
528
+ // The coordinator in the parent process is the authoritative source
529
+ // of MCP roots. On escalation approval it expands its roots and
530
+ // sends `notifications/roots/list_changed` to this relay. We fetch
531
+ // the fresh list from the coordinator via `server.listRoots()`,
532
+ // replace `relayRoots` in place, and propagate the notification to
533
+ // every backend client so they re-query via their `roots/list`
534
+ // handlers (registered above).
535
+ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
536
+ try {
537
+ const { roots: updatedRoots } = await server.listRoots();
538
+ // Replace contents in-place so all backend clientStates -- which
539
+ // share the same array reference as `relayRoots` -- see the new
540
+ // set on their next `roots/list` call. MCP `Root.name` is
541
+ // optional in the spec; fall back to the URI so our internal
542
+ // `name: string` contract holds.
543
+ relayRoots.length = 0;
544
+ for (const r of updatedRoots) {
545
+ relayRoots.push({ uri: r.uri, name: r.name ?? r.uri });
546
+ }
547
+ }
548
+ catch (err) {
549
+ if (sessionLogPath) {
550
+ const msg = err instanceof Error ? err.message : String(err);
551
+ logToSessionFile(sessionLogPath, `[proxy] Failed to fetch roots from coordinator: ${msg}`);
552
+ }
553
+ return;
554
+ }
555
+ await Promise.all([...clientStates.values()].map(async (state) => {
556
+ try {
557
+ await state.client.sendRootsListChanged();
558
+ }
559
+ catch (err) {
560
+ if (sessionLogPath) {
561
+ const msg = err instanceof Error ? err.message : String(err);
562
+ logToSessionFile(sessionLogPath, `[proxy] sendRootsListChanged to backend failed: ${msg}`);
563
+ }
564
+ }
565
+ }));
566
+ });
567
+ // Pass-through CallTool handler: forward to the backend client,
568
+ // return the raw result. No policy evaluation -- the coordinator
569
+ // in the parent process handles that.
1062
570
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
1063
- return handleCallTool(req.params.name, req.params.arguments ?? {}, callToolDeps);
571
+ const toolInfo = toolMap.get(req.params.name);
572
+ if (!toolInfo) {
573
+ return {
574
+ content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
575
+ isError: true,
576
+ };
577
+ }
578
+ // Virtual proxy tools stay local -- they speak to the MITM control
579
+ // socket via the ControlApiClient and never reach a backend MCP server.
580
+ const rawArgs = req.params.arguments ?? {};
581
+ if (toolInfo.serverName === 'proxy' && controlApiClient) {
582
+ try {
583
+ const result = await handleVirtualProxyTool(toolInfo.name, rawArgs, controlApiClient);
584
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
585
+ }
586
+ catch (err) {
587
+ const errorMessage = err instanceof Error ? err.message : String(err);
588
+ return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
589
+ }
590
+ }
591
+ const clientState = clientStates.get(toolInfo.serverName);
592
+ if (!clientState) {
593
+ return {
594
+ content: [{ type: 'text', text: `Internal error: no client connection for server "${toolInfo.serverName}"` }],
595
+ isError: true,
596
+ };
597
+ }
598
+ try {
599
+ const result = await clientState.client.callTool({ name: toolInfo.name, arguments: rawArgs }, CompatibilityCallToolResultSchema);
600
+ return result.isError ? { content: result.content, isError: true } : { content: result.content };
601
+ }
602
+ catch (err) {
603
+ const errorMessage = extractMcpErrorMessage(err);
604
+ return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
605
+ }
1064
606
  });
1065
607
  // ── Transport selection ───────────────────────────────────────────
1066
608
  const transportConfig = selectTransportConfig();
@@ -1105,7 +647,6 @@ async function main() {
1105
647
  catch {
1106
648
  /* ignore */
1107
649
  }
1108
- await auditLog.close();
1109
650
  process.exit(0);
1110
651
  }
1111
652
  process.on('SIGINT', () => {