@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.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 (328) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
  74. package/dist/activities/execute-deep-agent/hitl.js +5 -1
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +370 -56
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +6 -2
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
  243. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  244. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  245. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  246. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
  247. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  248. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  249. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  251. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  252. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  253. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  254. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  255. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  256. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  257. package/src/activities/execute-deep-agent/hitl.ts +15 -1
  258. package/src/activities/execute-deep-agent/index.ts +434 -64
  259. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  260. package/src/activities/execute-deep-agent/setup.ts +223 -125
  261. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  262. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  263. package/src/activities/execute-deep-agent/status-builder.ts +19 -7
  264. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  265. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  266. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  267. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  268. package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
  269. package/src/claimcheck/payload-codec.ts +8 -8
  270. package/src/client/stigmer-client.ts +9 -1
  271. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  272. package/src/middleware/approval-gate.ts +247 -38
  273. package/src/middleware/types.ts +5 -5
  274. package/src/shared/__tests__/activity-input.test.ts +78 -0
  275. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  276. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  277. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  278. package/src/shared/__tests__/args-preview.test.ts +78 -0
  279. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  280. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  281. package/src/shared/__tests__/file-change.test.ts +85 -0
  282. package/src/shared/__tests__/file-tools.test.ts +90 -0
  283. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  284. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  285. package/src/shared/__tests__/model-client.test.ts +162 -0
  286. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  287. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  288. package/src/shared/__tests__/status-offload.test.ts +573 -16
  289. package/src/shared/__tests__/status.test.ts +4 -5
  290. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  291. package/src/shared/__tests__/tool-row.test.ts +221 -0
  292. package/src/shared/activity-input.ts +57 -0
  293. package/src/shared/approval-canonicalize.ts +159 -0
  294. package/src/shared/approval-fingerprint.ts +148 -0
  295. package/src/shared/approval-policy.ts +303 -27
  296. package/src/shared/args-preview.ts +98 -0
  297. package/src/shared/artifact-storage.ts +62 -11
  298. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  299. package/src/shared/file-change.ts +64 -0
  300. package/src/shared/file-tools.ts +169 -0
  301. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  302. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  303. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  304. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  305. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  306. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  307. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  308. package/src/shared/filereview/capture.ts +727 -0
  309. package/src/shared/filereview/cas-substrate.ts +401 -0
  310. package/src/shared/filereview/digest.ts +83 -0
  311. package/src/shared/filereview/events.ts +449 -0
  312. package/src/shared/filereview/git-substrate.ts +555 -0
  313. package/src/shared/filereview/index.ts +60 -0
  314. package/src/shared/filereview/secret-paths.ts +121 -0
  315. package/src/shared/fingerprint-secret.ts +53 -0
  316. package/src/shared/model-client.ts +122 -0
  317. package/src/shared/plan-artifact.ts +0 -2
  318. package/src/shared/status-offload.ts +433 -77
  319. package/src/shared/status.ts +13 -0
  320. package/src/shared/tool-kind.ts +33 -0
  321. package/src/shared/tool-row.ts +135 -0
  322. package/src/shared/workspace/platform-dir.ts +41 -2
  323. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  324. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  325. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  326. package/src/workflows/call-agent-orchestrator.ts +53 -6
  327. package/src/workflows/connect-mcp-server.ts +179 -24
  328. package/src/workflows/types.ts +8 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The single canonical in-memory {@link ArtifactStorage} test double.
3
+ *
4
+ * Every runner test that needs a fake storage should use this — do NOT hand-roll
5
+ * another partial `ArtifactStorage` literal. Before this helper existed, ~16 test
6
+ * files each invented their own fake (and their own `getDownloadUrl` convention:
7
+ * `mem://`, `https://artifacts.local/`, `mock://`, identity, …), which drifted
8
+ * from the real port and from each other. This double is the one place the port
9
+ * is modeled for tests.
10
+ *
11
+ * Design:
12
+ * - `upload` / `download` / `exists` are backed by one shared `Map`, so a key
13
+ * that was uploaded reads back byte-exact — the double behaves like a real
14
+ * content store, not a set of disconnected stubs.
15
+ * - Every method is a `vi.fn`, so call assertions still work
16
+ * (`expect(storage.download).toHaveBeenCalledWith(key)`), and any method can
17
+ * be overridden per-test to force a failure
18
+ * (`storage.download.mockRejectedValueOnce(new Error("… HTTP 404 …"))`).
19
+ * - `getDownloadUrl` returns `${urlBase}${key}`; tests that assert on the URL
20
+ * string pass their own `urlBase`.
21
+ */
22
+
23
+ import { vi } from "vitest";
24
+ import type { ArtifactStorage } from "../shared/artifact-storage.js";
25
+
26
+ /** An {@link ArtifactStorage} whose methods are `vi.fn` spies over a shared Map. */
27
+ export type InMemoryArtifactStorage = {
28
+ [K in keyof ArtifactStorage]: ReturnType<typeof vi.fn>;
29
+ } & ArtifactStorage;
30
+
31
+ export interface InMemoryArtifactStorageOptions {
32
+ /** Prefix for the URLs `getDownloadUrl` returns. Defaults to `mem://`. */
33
+ readonly urlBase?: string;
34
+ }
35
+
36
+ export interface InMemoryArtifactStorageHandle {
37
+ /** The storage double to inject; all methods are spy-able `vi.fn`s. */
38
+ readonly storage: InMemoryArtifactStorage;
39
+ /** The backing store — inspect or seed it directly in a test. */
40
+ readonly blobs: Map<string, Buffer>;
41
+ }
42
+
43
+ /**
44
+ * Build an in-memory {@link ArtifactStorage} backed by a shared `Map`.
45
+ *
46
+ * @example
47
+ * const { storage, blobs } = makeInMemoryArtifactStorage();
48
+ * await storage.upload("k", Buffer.from("hi"));
49
+ * expect((await storage.download("k")).toString()).toBe("hi");
50
+ */
51
+ export function makeInMemoryArtifactStorage(
52
+ opts: InMemoryArtifactStorageOptions = {},
53
+ ): InMemoryArtifactStorageHandle {
54
+ const urlBase = opts.urlBase ?? "mem://";
55
+ const blobs = new Map<string, Buffer>();
56
+
57
+ const storage = {
58
+ upload: vi.fn(async (key: string, content: Buffer, _contentType?: string) => {
59
+ blobs.set(key, Buffer.from(content));
60
+ return key;
61
+ }),
62
+ getDownloadUrl: vi.fn(async (key: string) => `${urlBase}${key}`),
63
+ download: vi.fn(async (key: string) => {
64
+ const b = blobs.get(key);
65
+ if (!b) throw new Error(`Artifact not found for key '${key}'`);
66
+ return Buffer.from(b);
67
+ }),
68
+ exists: vi.fn(async (key: string) => blobs.has(key)),
69
+ } as InMemoryArtifactStorage;
70
+
71
+ return { storage, blobs };
72
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The runnable HITL gateway P0 safety contract.
3
+ *
4
+ * Runs the single, authoritative invariant catalog (see
5
+ * `__test-utils__/approval-contract/contract.ts`) against BOTH real enforcement
6
+ * substrates — the in-process deep-agent gate (a real LangGraph graph) and the
7
+ * out-of-process Cursor deny-oracle (the real bash hook) — and then asserts the
8
+ * two substrates AGREE on the same logical action.
9
+ *
10
+ * This is the consolidation home for the Phase-2 (T03) gateway safety behaviors:
11
+ * reverting any P0 behavior on either substrate fails here, and a future
12
+ * substrate joins the safety net by implementing `GatewaySubstrate` and adding
13
+ * one line below. The Cursor suite self-skips where `bash` is unavailable.
14
+ */
15
+
16
+ import {
17
+ describeGatewayContract,
18
+ describeCrossSubstrateAgreement,
19
+ } from "../__test-utils__/approval-contract/contract.js";
20
+ import { createDeepAgentSubstrate } from "../activities/execute-deep-agent/__test-utils__/gateway-substrate.js";
21
+ import { createCursorSubstrate } from "../activities/execute-cursor/__test-utils__/gateway-substrate.js";
22
+
23
+ const deepAgent = createDeepAgentSubstrate();
24
+ const cursor = createCursorSubstrate();
25
+
26
+ describeGatewayContract(deepAgent);
27
+ describeGatewayContract(cursor);
28
+
29
+ describeCrossSubstrateAgreement([deepAgent, cursor]);
@@ -1,8 +1,8 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { ClaimcheckPayloadCodec } from "../claimcheck/payload-codec.js";
3
3
  import { compress } from "../claimcheck/compressor.js";
4
4
  import type { ClaimcheckConfig } from "../claimcheck/config.js";
5
- import type { ArtifactStorage } from "../shared/artifact-storage.js";
5
+ import { makeInMemoryArtifactStorage } from "../__test-utils__/fake-artifact-storage.js";
6
6
  import type { Payload } from "@temporalio/common";
7
7
 
8
8
  function makeConfig(overrides: Partial<ClaimcheckConfig> = {}): ClaimcheckConfig {
@@ -15,23 +15,12 @@ function makeConfig(overrides: Partial<ClaimcheckConfig> = {}): ClaimcheckConfig
15
15
  };
16
16
  }
17
17
 
18
- function makeStorage(): ArtifactStorage & {
19
- uploads: Map<string, Buffer>;
20
- } {
21
- const uploads = new Map<string, Buffer>();
22
- return {
23
- uploads,
24
- async upload(key: string, content: Buffer) {
25
- uploads.set(key, content);
26
- return key;
27
- },
28
- async getDownloadUrl(key: string) {
29
- return `mock://storage/${key}`;
30
- },
31
- async exists(key: string) {
32
- return uploads.has(key);
33
- },
34
- };
18
+ function makeStorage() {
19
+ // The canonical in-memory double; `uploads` aliases its backing Map so the
20
+ // existing `storage.uploads.*` assertions keep working, and `download` reads
21
+ // straight from what `encode` uploaded (no fetch to stub).
22
+ const { storage, blobs } = makeInMemoryArtifactStorage();
23
+ return Object.assign(storage, { uploads: blobs });
35
24
  }
36
25
 
37
26
  function makePayload(data: string | Buffer): Payload {
@@ -158,21 +147,9 @@ describe("ClaimcheckPayloadCodec", () => {
158
147
  const original = makeLargePayload(2048);
159
148
  const [encoded] = await codec.encode([original]);
160
149
 
161
- const marker = JSON.parse(Buffer.from(encoded.data!).toString());
162
- const storedData = storage.uploads.get(marker.key)!;
163
-
164
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
165
- ok: true,
166
- arrayBuffer: () => Promise.resolve(storedData.buffer.slice(
167
- storedData.byteOffset,
168
- storedData.byteOffset + storedData.byteLength,
169
- )),
170
- }));
171
-
150
+ // decode reads back through storage.download from what encode uploaded.
172
151
  const [decoded] = await codec.decode([encoded]);
173
152
  expect(Buffer.from(decoded.data!)).toEqual(original.data);
174
-
175
- vi.unstubAllGlobals();
176
153
  });
177
154
 
178
155
  it("retrieves uncompressed payloads correctly", async () => {
@@ -184,24 +161,11 @@ describe("ClaimcheckPayloadCodec", () => {
184
161
  const original = makeLargePayload(1500);
185
162
  const [encoded] = await codec.encode([original]);
186
163
 
187
- const marker = JSON.parse(Buffer.from(encoded.data!).toString());
188
- const storedData = storage.uploads.get(marker.key)!;
189
-
190
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
191
- ok: true,
192
- arrayBuffer: () => Promise.resolve(storedData.buffer.slice(
193
- storedData.byteOffset,
194
- storedData.byteOffset + storedData.byteLength,
195
- )),
196
- }));
197
-
198
164
  const [decoded] = await codec.decode([encoded]);
199
165
  expect(Buffer.from(decoded.data!)).toEqual(original.data);
200
-
201
- vi.unstubAllGlobals();
202
166
  });
203
167
 
204
- it("throws on download failure", async () => {
168
+ it("wraps a download failure with the claimcheck error contract", async () => {
205
169
  const encoded: Payload = {
206
170
  metadata: { encoding: Buffer.from("binary/claimcheck") },
207
171
  data: Buffer.from(JSON.stringify({
@@ -211,16 +175,15 @@ describe("ClaimcheckPayloadCodec", () => {
211
175
  })),
212
176
  };
213
177
 
214
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
215
- ok: false,
216
- status: 404,
217
- }));
178
+ // Drive the failure through the port; the proxy download surfaces the HTTP
179
+ // status, which the codec must preserve in its wrapped, key-scoped message.
180
+ storage.download.mockRejectedValueOnce(
181
+ new Error("Artifact download failed (HTTP 404) for key 'claimcheck/missing'"),
182
+ );
218
183
 
219
184
  await expect(codec.decode([encoded])).rejects.toThrow(
220
- /Claimcheck retrieve failed.*HTTP 404/,
185
+ /Claimcheck retrieve failed for key claimcheck\/missing.*HTTP 404/,
221
186
  );
222
-
223
- vi.unstubAllGlobals();
224
187
  });
225
188
  });
226
189
 
@@ -78,6 +78,8 @@ function createMockActivities() {
78
78
  RunShell: async (): Promise<unknown> => "hello from shell",
79
79
  UpdateWorkflowTaskApprovalStatus: async (): Promise<void> => {},
80
80
  ClearWorkflowApprovalStatus: async (): Promise<void> => {},
81
+ UpdateWorkflowFileReviewStatus: async (): Promise<void> => {},
82
+ GetAwaitingFileReviewChangeSetIds: async (): Promise<string[]> => [],
81
83
  };
82
84
  }
83
85
 
@@ -92,7 +92,7 @@ describe("createRunnerTokenCoordinator", () => {
92
92
  it("re-mints from the control-plane token before expiry and reschedules", async () => {
93
93
  const applyProxyToken = vi.fn();
94
94
  const reMint = vi
95
- .fn<[], Promise<RefreshedRunnerToken | undefined>>()
95
+ .fn<() => Promise<RefreshedRunnerToken | undefined>>()
96
96
  .mockResolvedValueOnce({ token: "rt_v2", expiresInSeconds: 100 })
97
97
  .mockResolvedValueOnce({ token: "rt_v3", expiresInSeconds: 100 });
98
98
 
@@ -121,7 +121,7 @@ describe("createRunnerTokenCoordinator", () => {
121
121
  it("retries (without clobbering) when a re-mint fails, then recovers", async () => {
122
122
  const applyProxyToken = vi.fn();
123
123
  const reMint = vi
124
- .fn<[], Promise<RefreshedRunnerToken | undefined>>()
124
+ .fn<() => Promise<RefreshedRunnerToken | undefined>>()
125
125
  .mockResolvedValueOnce(undefined) // first refresh fails to mint
126
126
  .mockResolvedValueOnce({ token: "rt_recovered", expiresInSeconds: 100 });
127
127
 
@@ -149,7 +149,7 @@ describe("createRunnerTokenCoordinator", () => {
149
149
  });
150
150
 
151
151
  it("stop() cancels the refresh timer", async () => {
152
- const reMint = vi.fn<[], Promise<RefreshedRunnerToken | undefined>>();
152
+ const reMint = vi.fn<() => Promise<RefreshedRunnerToken | undefined>>();
153
153
  const coordinator = createRunnerTokenCoordinator({
154
154
  applyProxyToken: vi.fn(),
155
155
  reMint,
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { FileChangeSetStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
+
4
+ /**
5
+ * Unit tests for the call-agent-status activities that surface a child agent's
6
+ * HITL state on the parent workflow. They verify the runner sends the correct
7
+ * per-child SCOPED writes; the actual per-child merge (never clobbering a
8
+ * sibling) is enforced and tested in the Go + Java backend handlers.
9
+ */
10
+
11
+ interface CapturedUpdate {
12
+ executionId: string;
13
+ status: any;
14
+ options: any;
15
+ }
16
+
17
+ let capturedUpdates: CapturedUpdate[];
18
+ let mockGetExecutionResult: any;
19
+
20
+ vi.mock("../../client/stigmer-client.js", () => ({
21
+ StigmerClient: vi.fn().mockImplementation(() => ({
22
+ updateWorkflowExecutionStatus: (executionId: string, status: any, options: any) => {
23
+ capturedUpdates.push({ executionId, status, options });
24
+ return Promise.resolve({});
25
+ },
26
+ getExecution: (_id: string) => Promise.resolve(mockGetExecutionResult),
27
+ })),
28
+ }));
29
+
30
+ vi.mock("../../config.js", () => ({
31
+ loadConfig: () => ({ stigmerBackendEndpoint: "http://localhost:7234", stigmerToken: "t" }),
32
+ }));
33
+
34
+ import {
35
+ updateWorkflowFileReviewStatus,
36
+ getAwaitingFileReviewChangeSetIds,
37
+ updateWorkflowTaskApprovalStatus,
38
+ clearWorkflowApprovalStatus,
39
+ } from "../call-agent-status.js";
40
+
41
+ describe("call-agent-status file-review activities", () => {
42
+ beforeEach(() => {
43
+ capturedUpdates = [];
44
+ mockGetExecutionResult = undefined;
45
+ });
46
+
47
+ describe("updateWorkflowFileReviewStatus", () => {
48
+ it("writes a single per-child reference scoped to the child", async () => {
49
+ await updateWorkflowFileReviewStatus("wfx_1", "aex_child", ["fcs_1", "fcs_2"]);
50
+
51
+ expect(capturedUpdates).toHaveLength(1);
52
+ const { executionId, status, options } = capturedUpdates[0];
53
+ expect(executionId).toBe("wfx_1");
54
+ expect(options.updatePendingFileReviews).toBe(true);
55
+ expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
56
+ expect(status.pendingFileReviews).toHaveLength(1);
57
+ expect(status.pendingFileReviews[0].childAgentExecutionId).toBe("aex_child");
58
+ expect(status.pendingFileReviews[0].changeSetId).toEqual(["fcs_1", "fcs_2"]);
59
+ });
60
+
61
+ it("empty changeSetIds writes an empty list (scoped clear for the child)", async () => {
62
+ await updateWorkflowFileReviewStatus("wfx_1", "aex_child", []);
63
+
64
+ expect(capturedUpdates).toHaveLength(1);
65
+ const { status, options } = capturedUpdates[0];
66
+ expect(options.updatePendingFileReviews).toBe(true);
67
+ expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
68
+ expect(status.pendingFileReviews).toHaveLength(0);
69
+ });
70
+
71
+ it("is a no-op when executionId or childExecutionId is missing", async () => {
72
+ await updateWorkflowFileReviewStatus("", "aex_child", ["fcs_1"]);
73
+ await updateWorkflowFileReviewStatus("wfx_1", "", ["fcs_1"]);
74
+ expect(capturedUpdates).toHaveLength(0);
75
+ });
76
+ });
77
+
78
+ describe("getAwaitingFileReviewChangeSetIds", () => {
79
+ it("returns only the ids of change sets that are AWAITING_REVIEW", async () => {
80
+ mockGetExecutionResult = {
81
+ status: {
82
+ fileChangeSets: [
83
+ { id: "fcs_await1", status: FileChangeSetStatus.AWAITING_REVIEW },
84
+ { id: "fcs_capturing", status: FileChangeSetStatus.CAPTURING },
85
+ { id: "fcs_decided", status: FileChangeSetStatus.DECIDED },
86
+ { id: "fcs_await2", status: FileChangeSetStatus.AWAITING_REVIEW },
87
+ ],
88
+ },
89
+ };
90
+
91
+ const ids = await getAwaitingFileReviewChangeSetIds("aex_child");
92
+ expect(ids).toEqual(["fcs_await1", "fcs_await2"]);
93
+ });
94
+
95
+ it("returns empty when the child has no file change sets", async () => {
96
+ mockGetExecutionResult = { status: {} };
97
+ expect(await getAwaitingFileReviewChangeSetIds("aex_child")).toEqual([]);
98
+ });
99
+
100
+ it("returns empty (non-fatal) on a missing child id", async () => {
101
+ expect(await getAwaitingFileReviewChangeSetIds("")).toEqual([]);
102
+ expect(capturedUpdates).toHaveLength(0);
103
+ });
104
+ });
105
+
106
+ describe("approval activities are scoped per-child (unify)", () => {
107
+ it("updateWorkflowTaskApprovalStatus scopes the write to the notifying child", async () => {
108
+ await updateWorkflowTaskApprovalStatus("wfx_1", "task_a", {
109
+ executionId: "aex_child",
110
+ pendingApprovals: [{ toolCallId: "tc_1", toolName: "deploy" } as any],
111
+ } as any);
112
+
113
+ expect(capturedUpdates).toHaveLength(1);
114
+ const { status, options } = capturedUpdates[0];
115
+ expect(options.updatePendingApprovals).toBe(true);
116
+ expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
117
+ expect(status.pendingApprovals[0].childAgentExecutionId).toBe("aex_child");
118
+ });
119
+
120
+ it("clearWorkflowApprovalStatus scopes the clear to the given child", async () => {
121
+ await clearWorkflowApprovalStatus("wfx_1", "aex_child");
122
+
123
+ expect(capturedUpdates).toHaveLength(1);
124
+ const { status, options } = capturedUpdates[0];
125
+ expect(options.updatePendingApprovals).toBe(true);
126
+ expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
127
+ expect(status.pendingApprovals).toHaveLength(0);
128
+ });
129
+
130
+ it("clearWorkflowApprovalStatus is a no-op without a child id", async () => {
131
+ await clearWorkflowApprovalStatus("wfx_1", "");
132
+ expect(capturedUpdates).toHaveLength(0);
133
+ });
134
+ });
135
+ });
@@ -32,7 +32,7 @@ describe("callLlmAction", () => {
32
32
 
33
33
  // The LangChain provider SDKs pass a `Headers` instance (not a plain object)
34
34
  // to fetch, so read header values case-insensitively via the Headers API.
35
- function getHeader(options: { headers?: HeadersInit }, name: string): string | null {
35
+ function getHeader(options: { headers?: ConstructorParameters<typeof Headers>[0] }, name: string): string | null {
36
36
  return new Headers(options.headers).get(name);
37
37
  }
38
38
 
@@ -8,6 +8,9 @@ vi.mock("../../idle-watchdog.js", () => ({
8
8
 
9
9
  vi.mock("../../shared/model-registry.js", () => ({
10
10
  getSummarizationModel: vi.fn().mockResolvedValue("gpt-4o-mini"),
11
+ // buildChatModel resolves registry ids; identity keeps the economy model name
12
+ // intact so provider inference drives client selection in these tests.
13
+ resolveToApiModelId: vi.fn((m: string) => Promise.resolve(m)),
11
14
  }));
12
15
 
13
16
  const mockInvoke = vi.fn();
@@ -20,6 +23,14 @@ vi.mock("@langchain/openai", () => ({
20
23
  })),
21
24
  }));
22
25
 
26
+ vi.mock("@langchain/anthropic", () => ({
27
+ ChatAnthropic: vi.fn().mockImplementation(() => ({
28
+ withStructuredOutput: vi.fn().mockReturnValue({
29
+ invoke: mockInvoke,
30
+ }),
31
+ })),
32
+ }));
33
+
23
34
  vi.mock("@langchain/core/messages", () => ({
24
35
  SystemMessage: vi.fn().mockImplementation((content: string) => ({ content, role: "system" })),
25
36
  HumanMessage: vi.fn().mockImplementation((content: string) => ({ content, role: "human" })),
@@ -223,6 +234,106 @@ describe("ClassifyToolApprovals activity", () => {
223
234
  });
224
235
  });
225
236
 
237
+ describe("reconcileBatchClassifications — fail closed on partial output", () => {
238
+ it("fails closed for a tool the model omitted from its output", async () => {
239
+ const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
240
+
241
+ const batch: ToolDescriptor[] = [
242
+ { name: "delete_file", description: "Delete a file" },
243
+ { name: "send_email", description: "Send an email" },
244
+ ];
245
+ // Model only classified one of the two tools.
246
+ const llmResults = [
247
+ { tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
248
+ ];
249
+
250
+ const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, llmResults);
251
+
252
+ expect(failedClosedCount).toBe(1);
253
+ expect(reconciled).toEqual([
254
+ { tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
255
+ { tool_name: "send_email", requires_approval: true, message: "Execute send_email" },
256
+ ]);
257
+ });
258
+
259
+ it("keeps full classifications unchanged when every tool is present", async () => {
260
+ const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
261
+
262
+ const batch: ToolDescriptor[] = [
263
+ { name: "search_code", description: "Search" },
264
+ { name: "delete_file", description: "Delete" },
265
+ ];
266
+ const llmResults = [
267
+ { tool_name: "search_code", requires_approval: false, message: "" },
268
+ { tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
269
+ ];
270
+
271
+ const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, llmResults);
272
+
273
+ expect(failedClosedCount).toBe(0);
274
+ expect(reconciled).toEqual(llmResults);
275
+ });
276
+
277
+ it("drops hallucinated output names that were never in the batch", async () => {
278
+ const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
279
+
280
+ const batch: ToolDescriptor[] = [{ name: "real_tool", description: "Real" }];
281
+ const llmResults = [
282
+ { tool_name: "real_tool", requires_approval: false, message: "" },
283
+ { tool_name: "ghost_tool", requires_approval: false, message: "" },
284
+ ];
285
+
286
+ const { reconciled } = reconcileBatchClassifications(batch, llmResults);
287
+
288
+ expect(reconciled).toHaveLength(1);
289
+ expect(reconciled[0].tool_name).toBe("real_tool");
290
+ });
291
+
292
+ it("fails closed for the entire batch when the model returns nothing", async () => {
293
+ const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
294
+
295
+ const batch: ToolDescriptor[] = [
296
+ { name: "tool_a", description: "" },
297
+ { name: "tool_b", description: "" },
298
+ ];
299
+
300
+ const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, []);
301
+
302
+ expect(failedClosedCount).toBe(2);
303
+ expect(reconciled.every((r) => r.requires_approval)).toBe(true);
304
+ });
305
+
306
+ it("end-to-end: an omitted mutating tool is gated, not silently dropped", async () => {
307
+ const { classifyTools } = await import("../classify-tool-approvals.js");
308
+
309
+ // The model omits `wire_transfer` entirely — only classifies the read tool.
310
+ mockInvoke.mockResolvedValueOnce({
311
+ approvals: [
312
+ { tool_name: "get_balance", requires_approval: false, message: "" },
313
+ ],
314
+ });
315
+
316
+ const result = await classifyTools(
317
+ {
318
+ tools: [
319
+ { name: "get_balance", description: "Get account balance" },
320
+ { name: "wire_transfer", description: "Transfer money" },
321
+ ],
322
+ serverName: "bank",
323
+ serverDescription: "",
324
+ mcpServerId: null,
325
+ },
326
+ makeOptions(),
327
+ );
328
+
329
+ // get_balance auto-approved by the classifier; wire_transfer (omitted)
330
+ // fails closed via reconciliation.
331
+ expect(result).toHaveLength(1);
332
+ expect(result[0].tool_name).toBe("wire_transfer");
333
+ expect(result[0].requires_approval).toBe(true);
334
+ });
335
+ });
336
+
226
337
  describe("buildToolsPayload", () => {
227
338
  it("formats tools as compact JSON with parameter names only", async () => {
228
339
  const { buildToolsPayload } = await import("../classify-tool-approvals.js");
@@ -280,7 +391,7 @@ describe("ClassifyToolApprovals activity", () => {
280
391
  });
281
392
 
282
393
  describe("fallbackApprovals", () => {
283
- it("marks all tools as requiring approval with default message", async () => {
394
+ it("marks ambiguous tools as requiring approval with default message", async () => {
284
395
  const { fallbackApprovals } = await import("../classify-tool-approvals.js");
285
396
 
286
397
  const tools: ToolDescriptor[] = [
@@ -295,6 +406,79 @@ describe("ClassifyToolApprovals activity", () => {
295
406
  { tool_name: "tool_b", requires_approval: true, message: "Execute tool_b" },
296
407
  ]);
297
408
  });
409
+
410
+ it("fails closed for every tool, including read-only-looking names", async () => {
411
+ const { fallbackApprovals } = await import("../classify-tool-approvals.js");
412
+
413
+ // Full fail-closed: with no trusted classification, even a `get_*` name is
414
+ // gated. A name is an untrusted signal and must not relax a gate on outage.
415
+ const result = fallbackApprovals([
416
+ { name: "get_app_state", description: "" },
417
+ { name: "delete_file", description: "" },
418
+ ]);
419
+
420
+ expect(result).toEqual([
421
+ { tool_name: "get_app_state", requires_approval: true, message: "Execute get_app_state" },
422
+ { tool_name: "delete_file", requires_approval: true, message: "Execute delete_file" },
423
+ ]);
424
+ });
425
+ });
426
+
427
+ describe("read-only authority is the LLM classifier alone (no name relax)", () => {
428
+ it("keeps a classifier-gated tool gated even if its name leads with a read verb", async () => {
429
+ const { classifyTools } = await import("../classify-tool-approvals.js");
430
+
431
+ // The classic hole: `get_and_delete_stale_records` leads with `get` but
432
+ // deletes. The old name heuristic auto-approved it; now the trusted LLM
433
+ // decision (gate it) must stand.
434
+ mockInvoke.mockResolvedValueOnce({
435
+ approvals: [
436
+ { tool_name: "get_and_delete_stale_records", requires_approval: true, message: "Delete stale records" },
437
+ ],
438
+ });
439
+
440
+ const result = await classifyTools(
441
+ {
442
+ tools: [{ name: "get_and_delete_stale_records", description: "Reads then deletes stale rows" }],
443
+ serverName: "db",
444
+ serverDescription: "",
445
+ mcpServerId: null,
446
+ },
447
+ makeOptions(),
448
+ );
449
+
450
+ expect(result).toHaveLength(1);
451
+ expect(result[0].tool_name).toBe("get_and_delete_stale_records");
452
+ expect(result[0].requires_approval).toBe(true);
453
+ });
454
+
455
+ it("respects the classifier auto-approving a genuine read-only tool", async () => {
456
+ const { classifyTools } = await import("../classify-tool-approvals.js");
457
+
458
+ mockInvoke.mockResolvedValueOnce({
459
+ approvals: [
460
+ { tool_name: "get_app_state", requires_approval: false, message: "" },
461
+ { tool_name: "click", requires_approval: true, message: "Click {{args.element}}" },
462
+ ],
463
+ });
464
+
465
+ const result = await classifyTools(
466
+ {
467
+ tools: [
468
+ { name: "get_app_state", description: "Reads UI state" },
469
+ { name: "click", description: "Clicks an element" },
470
+ ],
471
+ serverName: "open-computer-use",
472
+ serverDescription: "",
473
+ mcpServerId: null,
474
+ },
475
+ makeOptions(),
476
+ );
477
+
478
+ // Only what the classifier gated remains; the read tool it cleared is gone.
479
+ expect(result).toHaveLength(1);
480
+ expect(result[0].tool_name).toBe("click");
481
+ });
298
482
  });
299
483
 
300
484
  describe("idle watchdog integration", () => {
@@ -345,6 +529,29 @@ describe("ClassifyToolApprovals activity", () => {
345
529
  });
346
530
  });
347
531
 
532
+ describe("provider routing", () => {
533
+ it("routes Anthropic economy models to ChatAnthropic, not the OpenAI path", async () => {
534
+ const { ChatAnthropic } = await import("@langchain/anthropic");
535
+ const { ChatOpenAI } = await import("@langchain/openai");
536
+ const { getSummarizationModel } = await import("../../shared/model-registry.js");
537
+ vi.mocked(getSummarizationModel).mockResolvedValueOnce("claude-haiku-4.5");
538
+
539
+ const { classifyTools } = await import("../classify-tool-approvals.js");
540
+
541
+ mockInvoke.mockResolvedValueOnce({
542
+ approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
543
+ });
544
+
545
+ await classifyTools(
546
+ { tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
547
+ { proxyEndpoint: "http://proxy:8080", stigmerToken: "tok", primaryModel: "claude-opus-4" },
548
+ );
549
+
550
+ expect(ChatAnthropic).toHaveBeenCalledTimes(1);
551
+ expect(ChatOpenAI).not.toHaveBeenCalled();
552
+ });
553
+ });
554
+
348
555
  describe("proxy headers", () => {
349
556
  it("passes X-Stigmer-Mcp-Server-Id when mcpServerId is provided", async () => {
350
557
  const { ChatOpenAI } = await import("@langchain/openai");