@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,160 @@
1
+ /**
2
+ * Runtime verification: does a SUB-AGENT approval interrupt propagate to the
3
+ * PARENT checkpoint, and does Command(resume=…) route back into the nested
4
+ * interrupt?
5
+ *
6
+ * This is the make-or-break question behind installing the approval gate on
7
+ * sub-agent graphs (HITL Phase 2, Slice B). The resume path in `hitl.ts` /
8
+ * `index.ts` reads pending interrupts from the PARENT's top-level
9
+ * `graphState.tasks[].interrupts` and resumes via `Command(resume={taskId: …})`.
10
+ * If a sub-agent interrupt does NOT surface there (or cannot be resumed),
11
+ * gating sub-agent tools would hang executions — strictly worse than the current
12
+ * bypass. So we prove the mechanism end-to-end against the real deepagents +
13
+ * LangGraph runtime before wiring it.
14
+ *
15
+ * Shape under test (faithful to the real pipeline): a deepagents PARENT with a
16
+ * checkpointer and `subagents: [worker]`; the worker is a deepagents agent
17
+ * carrying our real ApprovalGate middleware and NO checkpointer (exactly how
18
+ * compileSubagents builds it). The parent's `task` tool invokes the worker via
19
+ * `worker.invoke(state, {...parentConfig})` — so a GraphInterrupt thrown inside
20
+ * the worker (no checkpointer → re-thrown) must bubble to the parent (has
21
+ * checkpointer → records it).
22
+ */
23
+
24
+ import { describe, it, expect } from "vitest";
25
+ import { HumanMessage } from "@langchain/core/messages";
26
+ import { tool } from "@langchain/core/tools";
27
+ import { z } from "zod";
28
+ import { Command, MemorySaver } from "@langchain/langgraph";
29
+ import { createDeepAgent, StateBackend } from "deepagents";
30
+ import { createApprovalGateMiddleware } from "../../../middleware/approval-gate.js";
31
+ import {
32
+ ScriptedModel,
33
+ readPendingInterrupts,
34
+ type ScriptSelector,
35
+ } from "../__test-utils__/scripted-model.js";
36
+
37
+ // Role script for this test: the WORKER (its bound tools include the gated
38
+ // `overwrite_file`) writes a file; the PARENT delegates via `task`. createDeepAgent
39
+ // injects `task` into both, so the role is keyed on the worker's unique tool.
40
+ const roleScript: ScriptSelector = (toolNames) =>
41
+ toolNames.includes("overwrite_file")
42
+ ? {
43
+ toolCalls: [
44
+ { name: "overwrite_file", args: { path: "/out.txt", content: "hello" }, id: "write_1" },
45
+ ],
46
+ done: "worker done",
47
+ }
48
+ : {
49
+ toolCalls: [
50
+ { name: "task", args: { description: "write the file", subagent_type: "worker" }, id: "task_1" },
51
+ ],
52
+ done: "parent done",
53
+ };
54
+
55
+ describe("sub-agent approval interrupt propagation", () => {
56
+ it("surfaces a sub-agent interrupt at the parent checkpoint AND resumes it", async () => {
57
+ let executed = 0;
58
+ const overwriteFile = tool(
59
+ async () => {
60
+ executed += 1;
61
+ return "wrote file";
62
+ },
63
+ {
64
+ name: "overwrite_file",
65
+ description: "overwrite a file at a path",
66
+ schema: z.object({ path: z.string(), content: z.string() }),
67
+ },
68
+ );
69
+
70
+ // Worker: real ApprovalGate middleware, NO checkpointer — exactly how
71
+ // compileSubagents builds a sub-agent. overwrite_file classifies as
72
+ // FILE_WRITE, so the gate must interrupt before it runs.
73
+ const worker = await createDeepAgent({
74
+ model: new ScriptedModel(roleScript),
75
+ tools: [overwriteFile],
76
+ middleware: [
77
+ createApprovalGateMiddleware({ policies: new Map(), toolServerMap: new Map() }),
78
+ ],
79
+ backend: new StateBackend(),
80
+ generalPurposeAgent: false,
81
+ } as unknown as Parameters<typeof createDeepAgent>[0]);
82
+
83
+ const checkpointer = new MemorySaver();
84
+ const parent = await createDeepAgent({
85
+ model: new ScriptedModel(roleScript),
86
+ checkpointer: checkpointer as never,
87
+ backend: new StateBackend(),
88
+ subagents: [{ name: "worker", description: "writes files", runnable: worker as never }],
89
+ } as unknown as Parameters<typeof createDeepAgent>[0]);
90
+
91
+ const config = { configurable: { thread_id: "thread-1" }, recursionLimit: 50 };
92
+
93
+ // Drive to the first pause. The worker's gated tool call must NOT have run.
94
+ await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
95
+
96
+ const afterFirst = await parent.getState(config);
97
+ const pending = readPendingInterrupts(afterFirst as never);
98
+
99
+ expect(pending.length).toBeGreaterThanOrEqual(1);
100
+ expect(pending[0].toolCallId).toBe("write_1");
101
+ expect(executed).toBe(0);
102
+
103
+ // Resume with APPROVE, keyed by the actual interrupt id (NOT task.id — for a
104
+ // nested sub-agent interrupt the two differ, and only the interrupt id routes
105
+ // the resume value into the worker's interrupt()).
106
+ const resumeDict: Record<string, { action: string }> = {};
107
+ for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
108
+
109
+ await parent.invoke(new Command({ resume: resumeDict }), config);
110
+
111
+ const afterResume = await parent.getState(config);
112
+ const stillPending = readPendingInterrupts(afterResume as never);
113
+
114
+ expect(stillPending.length).toBe(0);
115
+ expect(executed).toBe(1);
116
+ });
117
+
118
+ it("PARENT-level gated tool: interrupt-id keyed resume also works (fix is safe)", async () => {
119
+ // Guards the hitl.ts change: keying resume by interrupts[].id must also drive
120
+ // a top-level (non-sub-agent) approval, so switching off task.id is safe.
121
+ let executed = 0;
122
+ const overwriteFile = tool(
123
+ async () => {
124
+ executed += 1;
125
+ return "wrote file";
126
+ },
127
+ {
128
+ name: "overwrite_file",
129
+ description: "overwrite a file at a path",
130
+ schema: z.object({ path: z.string(), content: z.string() }),
131
+ },
132
+ );
133
+
134
+ const checkpointer = new MemorySaver();
135
+ const parent = await createDeepAgent({
136
+ model: new ScriptedModel(roleScript),
137
+ checkpointer: checkpointer as never,
138
+ backend: new StateBackend(),
139
+ tools: [overwriteFile],
140
+ middleware: [
141
+ createApprovalGateMiddleware({ policies: new Map(), toolServerMap: new Map() }),
142
+ ],
143
+ } as unknown as Parameters<typeof createDeepAgent>[0]);
144
+
145
+ const config = { configurable: { thread_id: "thread-parent" }, recursionLimit: 50 };
146
+ await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
147
+
148
+ const afterFirst = await parent.getState(config);
149
+ const pending = readPendingInterrupts(afterFirst as never);
150
+ expect(pending.length).toBeGreaterThanOrEqual(1);
151
+ expect(executed).toBe(0);
152
+
153
+ const resumeDict: Record<string, { action: string }> = {};
154
+ for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
155
+ await parent.invoke(new Command({ resume: resumeDict }), config);
156
+
157
+ expect(readPendingInterrupts((await parent.getState(config)) as never).length).toBe(0);
158
+ expect(executed).toBe(1);
159
+ });
160
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that a
3
+ * SUB-AGENT's gitignored file writes are captured for CAS review at parity with
4
+ * the parent (Session 26, DD-19).
5
+ *
6
+ * These tests exercise the ACTUAL wiring under change — `compileSubagents` — which
7
+ * installs a CAS-observing backend on each sub-agent and flips its gate to flow
8
+ * gitignored writes (both derived from the shared observer, so "unobserved bytes"
9
+ * is impossible by construction). A non-secret gitignored write flows and is
10
+ * observed; a secret-like one is hard-blocked with nothing written; a git-tracked
11
+ * write is left to the boundary git diff (not observed).
12
+ *
13
+ * Scope note: the turn-boundary CANDIDATE authoring, `changeSetId`/`turnSeq`, and
14
+ * the Temporal resume loop are parent/activity-level machinery unchanged by this
15
+ * work (covered by the offline/corpus suites). These tests assert the observer
16
+ * capture surface (before-bytes + blocked-secret paths + on-disk result) that
17
+ * those consumers read.
18
+ */
19
+
20
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
21
+ import { mkdtemp, mkdir, rm, readFile, access } from "node:fs/promises";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { HumanMessage } from "@langchain/core/messages";
25
+ import { tool } from "@langchain/core/tools";
26
+ import { z } from "zod";
27
+ import { Command, MemorySaver } from "@langchain/langgraph";
28
+ import { createDeepAgent, StateBackend } from "deepagents";
29
+ import type { ApprovalGateConfig } from "../../../middleware/approval-gate.js";
30
+ import { partitionIgnoredPathsBySecret } from "../../../shared/filereview/secret-paths.js";
31
+ import { resolveWorkspacePath } from "../../../shared/file-change.js";
32
+ import { CasCaptureObserver } from "../cas-capture-observer.js";
33
+ import { compileSubagents, type TransformedSubagent } from "../subagent-transformer.js";
34
+ import { ScriptedModel, readPendingInterrupts, type ScriptSelector } from "../__test-utils__/scripted-model.js";
35
+
36
+ /** Only `src/**` is git-tracked (capturable); everything else is gitignored. */
37
+ const isTracked = (relPath: string): boolean => relPath.startsWith("src/");
38
+
39
+ describe("sub-agent gitignored capture (DD-19)", () => {
40
+ let root: string;
41
+
42
+ beforeEach(async () => {
43
+ root = await mkdtemp(join(tmpdir(), "sa-cas-"));
44
+ await mkdir(join(root, "dist"), { recursive: true });
45
+ await mkdir(join(root, "src"), { recursive: true });
46
+ await mkdir(join(root, "cache"), { recursive: true });
47
+ });
48
+
49
+ afterEach(async () => {
50
+ await rm(root, { recursive: true, force: true });
51
+ });
52
+
53
+ function makeObserver(): CasCaptureObserver {
54
+ return new CasCaptureObserver({
55
+ rootDir: root,
56
+ isIgnored: async (relPath) => !isTracked(relPath),
57
+ });
58
+ }
59
+
60
+ /**
61
+ * A capture-mode gate wired to `observer`, exactly as setup.ts builds the parent
62
+ * gate and buildSubAgentMiddleware inherits it when a CAS observer backs the
63
+ * sub-agent (captureIgnored true).
64
+ */
65
+ function captureGate(observer: CasCaptureObserver): ApprovalGateConfig {
66
+ return {
67
+ policies: new Map(),
68
+ toolServerMap: new Map(),
69
+ fileCaptureMode: true,
70
+ isCapturablePath: async (raw: string) =>
71
+ isTracked(resolveWorkspacePath(raw, root, true).path),
72
+ captureIgnored: true,
73
+ recordBlockedSecret: (raw: string) => observer.recordBlockedSecret(raw),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Compile a sub-agent through the production path (the code under change), with
79
+ * the shared CAS observer so its backend + gate are the real capture wiring.
80
+ */
81
+ async function compileWorker(
82
+ observer: CasCaptureObserver,
83
+ script: ScriptSelector,
84
+ tools: TransformedSubagent["tools"] = [],
85
+ ) {
86
+ const compiled = await compileSubagents(
87
+ [{ name: "worker", description: "test worker", systemPrompt: "do the work", tools }],
88
+ {
89
+ approvalGate: captureGate(observer),
90
+ parentModelName: "test-model",
91
+ workspaceRootDir: root,
92
+ casObserver: observer,
93
+ modelFactory: async () => new ScriptedModel(script),
94
+ },
95
+ );
96
+ expect(compiled).toHaveLength(1);
97
+ return compiled[0];
98
+ }
99
+
100
+ it("captures a sub-agent's non-secret gitignored write, hard-blocks a secret one, and leaves git-tracked to the git diff", async () => {
101
+ const observer = makeObserver();
102
+
103
+ // A single worker; script it to write three files in one turn (no `task`, so
104
+ // no sub-agent delegation ambiguity — we invoke the compiled sub-agent, which
105
+ // IS what the parent dispatches, directly).
106
+ const script: ScriptSelector = () => ({
107
+ toolCalls: [
108
+ { name: "write_file", args: { file_path: "dist/app.js", content: "console.log(1)" }, id: "w_ignored" },
109
+ { name: "write_file", args: { file_path: ".env", content: "SECRET=shh" }, id: "w_secret" },
110
+ { name: "write_file", args: { file_path: "src/app.ts", content: "export const x = 1;" }, id: "w_tracked" },
111
+ ],
112
+ done: "worker done",
113
+ });
114
+
115
+ const worker = await compileWorker(observer, script);
116
+ await worker.runnable.invoke(
117
+ { messages: [new HumanMessage({ content: "go" })] },
118
+ { configurable: { thread_id: "t1" }, recursionLimit: 50 },
119
+ );
120
+
121
+ // Non-secret gitignored write: flowed (on disk) AND observed (before=null ADD).
122
+ expect(await readFile(join(root, "dist/app.js"), "utf8")).toBe("console.log(1)");
123
+ expect(observer.before.has("dist/app.js")).toBe(true);
124
+ expect(observer.before.get("dist/app.js")).toBeNull();
125
+
126
+ // Secret-like gitignored write: hard-blocked — nothing written, path recorded.
127
+ await expect(access(join(root, ".env"))).rejects.toThrow();
128
+ expect([...observer.blockedSecretPaths]).toContain(".env");
129
+
130
+ // Git-tracked write: flowed to disk but NOT observed (the git diff owns it).
131
+ expect(await readFile(join(root, "src/app.ts"), "utf8")).toBe("export const x = 1;");
132
+ expect(observer.before.has("src/app.ts")).toBe(false);
133
+
134
+ // Bridge to the boundary: the observer state partitions into a
135
+ // GIT_IGNORED_CAPTURED (dist/app.js) and a DIFF_UNREVIEWABLE (.env), exactly
136
+ // as buildCasTurnCaptures does at the turn boundary.
137
+ const { capturablePaths, unreviewablePaths } = partitionIgnoredPathsBySecret(
138
+ observer.before.keys(),
139
+ observer.blockedSecretPaths,
140
+ );
141
+ expect([...capturablePaths]).toEqual(["dist/app.js"]);
142
+ expect([...unreviewablePaths]).toEqual([".env"]);
143
+ });
144
+
145
+ it("mixed turn: a sub-agent's gitignored write is captured even when the same turn pauses on a gated tool, and resume completes", async () => {
146
+ const observer = makeObserver();
147
+ let shellRuns = 0;
148
+ // `shell` classifies as the SHELL category, so capture mode never bypasses it —
149
+ // it interrupts (parity with the parent). `cas_marker` is a role marker unique
150
+ // to the worker (the parent lacks it), used only to select the worker's script
151
+ // branch — deepagents auto-injects `task` into every agent, so `task` cannot
152
+ // distinguish the roles.
153
+ const shell = tool(async () => { shellRuns += 1; return "ran"; }, {
154
+ name: "shell",
155
+ description: "run a command",
156
+ schema: z.object({ command: z.string() }),
157
+ });
158
+ const marker = tool(async () => "noop", {
159
+ name: "cas_marker",
160
+ description: "worker role marker",
161
+ schema: z.object({}),
162
+ });
163
+
164
+ const script: ScriptSelector = (toolNames) =>
165
+ toolNames.includes("cas_marker")
166
+ ? {
167
+ toolCalls: [
168
+ { name: "write_file", args: { file_path: "cache/data.txt", content: "cached" }, id: "w_ignored" },
169
+ { name: "shell", args: { command: "echo hi" }, id: "sh_1" },
170
+ ],
171
+ done: "worker done",
172
+ }
173
+ : {
174
+ toolCalls: [
175
+ { name: "task", args: { description: "work", subagent_type: "worker" }, id: "task_1" },
176
+ ],
177
+ done: "parent done",
178
+ };
179
+
180
+ const worker = await compileWorker(observer, script, [shell, marker] as TransformedSubagent["tools"]);
181
+
182
+ const parent = await createDeepAgent({
183
+ model: new ScriptedModel(script),
184
+ checkpointer: new MemorySaver() as never,
185
+ backend: new StateBackend(),
186
+ subagents: [{ name: worker.name, description: worker.description, runnable: worker.runnable as never }],
187
+ } as unknown as Parameters<typeof createDeepAgent>[0]);
188
+
189
+ const config = { configurable: { thread_id: "t2" }, recursionLimit: 50 };
190
+
191
+ // Invocation 1: the gitignored write flows and is captured BEFORE the turn
192
+ // pauses on the gated shell tool (the shell must NOT have run yet).
193
+ await parent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
194
+
195
+ const pending = readPendingInterrupts((await parent.getState(config)) as never);
196
+ expect(pending.some((p) => p.toolCallId === "sh_1")).toBe(true);
197
+ expect(shellRuns).toBe(0);
198
+ expect(await readFile(join(root, "cache/data.txt"), "utf8")).toBe("cached");
199
+ expect(observer.before.get("cache/data.txt")).toBeNull();
200
+
201
+ // Resume with APPROVE: the worker re-runs (no checkpointer) and the shell runs
202
+ // once. First-touch-wins keeps the single pre-turn baseline for the re-written
203
+ // gitignored file (no duplicate, no post-write baseline).
204
+ const resumeDict: Record<string, { action: string }> = {};
205
+ for (const p of pending) resumeDict[p.interruptId] = { action: "approve" };
206
+ await parent.invoke(new Command({ resume: resumeDict }), config);
207
+
208
+ expect(readPendingInterrupts((await parent.getState(config)) as never).length).toBe(0);
209
+ expect(shellRuns).toBe(1);
210
+ expect(observer.before.size).toBe(1);
211
+ expect(observer.before.get("cache/data.txt")).toBeNull();
212
+ });
213
+ });
@@ -428,8 +428,7 @@ describe("transformAndCompileSubagents", () => {
428
428
  parentMcpUsages: [] as McpServerUsage[],
429
429
  skillClient: {} as unknown,
430
430
  workspaceBackend: mockWorkspaceBackend(),
431
- approvalPolicies: new Map(),
432
- autoApproveAll: false,
431
+ approvalGate: null,
433
432
  parentModelName: "claude-sonnet-4-6",
434
433
  parentHasNativeThinking: true,
435
434
  costCap: undefined,
@@ -659,8 +658,7 @@ describe("subagent-transformer edge cases", () => {
659
658
  parentMcpUsages: [],
660
659
  skillClient: {} as unknown,
661
660
  workspaceBackend: mockWorkspaceBackend({ rootDir: "" }),
662
- approvalPolicies: new Map(),
663
- autoApproveAll: false,
661
+ approvalGate: null,
664
662
  parentModelName: "claude-sonnet-4-6",
665
663
  parentHasNativeThinking: true,
666
664
  costCap: undefined,
@@ -683,8 +681,7 @@ describe("subagent-transformer edge cases", () => {
683
681
  parentMcpUsages: [],
684
682
  skillClient: {} as unknown,
685
683
  workspaceBackend: mockWorkspaceBackend(),
686
- approvalPolicies: new Map(),
687
- autoApproveAll: false,
684
+ approvalGate: null,
688
685
  parentModelName: "claude-sonnet-4-6",
689
686
  parentHasNativeThinking: true,
690
687
  costCap: undefined,
@@ -1,6 +1,7 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
  import { buildSubAgentMiddleware } from "../subagent-wiring.js";
3
3
  import { createCostCapMiddleware } from "../../../middleware/cost-cap.js";
4
+ import * as approvalGateModule from "../../../middleware/approval-gate.js";
4
5
 
5
6
  describe("buildSubAgentMiddleware", () => {
6
7
  it("returns the correct middleware order without cost cap", () => {
@@ -68,4 +69,86 @@ describe("buildSubAgentMiddleware", () => {
68
69
 
69
70
  expect(stack[2].name).toBe("ToolTruncationMiddleware");
70
71
  });
72
+
73
+ it("installs the approval gate when an approvalGate config is provided", () => {
74
+ const stack = buildSubAgentMiddleware({
75
+ approvalGate: { policies: new Map(), toolServerMap: new Map() },
76
+ });
77
+
78
+ // loop, budget, truncation, approval gate
79
+ expect(stack).toHaveLength(4);
80
+ expect(stack[3].name).toBe("ApprovalGateMiddleware");
81
+ expect(stack[3].wrapToolCall).toBeDefined();
82
+ });
83
+
84
+ it("omits the approval gate when approvalGate is null (auto-approve-all parity)", () => {
85
+ const stack = buildSubAgentMiddleware({ approvalGate: null });
86
+
87
+ expect(stack).toHaveLength(3);
88
+ expect(stack.some((m) => m.name === "ApprovalGateMiddleware")).toBe(false);
89
+ });
90
+
91
+ it("orders the gate before the cost-cap view", () => {
92
+ const parentCostCap = createCostCapMiddleware({
93
+ maxCostUsd: 10,
94
+ inputPricePerMillion: 3,
95
+ outputPricePerMillion: 15,
96
+ cacheReadPricePerMillion: 0.3,
97
+ warningPct: 80,
98
+ });
99
+
100
+ const stack = buildSubAgentMiddleware({
101
+ costCap: parentCostCap,
102
+ approvalGate: { policies: new Map(), toolServerMap: new Map() },
103
+ });
104
+
105
+ // loop, budget, truncation, approval gate, cost cap view
106
+ expect(stack).toHaveLength(5);
107
+ expect(stack[3].name).toBe("ApprovalGateMiddleware");
108
+ expect(stack[4].name).toBe("CostCapSubAgentView");
109
+ });
110
+
111
+ // captureIgnored is the structural coupling that makes sub-agent gitignored
112
+ // capture safe (DD-19): the gate flows gitignored writes into CAS iff a CAS
113
+ // observer backs the sub-agent's backend (compileSubagents passes !!casObserver).
114
+ describe("captureIgnored (sub-agent CAS routing)", () => {
115
+ it("captureIgnored:true inherits the parent gate verbatim (CAS routing + secret sink preserved)", () => {
116
+ const recordBlockedSecret = vi.fn();
117
+ const parentGate = {
118
+ policies: new Map(),
119
+ toolServerMap: new Map(),
120
+ captureIgnored: true,
121
+ recordBlockedSecret,
122
+ };
123
+ const spy = vi.spyOn(approvalGateModule, "createApprovalGateMiddleware");
124
+
125
+ buildSubAgentMiddleware({ approvalGate: parentGate, captureIgnored: true });
126
+
127
+ const cfg = spy.mock.calls[0]![0];
128
+ expect(cfg.captureIgnored).toBe(true);
129
+ expect(cfg.recordBlockedSecret).toBe(recordBlockedSecret);
130
+ spy.mockRestore();
131
+ });
132
+
133
+ it("regression lock: without captureIgnored, CAS routing is forced OFF and the secret sink dropped even if the parent gate had them", () => {
134
+ const recordBlockedSecret = vi.fn();
135
+ // A parent gate that DOES route gitignored writes into CAS — the sub-agent
136
+ // must not inherit that unless a CAS observer explicitly backs it, else it
137
+ // would apply unobserved, unreviewable bytes.
138
+ const parentGate = {
139
+ policies: new Map(),
140
+ toolServerMap: new Map(),
141
+ captureIgnored: true,
142
+ recordBlockedSecret,
143
+ };
144
+ const spy = vi.spyOn(approvalGateModule, "createApprovalGateMiddleware");
145
+
146
+ buildSubAgentMiddleware({ approvalGate: parentGate });
147
+
148
+ const cfg = spy.mock.calls[0]![0];
149
+ expect(cfg.captureIgnored).toBe(false);
150
+ expect(cfg.recordBlockedSecret).toBeUndefined();
151
+ spy.mockRestore();
152
+ });
153
+ });
71
154
  });
@@ -271,12 +271,15 @@ describe("V3StatusBuilder", () => {
271
271
  sb.setApprovalProvider({
272
272
  policies: new Map([
273
273
  ["my-server/dangerous_tool", {
274
+ toolName: "dangerous_tool",
275
+ mcpServerSlug: "my-server",
274
276
  requiresApproval: true,
275
277
  approvalMessage: "This tool will modify {{args.path}}",
278
+ source: "classifier_default",
276
279
  }],
277
280
  ]),
278
281
  toolServerMap: new Map([["dangerous_tool", "my-server"]]),
279
- autoApproveAll: false,
282
+ globalBypass: false,
280
283
  } as ApprovalPolicyProvider);
281
284
 
282
285
  feedAll(sb, [
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pre-execution args capture for the HITL approval gate (native harness).
3
+ *
4
+ * When the graph pauses for approval — before the tool runs — this captures a
5
+ * sanitized args preview so the gate can show the proposed change. It owns only
6
+ * the native-specific seam: correlating a gated `tool_call_id` to its arguments
7
+ * from graph state (the authoritative single source of truth at the interrupt,
8
+ * since the streamed input cache is empty and the interrupt value carries only a
9
+ * few fields). The approval card renders the proposed write/edit content from
10
+ * these args; there is no separate captured `file_changes` (removed in Phase 5
11
+ * Slice 4 — the args are the single source, and the Cursor deny-gate's
12
+ * exact-apply reads the same args on resume; see execute-cursor/exact-apply.ts).
13
+ *
14
+ * @since First-Class Diff Review (#186), approval-gate phase
15
+ */
16
+
17
+ /** What the gate capture contributes to a `WAITING_APPROVAL` `ToolCall`. */
18
+ export interface ApprovalCaptureResult {
19
+ /** Sanitized JSON args preview, omitted when there are no args to show. */
20
+ readonly argsPreview?: string;
21
+ }
22
+
23
+ import { sanitizeArgsPreview } from "./status-builder-shared.js";
24
+
25
+ /**
26
+ * Correlate a gated `tool_call_id` to its arguments by scanning graph-state
27
+ * messages for the AI-message tool call that emitted it.
28
+ *
29
+ * Matching keys on the presence of a `tool_calls` array, which is robust to
30
+ * whether the message is a hydrated LangChain class instance or a plain
31
+ * serialized object: a `ToolMessage` carries a singular `tool_call_id`, never a
32
+ * `tool_calls` array, so it cannot be mistaken for the emitting AI message.
33
+ *
34
+ * Returns the args object (possibly empty when the tool call took no arguments),
35
+ * or `undefined` when no message in scope emitted this id — e.g. a tool call
36
+ * whose AI message lives only inside a nested sub-agent state, which this
37
+ * parent-scope scan does not reach. That degrades the gate to an args-less
38
+ * preview for that one interrupt, exactly as today.
39
+ */
40
+ export function findAiMessageToolCallArgs(
41
+ messages: readonly unknown[],
42
+ toolCallId: string,
43
+ ): Record<string, unknown> | undefined {
44
+ for (const msg of messages) {
45
+ if (!msg || typeof msg !== "object") continue;
46
+ const toolCalls = (msg as { tool_calls?: unknown }).tool_calls;
47
+ if (!Array.isArray(toolCalls)) continue;
48
+
49
+ for (const tc of toolCalls) {
50
+ if (!tc || typeof tc !== "object") continue;
51
+ const entry = tc as { id?: unknown; args?: unknown };
52
+ if (entry.id !== toolCallId) continue;
53
+
54
+ return entry.args &&
55
+ typeof entry.args === "object" &&
56
+ !Array.isArray(entry.args)
57
+ ? (entry.args as Record<string, unknown>)
58
+ : {};
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Capture the gate's args preview from a single correlation lookup, keeping the
66
+ * call site thin.
67
+ *
68
+ * Returns an empty result when the tool call cannot be correlated or took no
69
+ * arguments, so the gate falls back to today's behavior for that interrupt.
70
+ */
71
+ export function captureApprovalArtifacts(opts: {
72
+ readonly toolCallId: string;
73
+ readonly messages: readonly unknown[];
74
+ }): ApprovalCaptureResult {
75
+ const args = findAiMessageToolCallArgs(opts.messages, opts.toolCallId);
76
+ if (!args || Object.keys(args).length === 0) return {};
77
+
78
+ const argsPreview = sanitizeArgsPreview(args) || undefined;
79
+ return { argsPreview };
80
+ }
@@ -41,7 +41,14 @@ export interface InjectedFile {
41
41
  export interface InjectAttachmentsOptions {
42
42
  readonly backend: WorkspaceBackend;
43
43
  readonly attachments: readonly Attachment[];
44
- readonly storage: ArtifactStorage;
44
+ /**
45
+ * Artifact storage for cloud-mode attachment download. `undefined` when the
46
+ * runner could not build a store (proxy misconfig). With no attachments this is
47
+ * never touched (the function early-returns); a cloud-mode attachment that
48
+ * genuinely needs storage surfaces a clear {@link AttachmentInjectionError}
49
+ * rather than dereferencing undefined.
50
+ */
51
+ readonly storage: ArtifactStorage | undefined;
45
52
  readonly isLocalMode: boolean;
46
53
  }
47
54
 
@@ -328,7 +335,7 @@ function deriveFilename(storageKey: string): string {
328
335
 
329
336
  async function downloadAttachment(
330
337
  attachment: Attachment,
331
- storage: ArtifactStorage,
338
+ storage: ArtifactStorage | undefined,
332
339
  isLocalMode: boolean,
333
340
  ): Promise<Buffer> {
334
341
  // Local mode fast path: read directly from filesystem
@@ -351,16 +358,18 @@ async function downloadAttachment(
351
358
  );
352
359
  }
353
360
 
354
- try {
355
- const url = await storage.getDownloadUrl(attachment.storageKey);
356
- const response = await fetch(url);
357
-
358
- if (!response.ok) {
359
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
360
- }
361
+ // The attachment must come from storage, but the runner could not build one
362
+ // (proxy misconfig). Fail with an actionable message rather than dereferencing
363
+ // undefined consistent with the degraded posture, never a silent skip.
364
+ if (!storage) {
365
+ throw new AttachmentInjectionError(
366
+ attachment.filename,
367
+ `artifact storage is unavailable, so this attachment (key: ${attachment.storageKey}) cannot be downloaded`,
368
+ );
369
+ }
361
370
 
362
- const arrayBuffer = await response.arrayBuffer();
363
- return Buffer.from(arrayBuffer);
371
+ try {
372
+ return await storage.download(attachment.storageKey);
364
373
  } catch (err) {
365
374
  if (err instanceof AttachmentInjectionError) throw err;
366
375
  throw new AttachmentInjectionError(