@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248

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 (355) hide show
  1. package/README.md +14 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/call-llm.js +9 -10
  4. package/dist/activities/call-llm.js.map +1 -1
  5. package/dist/activities/classify-tool-approvals.d.ts +2 -1
  6. package/dist/activities/classify-tool-approvals.js +28 -2
  7. package/dist/activities/classify-tool-approvals.js.map +1 -1
  8. package/dist/activities/discover-mcp-server.d.ts +39 -0
  9. package/dist/activities/discover-mcp-server.js +165 -28
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/emit-event.d.ts +14 -2
  12. package/dist/activities/emit-event.js +52 -17
  13. package/dist/activities/emit-event.js.map +1 -1
  14. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  15. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  17. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  18. package/dist/activities/execute-cursor/approval-state.js +7 -1
  19. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  20. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  21. package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
  22. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  24. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  25. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  26. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  27. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  28. package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
  29. package/dist/activities/execute-cursor/error-classifier.js +115 -6
  30. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  31. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  32. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  33. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  34. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  35. package/dist/activities/execute-cursor/hook-script.js +72 -10
  36. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  37. package/dist/activities/execute-cursor/index.d.ts +51 -11
  38. package/dist/activities/execute-cursor/index.js +177 -93
  39. package/dist/activities/execute-cursor/index.js.map +1 -1
  40. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  41. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  42. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  43. package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
  44. package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
  45. package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
  46. package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
  47. package/dist/activities/execute-cursor/prompt-builder.js +49 -7
  48. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  49. package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
  50. package/dist/activities/execute-cursor/turn-recovery.js +193 -0
  51. package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
  52. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  53. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  54. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  55. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  56. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  57. package/dist/activities/execute-deep-agent/environment.js +3 -1
  58. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  59. package/dist/activities/execute-deep-agent/index.js +15 -0
  60. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  62. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  63. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  64. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  65. package/dist/activities/execute-deep-agent/setup.js +65 -24
  66. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
  68. package/dist/activities/execute-deep-agent/shell-env.js +7 -9
  69. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  70. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  71. package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
  72. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  73. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
  74. package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
  75. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  76. package/dist/activities/hydrate-workflow-execution.js +8 -3
  77. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  78. package/dist/activities/run-command.d.ts +5 -2
  79. package/dist/activities/run-command.js +22 -10
  80. package/dist/activities/run-command.js.map +1 -1
  81. package/dist/activities/run-env.d.ts +40 -0
  82. package/dist/activities/run-env.js +66 -0
  83. package/dist/activities/run-env.js.map +1 -0
  84. package/dist/activities/workflow-event-activities.d.ts +28 -10
  85. package/dist/activities/workflow-event-activities.js +87 -58
  86. package/dist/activities/workflow-event-activities.js.map +1 -1
  87. package/dist/bootstrap.js +5 -0
  88. package/dist/bootstrap.js.map +1 -1
  89. package/dist/claimcheck/payload-codec.js +21 -1
  90. package/dist/claimcheck/payload-codec.js.map +1 -1
  91. package/dist/client/stigmer-client.d.ts +83 -21
  92. package/dist/client/stigmer-client.js +106 -29
  93. package/dist/client/stigmer-client.js.map +1 -1
  94. package/dist/encryption/config.d.ts +64 -0
  95. package/dist/encryption/config.js +108 -0
  96. package/dist/encryption/config.js.map +1 -0
  97. package/dist/encryption/index.d.ts +3 -0
  98. package/dist/encryption/index.js +3 -0
  99. package/dist/encryption/index.js.map +1 -0
  100. package/dist/encryption/payload-codec.d.ts +41 -0
  101. package/dist/encryption/payload-codec.js +130 -0
  102. package/dist/encryption/payload-codec.js.map +1 -0
  103. package/dist/middleware/index.d.ts +3 -0
  104. package/dist/middleware/index.js +7 -0
  105. package/dist/middleware/index.js.map +1 -1
  106. package/dist/middleware/path-normalization.d.ts +57 -0
  107. package/dist/middleware/path-normalization.js +109 -0
  108. package/dist/middleware/path-normalization.js.map +1 -0
  109. package/dist/middleware/types.d.ts +8 -0
  110. package/dist/payload-codecs.d.ts +17 -0
  111. package/dist/payload-codecs.js +42 -0
  112. package/dist/payload-codecs.js.map +1 -0
  113. package/dist/preflight.d.ts +31 -0
  114. package/dist/preflight.js +43 -0
  115. package/dist/preflight.js.map +1 -1
  116. package/dist/runner-manager.js +21 -17
  117. package/dist/runner-manager.js.map +1 -1
  118. package/dist/runner.js +11 -18
  119. package/dist/runner.js.map +1 -1
  120. package/dist/shared/approval-policy.d.ts +9 -3
  121. package/dist/shared/approval-policy.js +15 -6
  122. package/dist/shared/approval-policy.js.map +1 -1
  123. package/dist/shared/artifact-storage.d.ts +23 -1
  124. package/dist/shared/artifact-storage.js +51 -10
  125. package/dist/shared/artifact-storage.js.map +1 -1
  126. package/dist/shared/attachment-naming.d.ts +53 -0
  127. package/dist/shared/attachment-naming.js +59 -0
  128. package/dist/shared/attachment-naming.js.map +1 -0
  129. package/dist/shared/caller-identity.d.ts +23 -2
  130. package/dist/shared/caller-identity.js +36 -5
  131. package/dist/shared/caller-identity.js.map +1 -1
  132. package/dist/shared/channel-attachment.js +1 -0
  133. package/dist/shared/channel-attachment.js.map +1 -1
  134. package/dist/shared/checkpointer/http-saver.d.ts +26 -1
  135. package/dist/shared/checkpointer/http-saver.js +54 -8
  136. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  137. package/dist/shared/conversation-attachment.js +1 -0
  138. package/dist/shared/conversation-attachment.js.map +1 -1
  139. package/dist/shared/conversation-catchup.d.ts +9 -2
  140. package/dist/shared/conversation-catchup.js +39 -6
  141. package/dist/shared/conversation-catchup.js.map +1 -1
  142. package/dist/shared/datastore-attachment.d.ts +50 -7
  143. package/dist/shared/datastore-attachment.js +93 -11
  144. package/dist/shared/datastore-attachment.js.map +1 -1
  145. package/dist/shared/grpc-retry.d.ts +5 -1
  146. package/dist/shared/grpc-retry.js +5 -1
  147. package/dist/shared/grpc-retry.js.map +1 -1
  148. package/dist/shared/http-retry.d.ts +85 -0
  149. package/dist/shared/http-retry.js +101 -0
  150. package/dist/shared/http-retry.js.map +1 -0
  151. package/dist/shared/llm-backend.d.ts +275 -0
  152. package/dist/shared/llm-backend.js +425 -0
  153. package/dist/shared/llm-backend.js.map +1 -0
  154. package/dist/shared/llm-proxy.d.ts +8 -0
  155. package/dist/shared/llm-proxy.js +15 -0
  156. package/dist/shared/llm-proxy.js.map +1 -1
  157. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  158. package/dist/shared/mcp-enabled-tools.js +86 -0
  159. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  160. package/dist/shared/mcp-manager.d.ts +11 -5
  161. package/dist/shared/mcp-manager.js +47 -8
  162. package/dist/shared/mcp-manager.js.map +1 -1
  163. package/dist/shared/mcp-resolver.d.ts +39 -2
  164. package/dist/shared/mcp-resolver.js +38 -2
  165. package/dist/shared/mcp-resolver.js.map +1 -1
  166. package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
  167. package/dist/shared/mcp-schema-sanitizer.js +197 -0
  168. package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
  169. package/dist/shared/model-client.d.ts +18 -5
  170. package/dist/shared/model-client.js +149 -18
  171. package/dist/shared/model-client.js.map +1 -1
  172. package/dist/shared/model-error.js +198 -5
  173. package/dist/shared/model-error.js.map +1 -1
  174. package/dist/shared/model-pricing-data.d.ts +2 -0
  175. package/dist/shared/model-pricing-data.js +13 -3
  176. package/dist/shared/model-pricing-data.js.map +1 -1
  177. package/dist/shared/model-registry.js +3 -2
  178. package/dist/shared/model-registry.js.map +1 -1
  179. package/dist/shared/plan-mode-permissions.d.ts +33 -0
  180. package/dist/shared/plan-mode-permissions.js +35 -0
  181. package/dist/shared/plan-mode-permissions.js.map +1 -0
  182. package/dist/shared/registry-endpoint.d.ts +20 -0
  183. package/dist/shared/registry-endpoint.js +25 -0
  184. package/dist/shared/registry-endpoint.js.map +1 -1
  185. package/dist/shared/runner-credential-keys.d.ts +24 -0
  186. package/dist/shared/runner-credential-keys.js +47 -0
  187. package/dist/shared/runner-credential-keys.js.map +1 -0
  188. package/dist/worker.d.ts +2 -1
  189. package/dist/worker.js +2 -4
  190. package/dist/worker.js.map +1 -1
  191. package/dist/workflow-engine/resolve.d.ts +19 -1
  192. package/dist/workflow-engine/resolve.js +37 -2
  193. package/dist/workflow-engine/resolve.js.map +1 -1
  194. package/dist/workflow-engine/types.d.ts +18 -0
  195. package/dist/workflow-engine/types.js.map +1 -1
  196. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  197. package/dist/workflows/call-agent-orchestrator.js +1 -0
  198. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  199. package/dist/workflows/connect-mcp-server.js +9 -0
  200. package/dist/workflows/connect-mcp-server.js.map +1 -1
  201. package/dist/workflows/engine-core.js +23 -2
  202. package/dist/workflows/engine-core.js.map +1 -1
  203. package/dist/workflows/execute-from-execution.d.ts +1 -1
  204. package/dist/workflows/execute-from-execution.js +11 -1
  205. package/dist/workflows/execute-from-execution.js.map +1 -1
  206. package/dist/workflows/types.d.ts +8 -0
  207. package/package.json +9 -4
  208. package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
  209. package/src/__test-utils__/vitest-global-setup.ts +50 -0
  210. package/src/__tests__/bootstrap.test.ts +43 -0
  211. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  212. package/src/__tests__/encryption-codec.test.ts +287 -0
  213. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  214. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  215. package/src/__tests__/preflight.test.ts +65 -2
  216. package/src/activities/__tests__/call-llm.test.ts +75 -0
  217. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  218. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  219. package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
  220. package/src/activities/__tests__/error-classifier.test.ts +94 -2
  221. package/src/activities/__tests__/run-command.test.ts +221 -0
  222. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  223. package/src/activities/call-llm.ts +9 -16
  224. package/src/activities/classify-tool-approvals.ts +34 -4
  225. package/src/activities/discover-mcp-server.ts +203 -31
  226. package/src/activities/emit-event.ts +65 -21
  227. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  228. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  229. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
  230. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
  231. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  232. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  233. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  234. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  235. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
  236. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  237. package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
  238. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  239. package/src/activities/execute-cursor/approval-state.ts +30 -1
  240. package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
  241. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  242. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  243. package/src/activities/execute-cursor/error-classifier.ts +131 -7
  244. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  245. package/src/activities/execute-cursor/hook-script.ts +74 -10
  246. package/src/activities/execute-cursor/index.ts +209 -109
  247. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  248. package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
  249. package/src/activities/execute-cursor/prompt-builder.ts +85 -9
  250. package/src/activities/execute-cursor/turn-recovery.ts +208 -0
  251. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  252. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
  253. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  254. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  255. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  256. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  257. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
  258. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  259. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  260. package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
  261. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
  262. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  263. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  264. package/src/activities/execute-deep-agent/environment.ts +3 -1
  265. package/src/activities/execute-deep-agent/index.ts +20 -0
  266. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  267. package/src/activities/execute-deep-agent/setup.ts +85 -29
  268. package/src/activities/execute-deep-agent/shell-env.ts +8 -9
  269. package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
  270. package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
  271. package/src/activities/hydrate-workflow-execution.ts +8 -3
  272. package/src/activities/run-command.ts +25 -12
  273. package/src/activities/run-env.ts +79 -0
  274. package/src/activities/workflow-event-activities.ts +96 -69
  275. package/src/bootstrap.ts +5 -0
  276. package/src/claimcheck/payload-codec.ts +33 -1
  277. package/src/client/__tests__/stigmer-client.test.ts +110 -11
  278. package/src/client/stigmer-client.ts +150 -37
  279. package/src/encryption/config.ts +155 -0
  280. package/src/encryption/index.ts +3 -0
  281. package/src/encryption/payload-codec.ts +152 -0
  282. package/src/middleware/__tests__/path-normalization.test.ts +140 -0
  283. package/src/middleware/index.ts +8 -0
  284. package/src/middleware/path-normalization.ts +125 -0
  285. package/src/middleware/types.ts +11 -0
  286. package/src/payload-codecs.ts +62 -0
  287. package/src/preflight.ts +45 -0
  288. package/src/runner-manager.ts +26 -26
  289. package/src/runner.ts +12 -27
  290. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  291. package/src/shared/__tests__/artifact-storage.test.ts +145 -2
  292. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  293. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  294. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  295. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  296. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  297. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  298. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  299. package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
  300. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  301. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  302. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  303. package/src/shared/__tests__/http-retry.test.ts +68 -0
  304. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  305. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  306. package/src/shared/__tests__/mcp-manager.test.ts +137 -15
  307. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  308. package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
  309. package/src/shared/__tests__/model-client.test.ts +200 -0
  310. package/src/shared/__tests__/model-error.test.ts +289 -1
  311. package/src/shared/__tests__/model-pricing.test.ts +62 -2
  312. package/src/shared/__tests__/model-registry.test.ts +22 -0
  313. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  314. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  315. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  316. package/src/shared/approval-policy.ts +14 -7
  317. package/src/shared/artifact-storage.ts +75 -10
  318. package/src/shared/attachment-naming.ts +78 -0
  319. package/src/shared/caller-identity.ts +40 -5
  320. package/src/shared/channel-attachment.ts +1 -0
  321. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  322. package/src/shared/checkpointer/http-saver.ts +71 -8
  323. package/src/shared/conversation-attachment.ts +1 -0
  324. package/src/shared/conversation-catchup.ts +39 -6
  325. package/src/shared/datastore-attachment.ts +106 -11
  326. package/src/shared/grpc-retry.ts +5 -1
  327. package/src/shared/http-retry.ts +139 -0
  328. package/src/shared/llm-backend.ts +544 -0
  329. package/src/shared/llm-proxy.ts +15 -0
  330. package/src/shared/mcp-enabled-tools.ts +105 -0
  331. package/src/shared/mcp-manager.ts +56 -8
  332. package/src/shared/mcp-resolver.ts +73 -2
  333. package/src/shared/mcp-schema-sanitizer.ts +224 -0
  334. package/src/shared/model-client.ts +179 -19
  335. package/src/shared/model-error.ts +222 -4
  336. package/src/shared/model-pricing-data.ts +22 -3
  337. package/src/shared/model-registry.ts +7 -2
  338. package/src/shared/plan-mode-permissions.ts +37 -0
  339. package/src/shared/registry-endpoint.ts +27 -0
  340. package/src/shared/runner-credential-keys.ts +46 -0
  341. package/src/worker.ts +4 -5
  342. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
  343. package/src/workflow-engine/resolve.ts +48 -2
  344. package/src/workflow-engine/types.ts +18 -0
  345. package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
  346. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  347. package/src/workflows/call-agent-orchestrator.ts +10 -0
  348. package/src/workflows/connect-mcp-server.ts +9 -0
  349. package/src/workflows/engine-core.ts +23 -2
  350. package/src/workflows/execute-from-execution.ts +12 -2
  351. package/src/workflows/types.ts +8 -0
  352. package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
  353. package/dist/activities/execute-cursor/mcp-config.js +0 -39
  354. package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
  355. package/src/activities/execute-cursor/mcp-config.ts +0 -66
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ effectiveEnabledTools,
4
+ filterToolsByEnabledTools,
5
+ enabledToolsBySlug,
6
+ } from "../mcp-enabled-tools.js";
7
+
8
+ describe("effectiveEnabledTools", () => {
9
+ it("uses the usage's non-empty enabled_tools verbatim", () => {
10
+ expect(effectiveEnabledTools(["a", "b"], ["c"])).toEqual(["a", "b"]);
11
+ });
12
+
13
+ it("falls back to default_enabled_tools when the usage list is empty", () => {
14
+ expect(effectiveEnabledTools([], ["c", "d"])).toEqual(["c", "d"]);
15
+ });
16
+
17
+ it("falls back to default_enabled_tools when the usage list is absent", () => {
18
+ expect(effectiveEnabledTools(undefined, ["c"])).toEqual(["c"]);
19
+ });
20
+
21
+ it("returns undefined (unrestricted) when both lists are empty or absent", () => {
22
+ expect(effectiveEnabledTools([], [])).toBeUndefined();
23
+ expect(effectiveEnabledTools(undefined, undefined)).toBeUndefined();
24
+ expect(effectiveEnabledTools([], undefined)).toBeUndefined();
25
+ });
26
+
27
+ it("returns copies, never the caller's arrays", () => {
28
+ const usage = ["a"];
29
+ const result = effectiveEnabledTools(usage, undefined)!;
30
+ result.push("b");
31
+ expect(usage).toEqual(["a"]);
32
+ });
33
+ });
34
+
35
+ describe("filterToolsByEnabledTools", () => {
36
+ const tool = (name: string) => ({ name });
37
+
38
+ beforeEach(() => {
39
+ vi.restoreAllMocks();
40
+ vi.spyOn(console, "log").mockImplementation(() => {});
41
+ vi.spyOn(console, "warn").mockImplementation(() => {});
42
+ });
43
+
44
+ it("passes every tool through untouched when unrestricted (undefined)", () => {
45
+ const tools = [tool("a"), tool("b")];
46
+ expect(filterToolsByEnabledTools("srv", tools, undefined)).toEqual(tools);
47
+ });
48
+
49
+ it("keeps only the tools on the allow-list (exact bare-name match)", () => {
50
+ const tools = [tool("read"), tool("write"), tool("delete")];
51
+ expect(
52
+ filterToolsByEnabledTools("srv", tools, ["read"]).map((t) => t.name),
53
+ ).toEqual(["read"]);
54
+ });
55
+
56
+ it("warns and intersects when an enabled name is not among the discovered tools", () => {
57
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
58
+ const tools = [tool("read"), tool("write")];
59
+
60
+ const filtered = filterToolsByEnabledTools("srv", tools, ["read", "reed"]);
61
+
62
+ // The restriction still holds (intersection), the run is not failed,
63
+ // and the operator gets a loud pointer at the mismatched name.
64
+ expect(filtered.map((t) => t.name)).toEqual(["read"]);
65
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("reed"));
66
+ });
67
+
68
+ it("yields an empty toolset when no enabled name matches (restriction over availability)", () => {
69
+ const tools = [tool("read")];
70
+ expect(filterToolsByEnabledTools("srv", tools, ["ghost"])).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe("enabledToolsBySlug", () => {
75
+ it("collects only restricted servers, keyed by slug", () => {
76
+ const map = enabledToolsBySlug([
77
+ { slug: "restricted", enabledTools: ["a"] },
78
+ { slug: "open" },
79
+ ]);
80
+ expect(map).toEqual({ restricted: ["a"] });
81
+ });
82
+
83
+ it("returns an empty object when nothing is restricted (the common case)", () => {
84
+ expect(enabledToolsBySlug([{ slug: "open" }])).toEqual({});
85
+ });
86
+ });
@@ -1,14 +1,32 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
2
2
  import type { Connection } from "@langchain/mcp-adapters";
3
- import { toMcpClientConfig } from "../mcp-manager.js";
3
+ import { connectMcpServers, toMcpClientConfig } from "../mcp-manager.js";
4
+ import { RUNNER_CREDENTIAL_ENV_KEYS } from "../runner-credential-keys.js";
4
5
  import type { ResolvedMcpServer } from "../mcp-resolver.js";
5
6
 
7
+ // The adapter client is mocked ONLY for the connectMcpServers tests below
8
+ // (initializeConnections returns the canned per-server tool map); the
9
+ // toMcpClientConfig suites are pure and never construct it.
10
+ const mockMcpClient = vi.hoisted(() => ({
11
+ toolMap: {} as Record<string, { name: string }[]>,
12
+ }));
13
+
14
+ vi.mock("@langchain/mcp-adapters", () => ({
15
+ MultiServerMCPClient: class {
16
+ async initializeConnections() {
17
+ return mockMcpClient.toolMap;
18
+ }
19
+ async close() {}
20
+ },
21
+ }));
22
+
6
23
  function makeServer(overrides: Partial<ResolvedMcpServer>): ResolvedMcpServer {
7
24
  return {
8
25
  slug: "test-server",
9
26
  connectionType: "stdio",
10
27
  toolApprovals: [],
11
28
  pinnedToolApprovals: [],
29
+ toolApprovalOverrides: [],
12
30
  discoveredCapabilitiesEmpty: false,
13
31
  ...overrides,
14
32
  };
@@ -95,25 +113,15 @@ describe("toMcpClientConfig", () => {
95
113
  });
96
114
  });
97
115
 
98
- // Runner-internal credentials that must never reach an MCP stdio subprocess
99
- // (oss#256). Names mirror what the runner actually reads: config.ts,
100
- // fingerprint-secret.ts, model-client.ts, registry-endpoint.ts.
101
- const RUNNER_CREDENTIAL_KEYS = [
102
- "STIGMER_RUNNER_HITL_SECRET",
103
- "STIGMER_TOKEN",
104
- "STIGMER_AUTH_TOKEN",
105
- "CURSOR_API_KEY",
106
- "ANTHROPIC_API_KEY",
107
- "OPENAI_API_KEY",
108
- ] as const;
109
-
110
116
  describe("toMcpClientConfig — stdio env isolation (oss#256)", () => {
111
117
  afterEach(() => {
112
118
  vi.unstubAllEnvs();
113
119
  });
114
120
 
115
121
  it("gives a stdio server with no declared env none of the runner's variables", () => {
116
- for (const key of RUNNER_CREDENTIAL_KEYS) {
122
+ // The canaries are the runner's real credential names (oss#385's single
123
+ // source of truth), so a key added there is automatically covered here.
124
+ for (const key of RUNNER_CREDENTIAL_ENV_KEYS) {
117
125
  vi.stubEnv(key, `leaked-${key}`);
118
126
  }
119
127
  vi.stubEnv("STIGMER_TEST_LEAK_SENTINEL", "canary");
@@ -167,3 +175,117 @@ describe("toMcpClientConfig — stdio env isolation (oss#256)", () => {
167
175
  expect(stdioEnv(config, "empty")).toEqual({});
168
176
  });
169
177
  });
178
+
179
+ describe("connectMcpServers — enabled_tools enforcement (issue #350)", () => {
180
+ const tool = (name: string) => ({ name }) as any;
181
+
182
+ beforeEach(() => {
183
+ vi.spyOn(console, "log").mockImplementation(() => {});
184
+ vi.spyOn(console, "warn").mockImplementation(() => {});
185
+ mockMcpClient.toolMap = {};
186
+ });
187
+
188
+ afterEach(() => {
189
+ vi.restoreAllMocks();
190
+ });
191
+
192
+ it("filters a restricted server's discovered tools down to the allow-list, in BOTH the flat list and the per-server map", async () => {
193
+ mockMcpClient.toolMap = { planton: [tool("get"), tool("apply"), tool("destroy")] };
194
+
195
+ const result = await connectMcpServers([
196
+ makeServer({ slug: "planton", command: "npx", enabledTools: ["get"] }),
197
+ ]);
198
+
199
+ // Both result shapes must narrow together: `tools` feeds the model and
200
+ // the built-in general-purpose sub-agent, `serverToolMap` feeds the
201
+ // approval gate's toolServerMap and the sub-agent McpAccess subset check.
202
+ expect(result.tools.map((t) => t.name)).toEqual(["get"]);
203
+ expect(result.serverToolMap.planton.map((t) => t.name)).toEqual(["get"]);
204
+ });
205
+
206
+ it("leaves an unrestricted server (enabledTools absent — e.g. a synthesized attachment) untouched", async () => {
207
+ mockMcpClient.toolMap = { open: [tool("a"), tool("b")] };
208
+
209
+ const result = await connectMcpServers([
210
+ makeServer({ slug: "open", command: "npx" }),
211
+ ]);
212
+
213
+ expect(result.tools.map((t) => t.name)).toEqual(["a", "b"]);
214
+ });
215
+
216
+ it("restricts per server: one restricted server never narrows its unrestricted sibling", async () => {
217
+ mockMcpClient.toolMap = {
218
+ restricted: [tool("get"), tool("apply")],
219
+ open: [tool("get"), tool("apply")],
220
+ };
221
+
222
+ const result = await connectMcpServers([
223
+ makeServer({ slug: "restricted", command: "npx", enabledTools: ["get"] }),
224
+ makeServer({ slug: "open", command: "npx" }),
225
+ ]);
226
+
227
+ expect(result.serverToolMap.restricted.map((t) => t.name)).toEqual(["get"]);
228
+ expect(result.serverToolMap.open.map((t) => t.name)).toEqual(["get", "apply"]);
229
+ });
230
+
231
+ it("warns and intersects an allow-list naming a tool the server does not expose", async () => {
232
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
233
+ mockMcpClient.toolMap = { planton: [tool("get")] };
234
+
235
+ const result = await connectMcpServers([
236
+ makeServer({ slug: "planton", command: "npx", enabledTools: ["get", "ghost"] }),
237
+ ]);
238
+
239
+ expect(result.tools.map((t) => t.name)).toEqual(["get"]);
240
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("ghost"));
241
+ });
242
+ });
243
+
244
+ describe("connectMcpServers — schema sanitization (issue #420)", () => {
245
+ beforeEach(() => {
246
+ vi.spyOn(console, "log").mockImplementation(() => {});
247
+ vi.spyOn(console, "warn").mockImplementation(() => {});
248
+ mockMcpClient.toolMap = {};
249
+ });
250
+
251
+ afterEach(() => {
252
+ vi.restoreAllMocks();
253
+ });
254
+
255
+ it("drops unicode-invalid regex patterns from every discovered tool's schema and warns per tool", async () => {
256
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
257
+ const toxic = {
258
+ name: "toxic",
259
+ schema: {
260
+ type: "object",
261
+ // PayPal's issue-#420 shape: legal plain regex, invalid under /u.
262
+ properties: { url: { type: "string", pattern: "^https\\:\\/\\/" } },
263
+ },
264
+ } as any;
265
+ const clean = {
266
+ name: "clean",
267
+ schema: {
268
+ type: "object",
269
+ properties: { id: { type: "string", pattern: "^[a-z]+$" } },
270
+ },
271
+ } as any;
272
+ mockMcpClient.toolMap = { paypal: [toxic, clean] };
273
+
274
+ const result = await connectMcpServers([
275
+ makeServer({ slug: "paypal", connectionType: "http", url: "https://mcp.example.com/mcp" }),
276
+ ]);
277
+
278
+ // Sanitization happens on the discovered tool objects themselves, so
279
+ // every downstream consumer (model binding, approval gate, sub-agent
280
+ // filter) sees the sanitized schema.
281
+ expect(result.serverToolMap.paypal[0].schema).toEqual({
282
+ type: "object",
283
+ properties: { url: { type: "string" } },
284
+ });
285
+ expect(result.serverToolMap.paypal[1].schema).toEqual(clean.schema);
286
+ expect(warn).toHaveBeenCalledTimes(1);
287
+ expect(warn).toHaveBeenCalledWith(
288
+ expect.stringContaining("Server 'paypal' tool 'toxic'"),
289
+ );
290
+ });
291
+ });
@@ -1,11 +1,17 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { resolveMcpServers } from "../mcp-resolver.js";
2
+ import { mcpServerToResolved, mergeMcpServerUsages, resolveMcpServers } from "../mcp-resolver.js";
3
3
  import { McpTransportError } from "../mcp-transport-guard.js";
4
4
 
5
- function makeUsage(slug: string, org = "test-org") {
5
+ function makeUsage(
6
+ slug: string,
7
+ org = "test-org",
8
+ enabledTools: string[] = [],
9
+ toolApprovalOverrides: Array<{ toolName: string; requiresApproval: boolean }> = [],
10
+ ) {
6
11
  return {
7
12
  mcpServerRef: { slug, org, kind: 0 },
8
- toolApprovalOverrides: [],
13
+ enabledTools,
14
+ toolApprovalOverrides,
9
15
  } as any;
10
16
  }
11
17
 
@@ -106,3 +112,140 @@ describe("resolveMcpServers — transport guard integration", () => {
106
112
  expect(result.resolvedServers).toHaveLength(0);
107
113
  });
108
114
  });
115
+
116
+ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
117
+ function withDefaults(server: any, defaultEnabledTools: string[]) {
118
+ server.spec.defaultEnabledTools = defaultEnabledTools;
119
+ return server;
120
+ }
121
+
122
+ it("carries the usage's enabled_tools as the effective allow-list", async () => {
123
+ const client = clientReturning({ github: httpMcpServer("github") });
124
+
125
+ const result = await resolveMcpServers(
126
+ client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed",
127
+ );
128
+
129
+ expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
130
+ });
131
+
132
+ it("falls back to the server's default_enabled_tools for an empty usage list", async () => {
133
+ const client = clientReturning({
134
+ github: withDefaults(httpMcpServer("github"), ["search_code"]),
135
+ });
136
+
137
+ const result = await resolveMcpServers(
138
+ client, [makeUsage("github")], {}, "stdio-allowed",
139
+ );
140
+
141
+ expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
142
+ });
143
+
144
+ it("resolves unrestricted (absent field) when both lists are empty", async () => {
145
+ const client = clientReturning({ github: httpMcpServer("github") });
146
+
147
+ const result = await resolveMcpServers(
148
+ client, [makeUsage("github")], {}, "stdio-allowed",
149
+ );
150
+
151
+ expect(result.resolvedServers[0].enabledTools).toBeUndefined();
152
+ });
153
+
154
+ it("mcpServerToResolved without a usage keeps only the default fallback (the discovery path)", () => {
155
+ const server = withDefaults(httpMcpServer("github"), ["search_code"]);
156
+
157
+ const resolved = mcpServerToResolved(server, "github", {});
158
+
159
+ // No usage in hand (discovery) — a per-agent restriction cannot apply,
160
+ // only the server-declared default does.
161
+ expect(resolved?.enabledTools).toEqual(["search_code"]);
162
+ });
163
+ });
164
+
165
+ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)", () => {
166
+ it("carries the usage's overrides on its own resolved server only", async () => {
167
+ // Riding the server is the scoping mechanism: an override can no longer
168
+ // reach a same-named tool on another server, because it never exists
169
+ // anywhere but its own server's object.
170
+ const client = clientReturning({
171
+ github: httpMcpServer("github"),
172
+ slack: httpMcpServer("slack"),
173
+ });
174
+
175
+ const result = await resolveMcpServers(
176
+ client,
177
+ [
178
+ makeUsage("github", "test-org", [], [{ toolName: "delete_item", requiresApproval: false }]),
179
+ makeUsage("slack"),
180
+ ],
181
+ {},
182
+ "stdio-allowed",
183
+ );
184
+
185
+ const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
186
+ expect(bySlug.get("github")!.toolApprovalOverrides).toEqual([
187
+ { toolName: "delete_item", requiresApproval: false },
188
+ ]);
189
+ expect(bySlug.get("slack")!.toolApprovalOverrides).toEqual([]);
190
+ });
191
+
192
+ it("mcpServerToResolved without a usage carries no overrides (the discovery path)", () => {
193
+ const resolved = mcpServerToResolved(httpMcpServer("github"), "github", {});
194
+
195
+ // No usage in hand (discovery) — there is no agent context, so no
196
+ // layer-3 overrides can exist.
197
+ expect(resolved?.toolApprovalOverrides).toEqual([]);
198
+ });
199
+
200
+ it("session-wins merge feeds the SESSION usage's overrides to the resolver (native-harness parity)", async () => {
201
+ // Pins the end-to-end path that was broken before #349: the deep-agent
202
+ // harness resolved servers from the merged usages but flattened
203
+ // overrides from the AGENT usages only, so a session's overrides were
204
+ // silently ignored there (and honored by the Cursor harness).
205
+ const client = clientReturning({ github: httpMcpServer("github") });
206
+ const merged = mergeMcpServerUsages(
207
+ [makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: true }])],
208
+ [makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: false }])],
209
+ );
210
+
211
+ const result = await resolveMcpServers(client, merged, {}, "stdio-allowed");
212
+
213
+ expect(result.resolvedServers[0].toolApprovalOverrides).toEqual([
214
+ { toolName: "push", requiresApproval: false },
215
+ ]);
216
+ });
217
+ });
218
+
219
+ describe("mergeMcpServerUsages — session-wins-per-slug (shared by both harnesses)", () => {
220
+ it("session usage replaces the agent's WHOLE usage for the same slug, including enabled_tools", () => {
221
+ const merged = mergeMcpServerUsages(
222
+ [makeUsage("github", "test-org", ["create_pr"])],
223
+ [makeUsage("github", "test-org", ["search_code"])],
224
+ );
225
+
226
+ expect(merged).toHaveLength(1);
227
+ expect(merged[0].enabledTools).toEqual(["search_code"]);
228
+ });
229
+
230
+ it("a session usage with an empty list overrides to the server-default/unrestricted shape", () => {
231
+ // Session-wins is whole-usage precedence, not a list union: an empty
232
+ // session list means "back to the server's defaults", exactly as if the
233
+ // agent-level usage did not exist.
234
+ const merged = mergeMcpServerUsages(
235
+ [makeUsage("github", "test-org", ["create_pr"])],
236
+ [makeUsage("github")],
237
+ );
238
+
239
+ expect(merged).toHaveLength(1);
240
+ expect(merged[0].enabledTools).toEqual([]);
241
+ });
242
+
243
+ it("unions distinct slugs and skips usages without one", () => {
244
+ const merged = mergeMcpServerUsages(
245
+ [makeUsage("github"), { toolApprovalOverrides: [] } as any],
246
+ [makeUsage("planton")],
247
+ );
248
+
249
+ expect(merged.map((u) => u.mcpServerRef?.slug)).toEqual(["github", "planton"]);
250
+ });
251
+ });
@@ -0,0 +1,267 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { loadMcpTools } from "@langchain/mcp-adapters";
3
+ import { DynamicStructuredTool } from "@langchain/core/tools";
4
+ import { sanitizeSchemaPatterns } from "../mcp-schema-sanitizer.js";
5
+
6
+ // PayPal's real-world specimen from issue #420: legal as a plain ECMAScript
7
+ // regex (Annex B identity escapes), invalid under the /u flag.
8
+ const TOXIC_PATTERN = "^https\\:\\/\\/";
9
+
10
+ describe("sanitizeSchemaPatterns — walker semantics", () => {
11
+ it("drops a unicode-invalid pattern and reports it", () => {
12
+ const schema = {
13
+ type: "object",
14
+ properties: {
15
+ url: { type: "string", pattern: TOXIC_PATTERN },
16
+ },
17
+ };
18
+
19
+ const dropped = sanitizeSchemaPatterns(schema);
20
+
21
+ expect(schema.properties.url).toEqual({ type: "string" });
22
+ expect(dropped).toEqual([
23
+ {
24
+ location: "/properties/url/pattern",
25
+ pattern: TOXIC_PATTERN,
26
+ compilesWithoutUnicodeFlag: true,
27
+ },
28
+ ]);
29
+ });
30
+
31
+ it("flags patterns that are broken outright (invalid without /u too)", () => {
32
+ const schema = {
33
+ type: "object",
34
+ properties: { name: { type: "string", pattern: "(unclosed" } },
35
+ };
36
+
37
+ const dropped = sanitizeSchemaPatterns(schema);
38
+
39
+ expect(schema.properties.name).toEqual({ type: "string" });
40
+ expect(dropped[0].compilesWithoutUnicodeFlag).toBe(false);
41
+ });
42
+
43
+ it("leaves clean schemas byte-identical", () => {
44
+ const schema = {
45
+ type: "object",
46
+ properties: {
47
+ url: { type: "string", pattern: "^https://" },
48
+ tags: { type: "array", items: { type: "string", pattern: "^[a-z]+$" } },
49
+ },
50
+ patternProperties: { "^x-": { type: "string" } },
51
+ additionalProperties: false,
52
+ required: ["url"],
53
+ };
54
+ const before = JSON.parse(JSON.stringify(schema));
55
+
56
+ expect(sanitizeSchemaPatterns(schema)).toEqual([]);
57
+ expect(schema).toEqual(before);
58
+ });
59
+
60
+ it("never touches a property literally named 'pattern'", () => {
61
+ // 'pattern' as a property NAME is data, not a keyword — but the pattern
62
+ // KEYWORD on that property's own schema is still sanitized.
63
+ const schema = {
64
+ type: "object",
65
+ properties: {
66
+ pattern: { type: "string", pattern: TOXIC_PATTERN },
67
+ },
68
+ };
69
+
70
+ const dropped = sanitizeSchemaPatterns(schema);
71
+
72
+ expect(Object.keys(schema.properties)).toEqual(["pattern"]);
73
+ expect(schema.properties.pattern).toEqual({ type: "string" });
74
+ expect(dropped[0].location).toBe("/properties/pattern/pattern");
75
+ });
76
+
77
+ it("leaves a non-string pattern value alone", () => {
78
+ const schema = { type: "object", properties: { n: { pattern: 123 } } };
79
+ expect(sanitizeSchemaPatterns(schema)).toEqual([]);
80
+ expect((schema.properties.n as { pattern: unknown }).pattern).toBe(123);
81
+ });
82
+
83
+ it("reaches nested schema positions", () => {
84
+ const schema = {
85
+ type: "object",
86
+ properties: {
87
+ choice: {
88
+ anyOf: [
89
+ { type: "string", pattern: TOXIC_PATTERN },
90
+ { type: "number" },
91
+ ],
92
+ },
93
+ tuple: {
94
+ type: "array",
95
+ items: [{ type: "string", pattern: TOXIC_PATTERN }],
96
+ },
97
+ names: { propertyNames: { pattern: TOXIC_PATTERN } },
98
+ },
99
+ $defs: {
100
+ link: { type: "string", pattern: TOXIC_PATTERN },
101
+ },
102
+ dependencies: {
103
+ // Array form is dependentRequired (names, not schemas) — untouched.
104
+ a: ["b"],
105
+ c: { properties: { d: { type: "string", pattern: TOXIC_PATTERN } } },
106
+ },
107
+ };
108
+
109
+ const dropped = sanitizeSchemaPatterns(schema);
110
+
111
+ expect(dropped.map((d) => d.location).sort()).toEqual([
112
+ "/$defs/link/pattern",
113
+ "/dependencies/c/properties/d/pattern",
114
+ "/properties/choice/anyOf/0/pattern",
115
+ "/properties/names/propertyNames/pattern",
116
+ "/properties/tuple/items/0/pattern",
117
+ ]);
118
+ expect(schema.dependencies.a).toEqual(["b"]);
119
+ });
120
+
121
+ it("terminates on a cyclic schema graph", () => {
122
+ const node: Record<string, unknown> = {
123
+ type: "object",
124
+ properties: { url: { type: "string", pattern: TOXIC_PATTERN } },
125
+ };
126
+ node.not = node;
127
+
128
+ const dropped = sanitizeSchemaPatterns(node);
129
+ expect(dropped).toHaveLength(1);
130
+ });
131
+
132
+ it("ignores non-object input", () => {
133
+ expect(sanitizeSchemaPatterns(undefined)).toEqual([]);
134
+ expect(sanitizeSchemaPatterns("pattern")).toEqual([]);
135
+ expect(sanitizeSchemaPatterns([{ pattern: TOXIC_PATTERN }])).toEqual([]);
136
+ });
137
+ });
138
+
139
+ describe("sanitizeSchemaPatterns — never-tighten invariant (patternProperties)", () => {
140
+ // Slash-free /u-invalid key so the expected report location needs no
141
+ // JSON-pointer escaping (the walker escapes `/` in map keys as `~1`).
142
+ const TOXIC_KEY = "^x\\:";
143
+
144
+ it("drops a broken patternProperties key and relaxes restrictive additionalProperties", () => {
145
+ const schema = {
146
+ type: "object",
147
+ patternProperties: {
148
+ [TOXIC_KEY]: { type: "string" },
149
+ "^clean-": { type: "number" },
150
+ },
151
+ additionalProperties: false,
152
+ };
153
+
154
+ const dropped = sanitizeSchemaPatterns(schema);
155
+
156
+ expect(dropped).toEqual([
157
+ {
158
+ location: `/patternProperties/${TOXIC_KEY}`,
159
+ pattern: TOXIC_KEY,
160
+ compilesWithoutUnicodeFlag: true,
161
+ },
162
+ ]);
163
+ expect(schema.patternProperties).toEqual({ "^clean-": { type: "number" } });
164
+ // Keys the dropped pattern used to match would fall through to
165
+ // additionalProperties: false and be falsely REJECTED — the relaxation
166
+ // is what keeps sanitization strictly loosening.
167
+ expect("additionalProperties" in schema).toBe(false);
168
+ });
169
+
170
+ it("removes an emptied patternProperties and relaxes a restrictive subschema additionalProperties", () => {
171
+ const schema = {
172
+ type: "object",
173
+ patternProperties: { [TOXIC_KEY]: { type: "string" } },
174
+ additionalProperties: { type: "number" },
175
+ unevaluatedProperties: false,
176
+ };
177
+
178
+ sanitizeSchemaPatterns(schema);
179
+
180
+ expect("patternProperties" in schema).toBe(false);
181
+ expect("additionalProperties" in schema).toBe(false);
182
+ expect("unevaluatedProperties" in schema).toBe(false);
183
+ });
184
+
185
+ it("keeps additionalProperties when no patternProperties entry was dropped", () => {
186
+ const schema = {
187
+ type: "object",
188
+ patternProperties: { "^clean-": { type: "string" } },
189
+ additionalProperties: false,
190
+ };
191
+
192
+ sanitizeSchemaPatterns(schema);
193
+
194
+ expect(schema.additionalProperties).toBe(false);
195
+ });
196
+
197
+ it("proves the loosened schema through the real validator: previously matched keys stay accepted", async () => {
198
+ // End-to-end through @langchain/core's own validation path (the exact
199
+ // consumer that compiles patterns with /u): after sanitization, an input
200
+ // whose key only the dropped pattern used to match must validate — not
201
+ // fall through to additionalProperties: false.
202
+ const schema = {
203
+ type: "object" as const,
204
+ patternProperties: { "^x\\:": { type: "string" as const } },
205
+ additionalProperties: false,
206
+ };
207
+ sanitizeSchemaPatterns(schema);
208
+
209
+ // Default generics: the JSON-schema constructor arm types invoke input
210
+ // as ToolCall otherwise, rejecting the pattern-shaped key at compile time.
211
+ const tool: DynamicStructuredTool = new DynamicStructuredTool({
212
+ name: "probe",
213
+ description: "d",
214
+ schema,
215
+ func: async () => "ok",
216
+ });
217
+
218
+ await expect(tool.invoke({ "x:key": "value" })).resolves.toBe("ok");
219
+ });
220
+ });
221
+
222
+ describe("sanitizeSchemaPatterns — issue #420 regression pin", () => {
223
+ // The issue's exact repro: a tool loaded through the real adapter with the
224
+ // PayPal pattern shape loads fine but fails EVERY invocation with
225
+ // "SyntaxError: Invalid regular expression: /^https\:\/\//u".
226
+ const toxicTool = {
227
+ name: "toxic",
228
+ description: "d",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: { url: { type: "string", pattern: TOXIC_PATTERN } },
232
+ },
233
+ };
234
+ const fakeClient = {
235
+ listTools: async () => ({ tools: [toxicTool] }),
236
+ callTool: async () => ({ content: [{ type: "text", text: "called" }] }),
237
+ };
238
+
239
+ it("negative control: without sanitization the tool is uncallable (the defect)", async () => {
240
+ const tools = await loadMcpTools("srv", fakeClient as never, {
241
+ throwOnLoadError: true,
242
+ });
243
+
244
+ await expect(tools[0].invoke({ url: "https://x.com" })).rejects.toThrow(
245
+ /Invalid regular expression/,
246
+ );
247
+ });
248
+
249
+ it("after sanitization the same tool invokes successfully", async () => {
250
+ const tools = await loadMcpTools("srv", fakeClient as never, {
251
+ throwOnLoadError: true,
252
+ });
253
+
254
+ const dropped = sanitizeSchemaPatterns(tools[0].schema);
255
+
256
+ expect(dropped).toEqual([
257
+ {
258
+ location: "/properties/url/pattern",
259
+ pattern: TOXIC_PATTERN,
260
+ compilesWithoutUnicodeFlag: true,
261
+ },
262
+ ]);
263
+ await expect(tools[0].invoke({ url: "https://x.com" })).resolves.toBe(
264
+ "called",
265
+ );
266
+ });
267
+ });