@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
@@ -0,0 +1,832 @@
1
+ /**
2
+ * Tool Call Pipeline -- The core security evaluation logic.
3
+ *
4
+ * Owns the full tool-call policy pipeline: argument validation,
5
+ * annotation-driven normalization, policy evaluation, escalation
6
+ * (file-IPC and in-process), auto-approval, circuit breaking,
7
+ * whitelist matching, roots expansion, dispatch, and audit logging.
8
+ */
9
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+ import { atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
13
+ import { extractAnnotatedPaths } from './policy-engine.js';
14
+ import { getPathRoles } from '../types/argument-roles.js';
15
+ import { prepareToolArgs, rewriteResultContent } from './path-utils.js';
16
+ import { CONTAINER_WORKSPACE_DIR } from '../docker/agent-adapter.js';
17
+ import { directoryForPath } from './policy-roots.js';
18
+ import { annotateSandboxViolation } from './sandbox-integration.js';
19
+ import { ROOTS_REFRESH_TIMEOUT_MS } from './mcp-client-manager.js';
20
+ import { autoApprove, extractArgsForAutoApprove, readUserContext } from './auto-approver.js';
21
+ import { extractMcpErrorMessage } from './mcp-error-utils.js';
22
+ import { updateServerContext, formatServerContext } from './server-context.js';
23
+ import { extractWhitelistCandidates, } from './approval-whitelist.js';
24
+ import { handleVirtualProxyTool } from '../docker/proxy-tools.js';
25
+ import { ERROR_PREFIX_DENIED, ERROR_PREFIX_ESCALATION_REQUIRED, ERROR_PREFIX_ESCALATION_DENIED, ERROR_PREFIX_UNKNOWN_ARGS, ERROR_PREFIX_MISSING_ANNOTATION, } from './error-prefixes.js';
26
+ import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
27
+ /**
28
+ * Adds a root to a client's root list and waits for the server to
29
+ * fetch the updated list. No-op if the root URI is already present.
30
+ * Times out after ROOTS_REFRESH_TIMEOUT_MS if the server never requests
31
+ * the updated list (e.g. servers that don't implement Roots protocol).
32
+ *
33
+ * Returns `'added'` when the server acknowledged the update (called
34
+ * `roots/list`), `'timeout'` when the server didn't respond in time,
35
+ * or `false` when the root was already present.
36
+ */
37
+ async function addRootToClient(state, root) {
38
+ if (state.roots.some((r) => r.uri === root.uri))
39
+ return false;
40
+ state.roots.push(root);
41
+ let timer;
42
+ let outcome = 'timeout';
43
+ const refreshed = new Promise((resolve) => {
44
+ state.rootsRefreshed = () => {
45
+ clearTimeout(timer);
46
+ outcome = 'added';
47
+ resolve();
48
+ };
49
+ });
50
+ const timeout = new Promise((resolve) => {
51
+ timer = setTimeout(() => {
52
+ state.rootsRefreshed = undefined;
53
+ resolve();
54
+ }, ROOTS_REFRESH_TIMEOUT_MS);
55
+ timer.unref();
56
+ });
57
+ await state.client.sendRootsListChanged();
58
+ await Promise.race([refreshed, timeout]);
59
+ return outcome;
60
+ }
61
+ /**
62
+ * Delay before retrying a tool call that returned "access denied" immediately
63
+ * after roots expansion. The filesystem server needs time to async-validate
64
+ * (fs.realpath, fs.stat) the new roots after receiving the rootsListChanged
65
+ * notification. Exported for test access.
66
+ */
67
+ export const ROOTS_RACE_RETRY_DELAY_MS = 200;
68
+ /**
69
+ * Checks whether an MCP tool call result looks like a roots-race "access denied"
70
+ * error -- the filesystem server rejected the call because it hasn't finished
71
+ * processing the updated roots yet.
72
+ */
73
+ function isRootsRaceError(result) {
74
+ if (!('isError' in result) || !result.isError)
75
+ return false;
76
+ const text = extractTextFromContent(result.content);
77
+ if (!text)
78
+ return false;
79
+ const lower = text.toLowerCase();
80
+ return lower.includes('access denied') || lower.includes('outside allowed directories');
81
+ }
82
+ const ESCALATION_POLL_INTERVAL_MS = 500;
83
+ const DEFAULT_ESCALATION_TIMEOUT_SECONDS = 300;
84
+ /** Auto-approve trusted input older than this is rejected as stale. */
85
+ const TRUSTED_INPUT_STALENESS_MS = 120_000;
86
+ /**
87
+ * Returns true when a user context is safe to act on for auto-approval.
88
+ *
89
+ * For PTY sessions (isPtySession=true):
90
+ * - source must be 'mux-trusted-input' (set by the trusted mux layer)
91
+ * - timestamp must be present, valid, not in the future, and within
92
+ * TRUSTED_INPUT_STALENESS_MS of `now`
93
+ *
94
+ * For non-PTY sessions:
95
+ * - source is not checked (no mux layer in the path)
96
+ * - a missing or invalid timestamp does not prevent trust; a valid stale
97
+ * timestamp still prevents trust (belt-and-suspenders for future callers)
98
+ *
99
+ * @param context - The user context parsed from user-context.json
100
+ * @param isPtySession - Whether this is a PTY interactive session
101
+ * @param now - Current time in ms; defaults to Date.now() (injectable for tests)
102
+ */
103
+ export function isUserContextTrusted(context, isPtySession, now = Date.now()) {
104
+ const sourceValid = !isPtySession || context.source === 'mux-trusted-input';
105
+ if (!sourceValid)
106
+ return false;
107
+ let stale = isPtySession; // non-PTY sessions don't require timestamps
108
+ if (context.timestamp !== undefined) {
109
+ const tsMs = new Date(context.timestamp).getTime();
110
+ if (!Number.isNaN(tsMs)) {
111
+ const ageMs = now - tsMs;
112
+ stale = ageMs > TRUSTED_INPUT_STALENESS_MS || ageMs < 0;
113
+ }
114
+ }
115
+ return !stale;
116
+ }
117
+ /** Reads escalation timeout from env var, falling back to default. */
118
+ function getEscalationTimeoutMs() {
119
+ const envValue = process.env.ESCALATION_TIMEOUT_SECONDS;
120
+ if (envValue) {
121
+ const parsed = Number(envValue);
122
+ if (Number.isFinite(parsed) && parsed > 0) {
123
+ return parsed * 1000;
124
+ }
125
+ }
126
+ return DEFAULT_ESCALATION_TIMEOUT_SECONDS * 1000;
127
+ }
128
+ function tryUnlink(path) {
129
+ try {
130
+ unlinkSync(path);
131
+ }
132
+ catch {
133
+ /* ignore */
134
+ }
135
+ }
136
+ /**
137
+ * Removes both escalation IPC files. Request is deleted first so the
138
+ * session side sees "request gone + response exists" as normal
139
+ * approval-in-progress rather than an expiry.
140
+ */
141
+ function cleanupEscalationFiles(requestPath, responsePath) {
142
+ tryUnlink(requestPath);
143
+ tryUnlink(responsePath);
144
+ }
145
+ /**
146
+ * Reads and parses the escalation response file if it exists.
147
+ * Returns the response data, or undefined if the file is not present.
148
+ */
149
+ function readEscalationResponse(responsePath) {
150
+ if (!existsSync(responsePath))
151
+ return undefined;
152
+ const raw = JSON.parse(readFileSync(responsePath, 'utf-8'));
153
+ // Fail-closed: anything other than an explicit 'approved' or 'denied'
154
+ // string (typos, missing field, null, etc.) must collapse to denied.
155
+ // Treating unrecognized values as approval would silently fail-open
156
+ // on any malformed escalation response.
157
+ if (raw.decision !== 'approved' && raw.decision !== 'denied') {
158
+ return { decision: 'denied' };
159
+ }
160
+ // Validate whitelistSelection: must be an integer if present, discard otherwise
161
+ let whitelistSelection;
162
+ if (typeof raw.whitelistSelection === 'number' && Number.isInteger(raw.whitelistSelection)) {
163
+ whitelistSelection = raw.whitelistSelection;
164
+ }
165
+ return {
166
+ decision: raw.decision,
167
+ ...(whitelistSelection !== undefined ? { whitelistSelection } : {}),
168
+ };
169
+ }
170
+ /**
171
+ * Waits for a human decision via file-based IPC.
172
+ *
173
+ * Writes a request file to the escalation directory, then polls
174
+ * for a response file. The session process (on the other side)
175
+ * detects the request, surfaces it to the user, and writes the response.
176
+ *
177
+ * Returns 'approved' or 'denied'. On timeout, returns 'denied'.
178
+ */
179
+ async function waitForEscalationDecision(escalationDir, request) {
180
+ const requestPath = resolve(escalationDir, `request-${request.escalationId}.json`);
181
+ const responsePath = resolve(escalationDir, `response-${request.escalationId}.json`);
182
+ atomicWriteJsonSync(requestPath, request);
183
+ const deadline = Date.now() + getEscalationTimeoutMs();
184
+ while (Date.now() < deadline) {
185
+ const response = readEscalationResponse(responsePath);
186
+ if (response) {
187
+ cleanupEscalationFiles(requestPath, responsePath);
188
+ return response;
189
+ }
190
+ await new Promise((r) => setTimeout(r, ESCALATION_POLL_INTERVAL_MS));
191
+ }
192
+ // Final check -- response may have arrived between last poll and deadline
193
+ const lateResponse = readEscalationResponse(responsePath);
194
+ cleanupEscalationFiles(requestPath, responsePath);
195
+ return lateResponse ?? { decision: 'denied' };
196
+ }
197
+ /** Extracts concatenated text from MCP content blocks. */
198
+ export function extractTextFromContent(content) {
199
+ if (!Array.isArray(content))
200
+ return undefined;
201
+ const texts = content
202
+ .filter((c) => c.type === 'text' && typeof c.text === 'string')
203
+ .map((c) => c.text);
204
+ return texts.length > 0 ? texts.join('\n') : undefined;
205
+ }
206
+ /**
207
+ * Returns the JSON-Schema primitive type name for a JavaScript value,
208
+ * or `undefined` for values that don't map to a single JSON-Schema
209
+ * primitive (e.g. functions, symbols). Distinguishes `array` and
210
+ * `null` from `object` the way JSON Schema does.
211
+ */
212
+ function jsonSchemaTypeOf(value) {
213
+ if (value === null)
214
+ return 'null';
215
+ if (Array.isArray(value))
216
+ return 'array';
217
+ const t = typeof value;
218
+ if (t === 'string' || t === 'number' || t === 'boolean' || t === 'object')
219
+ return t;
220
+ return undefined;
221
+ }
222
+ /**
223
+ * Returns `true` when `value` is compatible with the JSON-Schema
224
+ * primitive `expected`. Only single-level primitive matching -- we
225
+ * don't recurse into nested object/array shapes; that's the MCP
226
+ * server's job.
227
+ *
228
+ * `integer` accepts finite whole numbers; `number` accepts any finite
229
+ * number. All other expected types must match exactly.
230
+ */
231
+ function matchesJsonSchemaType(value, expected) {
232
+ switch (expected) {
233
+ case 'integer':
234
+ return typeof value === 'number' && Number.isInteger(value);
235
+ case 'number':
236
+ return typeof value === 'number' && Number.isFinite(value);
237
+ case 'string':
238
+ case 'boolean':
239
+ case 'null':
240
+ case 'array':
241
+ case 'object':
242
+ return jsonSchemaTypeOf(value) === expected;
243
+ default:
244
+ // Unknown or composite types (e.g., string-array in schema) are
245
+ // out of scope for this primitive type-check. Treat as pass so
246
+ // we don't reject calls whose schema uses richer constructs.
247
+ return true;
248
+ }
249
+ }
250
+ /**
251
+ * Validates tool call arguments against the tool's `inputSchema`.
252
+ * Returns `null` if valid, or an error message describing the first
253
+ * problem encountered.
254
+ *
255
+ * Checks performed (in order):
256
+ * 1. Unknown argument names — any key not declared in
257
+ * `properties` fails with a listing of valid keys.
258
+ * 2. Primitive type mismatch — for each provided argument whose
259
+ * schema has a `type` string, the JS value's JSON-Schema
260
+ * primitive type must match. Does not recurse into object/array
261
+ * element shapes.
262
+ *
263
+ * Skips validation in two cases:
264
+ * - The schema has no `properties` object (nothing to check against).
265
+ * - `additionalProperties` is truthy (explicit opt-out).
266
+ *
267
+ * NOTE: This is stricter than plain JSON Schema, which treats a
268
+ * missing `additionalProperties` as equivalent to `true`. Here we
269
+ * treat it as forbidding unknown keys so typos in tool arguments
270
+ * fail fast instead of silently passing through to the MCP server.
271
+ * Servers that genuinely accept extra keys must declare
272
+ * `additionalProperties: true` explicitly.
273
+ */
274
+ export function validateToolArguments(args, inputSchema) {
275
+ const properties = inputSchema.properties;
276
+ if (!properties || typeof properties !== 'object')
277
+ return null;
278
+ if (inputSchema.additionalProperties)
279
+ return null;
280
+ const props = properties;
281
+ const validKeys = new Set(Object.keys(props));
282
+ const unknownKeys = Object.keys(args).filter((k) => !validKeys.has(k));
283
+ if (unknownKeys.length > 0) {
284
+ const unknownList = unknownKeys.map((k) => `"${k}"`).join(', ');
285
+ const validList = [...validKeys]
286
+ .sort()
287
+ .map((k) => `"${k}"`)
288
+ .join(', ');
289
+ return `${ERROR_PREFIX_UNKNOWN_ARGS} ${unknownList}. Valid parameters are: ${validList}`;
290
+ }
291
+ // Primitive type check: for each provided arg whose schema declares
292
+ // a primitive `type`, verify the argument's runtime type matches.
293
+ // This catches the "agent passed an object where a string was
294
+ // required" case before path normalization stringifies the object
295
+ // into a meaningless sandbox-escape message.
296
+ for (const key of Object.keys(args)) {
297
+ const propSchema = props[key];
298
+ if (!propSchema || typeof propSchema !== 'object')
299
+ continue;
300
+ const expected = propSchema.type;
301
+ if (typeof expected !== 'string')
302
+ continue;
303
+ if (!matchesJsonSchemaType(args[key], expected)) {
304
+ const received = jsonSchemaTypeOf(args[key]) ?? typeof args[key];
305
+ const article = /^[aeiou]/i.test(expected) ? 'an' : 'a';
306
+ return `Argument "${key}" must be ${article} ${expected} (got ${received}).`;
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ /**
312
+ * Splits a coordinator tool key of the form `server__tool` into its
313
+ * component parts. Falls back to `('unknown', key)` when the key has
314
+ * no separator (e.g. a raw tool name passed from an unregistered
315
+ * caller). Used for audit-log attribution on unknown-tool rejections.
316
+ */
317
+ export function splitToolKey(key) {
318
+ const sep = key.indexOf('__');
319
+ if (sep < 0)
320
+ return ['unknown', key];
321
+ return [key.slice(0, sep), key.slice(sep + 2)];
322
+ }
323
+ /** Builds a lookup map from tool name to ProxiedTool for routing. */
324
+ export function buildToolMap(allTools) {
325
+ const toolMap = new Map();
326
+ for (const tool of allTools) {
327
+ toolMap.set(tool.name, tool);
328
+ }
329
+ return toolMap;
330
+ }
331
+ /**
332
+ * Asserts that no two tools share the same bare name across servers.
333
+ *
334
+ * The relay's `CallTool` handler (in `mcp-proxy-server.ts`) routes by
335
+ * bare tool name via `toolMap`, which is last-write-wins on
336
+ * collisions. When `SERVER_FILTER` is set, only one backend is loaded
337
+ * and the check is trivially satisfied; multi-backend modes (e.g. the
338
+ * pipeline's list resolver) must not load two servers that expose the
339
+ * same tool name, or calls would silently route to the wrong backend.
340
+ *
341
+ * Throws with a descriptive message listing each duplicate tool and
342
+ * the servers that exposed it. Safe in single-server mode: no work.
343
+ */
344
+ export function assertUniqueToolNames(allTools) {
345
+ const byName = new Map();
346
+ for (const tool of allTools) {
347
+ const owners = byName.get(tool.name);
348
+ if (owners) {
349
+ owners.push(tool.serverName);
350
+ }
351
+ else {
352
+ byName.set(tool.name, [tool.serverName]);
353
+ }
354
+ }
355
+ const collisions = [...byName.entries()].filter(([, owners]) => owners.length > 1);
356
+ if (collisions.length === 0)
357
+ return;
358
+ const details = collisions.map(([name, owners]) => ` "${name}" exposed by: ${owners.join(', ')}`).join('\n');
359
+ throw new Error('Relay subprocess cannot load multiple backends that expose the same tool name. ' +
360
+ 'The CallTool handler routes by bare tool name, so collisions would route to ' +
361
+ 'the wrong backend. Either set SERVER_FILTER to load a single server, or ' +
362
+ 'resolve the collisions at the backend layer.\n' +
363
+ `Duplicate tool names:\n${details}`);
364
+ }
365
+ /**
366
+ * Creates an audit entry and logs it. Extracted so the audit shape
367
+ * construction is testable independently.
368
+ */
369
+ export function buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, options) {
370
+ return {
371
+ timestamp: request.timestamp,
372
+ requestId: request.requestId,
373
+ serverName: request.serverName,
374
+ toolName: request.toolName,
375
+ arguments: argsForTransport,
376
+ policyDecision,
377
+ escalationResult: options.escalationResult,
378
+ result,
379
+ durationMs,
380
+ sandboxed: options.sandboxed || undefined,
381
+ autoApproved: options.autoApproved || undefined,
382
+ whitelistApproved: options.whitelistApproved || undefined,
383
+ whitelistPatternId: options.whitelistPatternId,
384
+ persona: options.persona,
385
+ };
386
+ }
387
+ /**
388
+ * Handles a single tool call request: policy evaluation, escalation,
389
+ * circuit breaker check, and forwarding to the real MCP server.
390
+ *
391
+ * This is the core security logic -- the single source of truth for
392
+ * the policy evaluation pipeline.
393
+ *
394
+ * @param requestContext - optional caller-provided requestId/timestamp
395
+ * so structured callers keep correlation with audit entries.
396
+ * When omitted, both are generated internally.
397
+ */
398
+ export async function handleCallTool(toolName, rawArgs, deps, requestContext) {
399
+ // Use the caller's requestId/timestamp when supplied so structured
400
+ // callers can correlate their tracing with the audit entries we
401
+ // emit. Low-level callers (e.g. UTCP) leave `requestContext` unset
402
+ // and we synthesize fresh values below.
403
+ const callerRequestId = requestContext?.requestId ?? uuidv4();
404
+ const callerTimestamp = requestContext?.timestamp ?? new Date().toISOString();
405
+ const toolInfo = deps.toolMap.get(toolName);
406
+ if (!toolInfo) {
407
+ // Security invariant: every tool call outcome is audited, even
408
+ // the ones that short-circuit before policy evaluation.
409
+ const reason = `Unknown tool: ${toolName}`;
410
+ const [unknownServer, unknownTool] = splitToolKey(toolName);
411
+ deps.auditLog.log({
412
+ timestamp: callerTimestamp,
413
+ requestId: callerRequestId,
414
+ serverName: unknownServer,
415
+ toolName: unknownTool,
416
+ arguments: rawArgs,
417
+ policyDecision: { status: 'deny', rule: 'unknown-tool', reason },
418
+ result: { status: 'denied', error: reason },
419
+ durationMs: 0,
420
+ persona: deps.persona,
421
+ });
422
+ return {
423
+ content: [{ type: 'text', text: reason }],
424
+ isError: true,
425
+ _policyDecision: { status: 'deny', rule: 'unknown-tool', reason },
426
+ };
427
+ }
428
+ // Annotation-driven normalization: split into transport vs policy args.
429
+ // Trusted servers skip annotation lookup and prepareToolArgs -- use raw args directly.
430
+ const annotation = deps.policyEngine.getAnnotation(toolInfo.serverName, toolInfo.name, rawArgs);
431
+ const isTrusted = !annotation && deps.policyEngine.isTrustedServer(toolInfo.serverName);
432
+ if (!annotation && !isTrusted) {
433
+ const reason = `${ERROR_PREFIX_MISSING_ANNOTATION} ${toolInfo.serverName}__${toolInfo.name}. Re-run 'ironcurtain annotate-tools' to update.`;
434
+ // Security invariant: every tool call outcome is audited.
435
+ deps.auditLog.log({
436
+ timestamp: callerTimestamp,
437
+ requestId: callerRequestId,
438
+ serverName: toolInfo.serverName,
439
+ toolName: toolInfo.name,
440
+ arguments: rawArgs,
441
+ policyDecision: { status: 'deny', rule: 'missing-annotation', reason },
442
+ result: { status: 'denied', error: reason },
443
+ durationMs: 0,
444
+ persona: deps.persona,
445
+ });
446
+ return {
447
+ content: [{ type: 'text', text: reason }],
448
+ isError: true,
449
+ _policyDecision: { status: 'deny', rule: 'missing-annotation', reason },
450
+ };
451
+ }
452
+ // Validate argument names against the tool's schema.
453
+ // Skip for trusted servers (check directly, not via isTrusted which also requires missing annotation).
454
+ if (!deps.policyEngine.isTrustedServer(toolInfo.serverName)) {
455
+ const validationError = validateToolArguments(rawArgs, toolInfo.inputSchema);
456
+ if (validationError) {
457
+ deps.auditLog.log({
458
+ timestamp: callerTimestamp,
459
+ requestId: callerRequestId,
460
+ serverName: toolInfo.serverName,
461
+ toolName: toolInfo.name,
462
+ arguments: rawArgs,
463
+ policyDecision: { status: 'deny', rule: 'invalid-arguments', reason: validationError },
464
+ result: { status: 'denied', error: validationError },
465
+ durationMs: 0,
466
+ persona: deps.persona,
467
+ });
468
+ return {
469
+ content: [{ type: 'text', text: validationError }],
470
+ isError: true,
471
+ _policyDecision: { status: 'deny', rule: 'invalid-arguments', reason: validationError },
472
+ };
473
+ }
474
+ }
475
+ let argsForTransport;
476
+ let argsForPolicy;
477
+ if (isTrusted) {
478
+ // Trusted server with no annotation: use raw args for both transport and policy
479
+ argsForTransport = rawArgs;
480
+ argsForPolicy = rawArgs;
481
+ }
482
+ else {
483
+ // annotation is guaranteed non-null here: we returned early if both are falsy
484
+ const resolvedAnnotation = annotation;
485
+ // Enrich git tool args with the locally tracked working directory when
486
+ // the agent omits the `path` argument. The git MCP server tracks its own
487
+ // CWD (set via git_set_working_dir) and uses it implicitly, but the
488
+ // policy engine needs the explicit path to resolve sandbox containment
489
+ // and the default remote URL for domain-based policy rules.
490
+ // The serverContextMap mirrors the git server's working directory from
491
+ // successful git_set_working_dir / git_clone calls, avoiding an RPC.
492
+ let effectiveRawArgs = rawArgs;
493
+ const hasEffectivePath = 'path' in rawArgs && typeof rawArgs.path === 'string' && rawArgs.path.trim() !== '';
494
+ if (toolInfo.serverName === 'git' && 'path' in resolvedAnnotation.args && !hasEffectivePath) {
495
+ const gitWorkDir = deps.serverContextMap.get('git')?.workingDirectory;
496
+ if (!gitWorkDir) {
497
+ const errorMsg = 'Git server has no working directory set. Call git_set_working_dir first.';
498
+ deps.auditLog.log(buildAuditEntry({
499
+ requestId: callerRequestId,
500
+ serverName: toolInfo.serverName,
501
+ toolName: toolInfo.name,
502
+ arguments: rawArgs,
503
+ timestamp: callerTimestamp,
504
+ }, rawArgs, { status: 'deny', rule: 'git-path-enrichment-failed', reason: errorMsg }, { status: 'denied', error: errorMsg }, 0, { persona: deps.persona }));
505
+ return {
506
+ content: [{ type: 'text', text: `Error: ${errorMsg}` }],
507
+ isError: true,
508
+ _policyDecision: { status: 'deny', rule: 'git-path-enrichment-failed', reason: errorMsg },
509
+ };
510
+ }
511
+ effectiveRawArgs = { ...rawArgs, path: gitWorkDir };
512
+ }
513
+ ({ argsForTransport, argsForPolicy } = prepareToolArgs(effectiveRawArgs, resolvedAnnotation, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR));
514
+ }
515
+ const request = {
516
+ requestId: callerRequestId,
517
+ serverName: toolInfo.serverName,
518
+ toolName: toolInfo.name,
519
+ arguments: argsForPolicy,
520
+ timestamp: callerTimestamp,
521
+ };
522
+ const evaluation = deps.policyEngine.evaluate(request);
523
+ const policyDecision = {
524
+ status: evaluation.decision,
525
+ rule: evaluation.rule,
526
+ reason: evaluation.reason,
527
+ };
528
+ let escalationResult;
529
+ let autoApproved = false;
530
+ let whitelistApproved = false;
531
+ let whitelistPatternId;
532
+ let rootsExpanded = false;
533
+ const serverSandboxConfig = deps.resolvedSandboxConfigs.get(toolInfo.serverName);
534
+ const serverIsSandboxed = serverSandboxConfig?.sandboxed === true;
535
+ function logAudit(result, durationMs, overrideEscalation) {
536
+ const entry = buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, {
537
+ escalationResult: overrideEscalation ?? escalationResult,
538
+ sandboxed: serverIsSandboxed,
539
+ autoApproved,
540
+ whitelistApproved: whitelistApproved || undefined,
541
+ whitelistPatternId,
542
+ persona: deps.persona,
543
+ });
544
+ deps.auditLog.log(entry);
545
+ }
546
+ if (evaluation.decision === 'escalate') {
547
+ // Annotation is guaranteed non-null here: trusted servers always allow (never escalate),
548
+ // and missing-annotation returns early above. Narrow the type for TypeScript.
549
+ const resolvedAnnotation = annotation;
550
+ const whitelistMatch = deps.whitelist.match(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation);
551
+ if (whitelistMatch.matched) {
552
+ whitelistApproved = true;
553
+ whitelistPatternId = whitelistMatch.patternId;
554
+ escalationResult = 'approved';
555
+ policyDecision.status = 'allow';
556
+ policyDecision.reason = `Whitelist-approved: ${whitelistMatch.pattern.description}`;
557
+ }
558
+ if (!whitelistApproved) {
559
+ // Escalation routing: in-process callback supersedes file-IPC.
560
+ // If neither is configured, auto-deny with a descriptive error.
561
+ const hasInProcess = typeof deps.onEscalation === 'function';
562
+ if (!hasInProcess && !deps.escalationDir) {
563
+ logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
564
+ // No escalation handler is available, so the outcome is a
565
+ // denial -- not a runtime error. Stamp the returned decision
566
+ // as `deny` so the coordinator's status classifier sees it
567
+ // correctly.
568
+ return {
569
+ content: [
570
+ {
571
+ type: 'text',
572
+ text: `${ERROR_PREFIX_ESCALATION_REQUIRED} ${evaluation.reason}. Action denied (no escalation handler).`,
573
+ },
574
+ ],
575
+ isError: true,
576
+ _policyDecision: { ...policyDecision, status: 'deny' },
577
+ };
578
+ }
579
+ const escalationId = uuidv4();
580
+ // Capture the whitelist candidates and server context while the
581
+ // coordinator's call mutex is still held. The potentially slow
582
+ // auto-approval / human wait below can then run outside that mutex.
583
+ const { patterns: candidatePatterns, ipcs: candidateIpcs } = extractWhitelistCandidates(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation, evaluation.escalatedRoles, escalationId, evaluation.reason);
584
+ const escalationContext = formatServerContext(deps.serverContextMap, toolInfo.serverName);
585
+ const waitForDecision = async () => {
586
+ // Try auto-approve before falling through to human escalation.
587
+ // Prefer the caller-provided `autoApproveUserMessage` when set;
588
+ // otherwise fall back to the file-IPC user context.
589
+ if (deps.autoApproveModel) {
590
+ const userMessage = deps.autoApproveUserMessage ??
591
+ (deps.escalationDir ? readUserContext(deps.escalationDir)?.userMessage : undefined);
592
+ if (userMessage !== undefined && userMessage !== '') {
593
+ // When using the file-IPC context, validate trust metadata
594
+ // the same way the legacy code did. Direct callers bypass
595
+ // this check (they establish trust out-of-band).
596
+ let trusted = true;
597
+ if (deps.autoApproveUserMessage === undefined && deps.escalationDir) {
598
+ const ctx = readUserContext(deps.escalationDir);
599
+ const isPtySession = process.env.IRONCURTAIN_PTY_SESSION === '1';
600
+ trusted = ctx !== null && isUserContextTrusted(ctx, isPtySession);
601
+ }
602
+ if (trusted) {
603
+ const autoResult = await autoApprove({
604
+ userMessage,
605
+ toolName: `${toolInfo.serverName}/${toolInfo.name}`,
606
+ escalationReason: evaluation.reason,
607
+ arguments: extractArgsForAutoApprove(argsForPolicy, resolvedAnnotation),
608
+ }, deps.autoApproveModel);
609
+ if (autoResult.decision === 'approve') {
610
+ return { kind: 'auto-approved', reason: autoResult.reasoning };
611
+ }
612
+ }
613
+ }
614
+ }
615
+ // One of these paths is guaranteed to be taken: we returned
616
+ // early above when both `onEscalation` and `escalationDir`
617
+ // were unset. Declare `response` with a default to satisfy
618
+ // TS's control-flow analysis without weakening runtime
619
+ // guarantees.
620
+ let response = {
621
+ decision: 'denied',
622
+ };
623
+ if (deps.onEscalation) {
624
+ // In-process path: invoke the caller's callback synchronously
625
+ // in async terms and use its return value directly.
626
+ response = await deps.onEscalation({
627
+ requestId: request.requestId,
628
+ serverName: request.serverName,
629
+ toolName: request.toolName,
630
+ arguments: argsForTransport,
631
+ timestamp: request.timestamp,
632
+ }, evaluation.reason, escalationContext, candidateIpcs.length > 0 ? candidateIpcs : undefined);
633
+ }
634
+ else if (deps.escalationDir !== undefined) {
635
+ // File-IPC path (unchanged): the session on the other side
636
+ // of the escalation directory writes the response file.
637
+ response = await waitForEscalationDecision(deps.escalationDir, {
638
+ escalationId,
639
+ serverName: request.serverName,
640
+ toolName: request.toolName,
641
+ arguments: argsForTransport,
642
+ reason: evaluation.reason,
643
+ context: escalationContext,
644
+ whitelistCandidates: candidateIpcs.length > 0 ? candidateIpcs : undefined,
645
+ });
646
+ }
647
+ return { kind: 'human', response };
648
+ };
649
+ // If `waitForDecision` throws (e.g. a faulty `onEscalation` hook),
650
+ // the error propagates out of `handleCallTool` and the coordinator's
651
+ // `runEscalationWait` finally reacquires+releases the call mutex while
652
+ // `leaveToolCall` returns the admission slot -- no leaked mutex/slot.
653
+ // We now ALSO write one audit line for the attempted call before
654
+ // re-raising, so a thrown hook no longer leaves the escalation
655
+ // unaudited. The catch fires ONLY on a throw, so it never overlaps
656
+ // the settled-decision audit below (no double-audit), and it
657
+ // re-raises the original error to preserve the exception contract
658
+ // callers depend on (the `runEscalationWait` finally + `leaveToolCall`
659
+ // still run).
660
+ let waitResult;
661
+ try {
662
+ waitResult = await (deps.runEscalationWait ?? ((wait) => wait()))(waitForDecision);
663
+ }
664
+ catch (escalationError) {
665
+ const escalationErrorText = escalationError instanceof Error ? escalationError.message : String(escalationError);
666
+ const reason = `Escalation wait failed: ${escalationErrorText}`;
667
+ // Align the outer `policyDecision` (the object `logAudit` closes
668
+ // over) with the denied outcome we are auditing, mirroring the
669
+ // settled-denied branch below.
670
+ policyDecision.status = 'deny';
671
+ policyDecision.reason = reason;
672
+ logAudit({ status: 'denied', error: reason }, 0, 'denied');
673
+ throw escalationError;
674
+ }
675
+ if (waitResult.kind === 'auto-approved') {
676
+ autoApproved = true;
677
+ escalationResult = 'approved';
678
+ policyDecision.status = 'allow';
679
+ policyDecision.reason = `Auto-approved: ${waitResult.reason}`;
680
+ }
681
+ else {
682
+ const { response } = waitResult;
683
+ if (response.decision === 'denied') {
684
+ const deniedDecision = {
685
+ status: 'deny',
686
+ rule: policyDecision.rule,
687
+ reason: 'Denied by human during escalation',
688
+ };
689
+ // Align the outer `policyDecision` (the object `logAudit`
690
+ // closes over) with the decision we return to the caller so
691
+ // the audit entry and the returned PolicyDecision agree.
692
+ policyDecision.status = deniedDecision.status;
693
+ policyDecision.reason = deniedDecision.reason;
694
+ logAudit({ status: 'denied', error: deniedDecision.reason }, 0, 'denied');
695
+ return {
696
+ content: [{ type: 'text', text: `${ERROR_PREFIX_ESCALATION_DENIED} ${evaluation.reason}` }],
697
+ isError: true,
698
+ _policyDecision: deniedDecision,
699
+ };
700
+ }
701
+ escalationResult = 'approved';
702
+ policyDecision.status = 'allow';
703
+ policyDecision.reason = 'Approved by human during escalation';
704
+ // Handle whitelist selection from the response. For in-process
705
+ // callers, the candidates are supplied directly; for file-IPC
706
+ // callers, they are looked up from the pending cache.
707
+ if (response.whitelistSelection !== undefined) {
708
+ if (response.whitelistSelection >= 0 && response.whitelistSelection < candidatePatterns.length) {
709
+ const selectedPattern = candidatePatterns[response.whitelistSelection];
710
+ deps.whitelist.add(selectedPattern);
711
+ }
712
+ }
713
+ }
714
+ }
715
+ // Expand roots for approved path arguments only (skip URLs, opaques)
716
+ const state = deps.clientStates.get(toolInfo.serverName);
717
+ if (state && annotation) {
718
+ const pathValues = extractAnnotatedPaths(argsForTransport, annotation, getPathRoles());
719
+ for (const p of pathValues) {
720
+ const result = await addRootToClient(state, {
721
+ uri: `file://${directoryForPath(p)}`,
722
+ name: 'escalation-approved',
723
+ });
724
+ if (result === 'added')
725
+ rootsExpanded = true;
726
+ }
727
+ }
728
+ }
729
+ if (evaluation.decision === 'deny') {
730
+ logAudit({ status: 'denied', error: evaluation.reason }, 0);
731
+ return {
732
+ content: [{ type: 'text', text: `${ERROR_PREFIX_DENIED} ${evaluation.reason}` }],
733
+ isError: true,
734
+ _policyDecision: { ...policyDecision },
735
+ };
736
+ }
737
+ // Circuit breaker: deny if the same tool+args is called too many times.
738
+ // Key is server-qualified so two backends that happen to expose tools
739
+ // with the same bare name do not share a rate-limit bucket.
740
+ const circuitBreakerKey = `${toolInfo.serverName}__${toolInfo.name}`;
741
+ const cbVerdict = deps.circuitBreaker.check(circuitBreakerKey, argsForTransport);
742
+ if (!cbVerdict.allowed) {
743
+ logAudit({ status: 'denied', error: cbVerdict.reason }, 0);
744
+ return {
745
+ content: [{ type: 'text', text: cbVerdict.reason }],
746
+ isError: true,
747
+ _policyDecision: { status: 'deny', rule: 'circuit-breaker', reason: cbVerdict.reason },
748
+ };
749
+ }
750
+ // Virtual proxy tools: handle locally, no backend forwarding
751
+ if (toolInfo.serverName === 'proxy' && deps.controlApiClient) {
752
+ const startTime = Date.now();
753
+ try {
754
+ const result = await handleVirtualProxyTool(toolInfo.name, argsForTransport, deps.controlApiClient);
755
+ logAudit({ status: 'success' }, Date.now() - startTime);
756
+ return {
757
+ content: [{ type: 'text', text: JSON.stringify(result) }],
758
+ _policyDecision: { ...policyDecision },
759
+ };
760
+ }
761
+ catch (err) {
762
+ const errorMessage = err instanceof Error ? err.message : String(err);
763
+ logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
764
+ return {
765
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
766
+ isError: true,
767
+ _policyDecision: { ...policyDecision },
768
+ };
769
+ }
770
+ }
771
+ // Policy allows -- forward to the real MCP server with transport args
772
+ const startTime = Date.now();
773
+ try {
774
+ const clientState = deps.clientStates.get(toolInfo.serverName);
775
+ if (!clientState) {
776
+ const err = `Internal error: no client connection for server "${toolInfo.serverName}"`;
777
+ logAudit({ status: 'denied', error: err }, 0);
778
+ return {
779
+ content: [{ type: 'text', text: err }],
780
+ isError: true,
781
+ _policyDecision: { status: 'deny', rule: 'no-client-connection', reason: err },
782
+ };
783
+ }
784
+ const client = clientState.client;
785
+ // CompatibilityCallToolResultSchema accepts the legacy `toolResult` response format
786
+ // (protocol version 2024-10-07). Output schema validation is intentionally
787
+ // bypassed by the permissiveJsonSchemaValidator injected at Client construction time.
788
+ const callToolArgs = { name: toolInfo.name, arguments: argsForTransport };
789
+ const callToolOpts = { timeout: getEscalationTimeoutMs() };
790
+ let result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
791
+ // Race condition mitigation: after roots expansion the filesystem server
792
+ // has been notified but may not have finished async-validating the new
793
+ // roots (fs.realpath, fs.stat). Retry once after a short delay.
794
+ if (rootsExpanded && isRootsRaceError(result)) {
795
+ const initialError = extractTextFromContent(result.content) ?? 'access denied (roots race)';
796
+ logAudit({ status: 'error', error: `Roots race detected, retrying: ${initialError}` }, Date.now() - startTime);
797
+ await new Promise((r) => setTimeout(r, ROOTS_RACE_RETRY_DELAY_MS));
798
+ result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
799
+ }
800
+ // Reverse-rewrite host sandbox paths to container workspace paths in results
801
+ const rewrittenContent = deps.allowedDirectory
802
+ ? rewriteResultContent(result.content, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
803
+ : result.content;
804
+ if (result.isError) {
805
+ const errorText = extractTextFromContent(rewrittenContent) ?? 'Unknown tool error';
806
+ const errorMessage = annotateSandboxViolation(errorText, serverIsSandboxed);
807
+ logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
808
+ return {
809
+ content: rewrittenContent,
810
+ isError: true,
811
+ _policyDecision: { ...policyDecision },
812
+ };
813
+ }
814
+ updateServerContext(deps.serverContextMap, toolInfo.serverName, toolInfo.name, argsForTransport);
815
+ logAudit({ status: 'success' }, Date.now() - startTime);
816
+ return { content: rewrittenContent, _policyDecision: { ...policyDecision } };
817
+ }
818
+ catch (err) {
819
+ const rawError = extractMcpErrorMessage(err);
820
+ const errorMessage = annotateSandboxViolation(rawError, serverIsSandboxed);
821
+ logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
822
+ const errorContent = [{ type: 'text', text: `Error: ${errorMessage}` }];
823
+ return {
824
+ content: deps.allowedDirectory
825
+ ? rewriteResultContent(errorContent, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
826
+ : errorContent,
827
+ isError: true,
828
+ _policyDecision: { ...policyDecision },
829
+ };
830
+ }
831
+ }
832
+ //# sourceMappingURL=tool-call-pipeline.js.map