@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
@@ -16,9 +16,31 @@
16
16
  * pausing again.
17
17
  *
18
18
  * Platform tool defaults: DeepAgents JS backend tools (read, write,
19
- * edit, execute, etc.) are not covered by MCP policy chains. This
20
- * middleware applies sensible defaults: read-only tools are auto-approved,
21
- * mutating tools require approval when no explicit policy exists.
19
+ * edit, execute, etc.) are not covered by MCP policy chains. Built-in tools
20
+ * are classified through the shared {@link toolApprovalCategory} — the single
21
+ * source of truth, shared with the Cursor deny-oracle hook — so read-only tools
22
+ * are auto-approved and every mutating tool (write/edit/delete/shell) is gated
23
+ * fail-CLOSED, by category rather than by a hand-maintained name list. An
24
+ * unrecognized mutating built-in (e.g. `bash`, `overwrite_file`) is therefore
25
+ * gated by what it does, not by whether someone remembered to list it.
26
+ *
27
+ * Gateway invariant (Phase 2): this middleware IS the in-process execution
28
+ * gateway for the deep-agent harness — `handler(request)` is the side effect. A
29
+ * side effect runs only with a backing authorization: either (a) the tool was
30
+ * auto-approved (policy/classifier cleared it, or auto-approve-all disabled the
31
+ * whole gate), or (b) the user explicitly approved THIS interrupted call. Every
32
+ * other outcome — skip, reject, or an unrecognized decision — returns a
33
+ * ToolMessage WITHOUT executing. There is no path from a model proposal to a
34
+ * side effect that skips an authorization.
35
+ *
36
+ * Shadow ExecutionReceipt: when the gateway lets a side effect through it emits a
37
+ * structured, non-persisted receipt (a `[hitl-gateway] receipt …` log carrying
38
+ * the action's HMAC fingerprint and the authorization source). This is an audit
39
+ * + uniformity signal only — no proto, no storage — mirroring the Phase-1 shadow
40
+ * discipline. On the deep-agent normal path the fingerprint match is guaranteed
41
+ * by LangGraph checkpoint replay (the resumed action equals the approved one), so
42
+ * the receipt is defense-in-depth here; the fingerprint earns real enforcement
43
+ * teeth in the out-of-process Cursor substrate.
22
44
  */
23
45
 
24
46
  import { ToolMessage } from "@langchain/core/messages";
@@ -26,13 +48,79 @@ import { interrupt } from "@langchain/langgraph";
26
48
  import type { StigmerMiddleware, ToolCallRequest } from "./types.js";
27
49
  import {
28
50
  type MergedToolPolicy,
51
+ type PolicySource,
52
+ POLICY_ENGINE_VERSION,
29
53
  resolveApprovalMessage,
30
54
  } from "../shared/approval-policy.js";
55
+ import { toolApprovalCategory, type ToolApprovalCategory } from "../shared/tool-kind.js";
56
+ import { extractFilePath } from "../shared/file-tools.js";
57
+ import { isSecretLikePath } from "../shared/filereview/secret-paths.js";
58
+ import {
59
+ computeApprovalFingerprint,
60
+ type FingerprintKey,
61
+ } from "../shared/approval-fingerprint.js";
31
62
 
32
63
  export interface ApprovalGateConfig {
33
64
  readonly policies: ReadonlyMap<string, MergedToolPolicy>;
34
- readonly autoApproveAll: boolean;
65
+ /**
66
+ * Built-in approval categories with a run-lifetime lease (the scoped successor
67
+ * to auto-approve-all; see ActiveLeases). A built-in whose category is leased
68
+ * is auto-approved for the rest of the run. MCP-server leases are NOT carried
69
+ * here — they are applied upstream by dropping the server's tools from
70
+ * `policies` (mergeApprovalPolicies), which clears them for this gate too.
71
+ * Absent/empty means no built-in lease is active. Inherited verbatim by
72
+ * sub-agents along with the rest of this config.
73
+ */
74
+ readonly leasedCategories?: ReadonlySet<ToolApprovalCategory>;
35
75
  readonly toolServerMap: ReadonlyMap<string, string>;
76
+ /**
77
+ * Per-execution HMAC key for the shadow receipt's action fingerprint. Derived
78
+ * from the runner master secret + execution_id (see fingerprint-secret.ts).
79
+ * Optional: when absent (unit tests, no-secret paths) the receipt is emitted
80
+ * without a fingerprint rather than failing.
81
+ */
82
+ readonly fingerprintKey?: FingerprintKey;
83
+ /** Execution id, carried into the shadow receipt for audit correlation. */
84
+ readonly executionId?: string;
85
+ /**
86
+ * Apply-then-review capture mode (git workspaces). When true, a built-in
87
+ * `write`/`delete` whose target path is capturable (git-tracked — see
88
+ * {@link isCapturablePath}) FLOWS instead of interrupting: the edit is reviewed
89
+ * post-hoc as a captured `FileChangeSet`, not gated before it runs. A gitignored
90
+ * path is NOT capturable, so it stays gated (the git substrate cannot capture or
91
+ * revert it) — the exact twin of the Cursor hook's `__stigmer_is_gitignored`
92
+ * allow-branch. `shell` and MCP tools are never bypassed by this flag. Off (the
93
+ * default) keeps the classic true-pause gate for every mutation.
94
+ */
95
+ readonly fileCaptureMode?: boolean;
96
+ /**
97
+ * Capturability predicate for {@link fileCaptureMode}: given a tool's raw target
98
+ * path (as the model wrote it), resolves true when the path would be captured by
99
+ * the git snapshot (tracked / not gitignored). Injected by setup so the gate
100
+ * stays pure and testable, and so the harness owns path normalization (the
101
+ * deep-agent virtual-root path mapping). Absent ⇒ nothing is bypassed (safe).
102
+ */
103
+ readonly isCapturablePath?: (rawPath: string) => Promise<boolean>;
104
+ /**
105
+ * Route gitignored `write`/`edit` edits into CAS capture (apply-then-review)
106
+ * instead of the interrupt gate, applying the DD-E secret gate. When off, a
107
+ * gitignored path stays on the interrupt gate exactly as before.
108
+ *
109
+ * TRUE ONLY FOR THE PARENT GATE. Sub-agents build their own plain filesystem
110
+ * backends, which the CAS observer does not wrap, so flowing their gitignored
111
+ * edits would apply unobserved, unreviewable bytes. `buildSubAgentMiddleware`
112
+ * therefore forces this to false for sub-agent gates — it must NOT be inherited
113
+ * as true. (Sub-agent git-tracked edits are still captured by the turn-boundary
114
+ * git diff, which is backend-agnostic.)
115
+ */
116
+ readonly captureIgnored?: boolean;
117
+ /**
118
+ * Sink for a gitignored path hard-blocked as secret-like (DD-E): the write is
119
+ * never applied and never captured, and the turn boundary reads these paths to
120
+ * author a `DIFF_UNREVIEWABLE` change entry (path only — the name is not the
121
+ * secret; the CONTENT never leaves the workspace). Absent ⇒ nothing recorded.
122
+ */
123
+ readonly recordBlockedSecret?: (rawPath: string) => void;
36
124
  }
37
125
 
38
126
  interface ApprovalDecision {
@@ -40,36 +128,25 @@ interface ApprovalDecision {
40
128
  readonly comment?: string;
41
129
  }
42
130
 
43
- const SAFE_PLATFORM_TOOLS = new Set([
44
- "read", "read_file",
45
- "ls", "list_directory",
46
- "glob", "find_files",
47
- "grep", "search", "ripgrep",
48
- "think",
49
- ]);
50
-
51
- const DANGEROUS_PLATFORM_TOOLS = new Map<string, string>([
52
- ["write", "Write file: {{args.path}}"],
53
- ["write_file", "Write file: {{args.path}}"],
54
- ["edit", "Edit file: {{args.path}}"],
55
- ["edit_file", "Edit file: {{args.path}}"],
56
- ["create", "Create file: {{args.path}}"],
57
- ["create_file", "Create file: {{args.path}}"],
58
- ["delete", "Delete: {{args.path}}"],
59
- ["delete_file", "Delete file: {{args.path}}"],
60
- ["execute", "Execute command: {{args.command}}"],
61
- ["shell", "Execute shell command: {{args.command}}"],
62
- ["str_replace_editor", "Edit file: {{args.path}}"],
63
- ]);
131
+ // Approval-message template per mutating category. Keyed by category (not raw
132
+ // tool name) so every alias of a mutation renders one message; placeholders
133
+ // resolve against the deep-agent stream arg shape (`path`/`command`). Mirrors
134
+ // the Cursor side's CATEGORY_APPROVAL_MESSAGE.
135
+ const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
136
+ write: "Write file: {{args.path}}",
137
+ delete: "Delete: {{args.path}}",
138
+ shell: "Execute command: {{args.command}}",
139
+ };
64
140
 
65
141
  export function createApprovalGateMiddleware(
66
142
  config: ApprovalGateConfig,
67
143
  ): StigmerMiddleware {
68
- const { policies, autoApproveAll, toolServerMap } = config;
144
+ const { policies, toolServerMap } = config;
145
+ const leasedCategories = config.leasedCategories ?? EMPTY_CATEGORY_SET;
69
146
 
70
- if (autoApproveAll) {
71
- return { name: "ApprovalGateMiddleware" };
72
- }
147
+ // No global-bypass early return: a pre-armed spec.auto_approve_all means the
148
+ // gate is never even installed (setup.ts builds this config only when not
149
+ // global). Scoped leases keep the gate active so non-leased actions still gate.
73
150
 
74
151
  return {
75
152
  name: "ApprovalGateMiddleware",
@@ -78,15 +155,65 @@ export function createApprovalGateMiddleware(
78
155
  const { toolCall } = request;
79
156
  const toolName = toolCall.name;
80
157
  const serverSlug = toolServerMap.get(toolName) ?? "";
158
+ const category = toolApprovalCategory(toolName);
159
+
160
+ // Capture mode (git workspaces): a git-tracked built-in file mutation flows
161
+ // during the turn and is reviewed post-hoc via the file_review ledger (the
162
+ // apply-then-review model). shell and MCP tools (serverSlug present) are
163
+ // never bypassed here.
164
+ if (
165
+ config.fileCaptureMode &&
166
+ config.isCapturablePath &&
167
+ !serverSlug &&
168
+ (category === "write" || category === "delete")
169
+ ) {
170
+ const path = extractFilePath(toolCall.args);
171
+ if (path !== null) {
172
+ if (await config.isCapturablePath(path)) {
173
+ // Git-tracked: flows and is reviewed post-hoc by the git substrate.
174
+ emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", "file_capture");
175
+ return await handler(request);
176
+ }
177
+ // Gitignored path. Only the PARENT gate (captureIgnored) routes it into
178
+ // CAS capture; sub-agent gates fall through to the interrupt gate below,
179
+ // exactly as before (their backends are not CAS-observed).
180
+ if (config.captureIgnored) {
181
+ if (isSecretLikePath(path)) {
182
+ // DD-E fail-closed: a secret-like gitignored edit is NEVER applied
183
+ // and NEVER captured. Record it so the turn boundary authors a
184
+ // DIFF_UNREVIEWABLE entry (blocking approval); nothing is written.
185
+ config.recordBlockedSecret?.(path);
186
+ return new ToolMessage({
187
+ content:
188
+ `Tool '${toolName}' was blocked for security: '${path}' matches a ` +
189
+ `secret-like path Stigmer will not capture for review. Nothing was written.`,
190
+ tool_call_id: toolCall.id,
191
+ name: toolName,
192
+ });
193
+ }
194
+ if (category === "write") {
195
+ // Non-secret gitignored write/edit: flows (apply-then-review). The
196
+ // CAS observer already holds its before-bytes and the turn boundary
197
+ // captures it into CAS. (A gitignored delete has no backend capture
198
+ // path, so it falls through to the interrupt gate.)
199
+ emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", "file_capture");
200
+ return await handler(request);
201
+ }
202
+ }
203
+ }
204
+ }
81
205
 
82
206
  const requirement = resolveToolApproval(
83
207
  toolName,
84
208
  serverSlug,
85
209
  toolCall.args,
86
210
  policies,
211
+ leasedCategories,
87
212
  );
88
213
 
89
214
  if (!requirement.requiresApproval) {
215
+ // Backing authorization: the classifier/policy auto-approved this tool.
216
+ emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", requirement.source);
90
217
  return await handler(request);
91
218
  }
92
219
 
@@ -95,6 +222,10 @@ export function createApprovalGateMiddleware(
95
222
  tool_name: toolName,
96
223
  mcp_server_slug: serverSlug,
97
224
  message: requirement.message,
225
+ // Carry the gate's provenance verdict through the interrupt so the
226
+ // reinvocation that seeds the WAITING_APPROVAL tool call (index.ts) can
227
+ // persist ToolCall.approval_policy_source without re-deriving it.
228
+ policy_source: requirement.source,
98
229
  };
99
230
 
100
231
  const response = interrupt(approvalRequest) as ApprovalDecision;
@@ -106,6 +237,8 @@ export function createApprovalGateMiddleware(
106
237
  ).toString().toLowerCase();
107
238
 
108
239
  if (action === "approve") {
240
+ // Backing authorization: the user approved THIS interrupted call.
241
+ emitExecutionReceipt(config, toolCall, serverSlug, category, "approval", requirement.source);
109
242
  return await handler(request);
110
243
  }
111
244
 
@@ -143,6 +276,8 @@ export function createApprovalGateMiddleware(
143
276
  interface ApprovalRequirement {
144
277
  readonly requiresApproval: boolean;
145
278
  readonly message: string;
279
+ /** Which policy layer determined this verdict — stamped on the shadow receipt. */
280
+ readonly source: PolicySource;
146
281
  }
147
282
 
148
283
  function resolveToolApproval(
@@ -150,30 +285,104 @@ function resolveToolApproval(
150
285
  serverSlug: string,
151
286
  args: Record<string, unknown>,
152
287
  policies: ReadonlyMap<string, MergedToolPolicy>,
288
+ leasedCategories: ReadonlySet<ToolApprovalCategory>,
153
289
  ): ApprovalRequirement {
154
290
  if (serverSlug) {
291
+ // MCP tools stay governed by the connect-flow classifier + four-level policy
292
+ // chain. The policy map carries only the tools that REQUIRE approval, so an
293
+ // absent entry means the classifier already auto-approved it — fail-OPEN is
294
+ // correct here (a fail-closed default would re-gate everything the classifier
295
+ // cleared). This is deliberately NOT changed by the built-in fail-closed flip.
296
+ //
297
+ // A server-scoped lease surfaces here as an absent entry too: the leased
298
+ // server's tools are dropped from `policies` upstream (mergeApprovalPolicies),
299
+ // so a lease-cleared MCP tool takes this same auto-approve path. (Its shadow
300
+ // receipt therefore reads classifier_default rather than approval_lease; a
301
+ // faithful per-server lease provenance would need the lease set threaded here
302
+ // and is deferred with the rest of the persisted-receipt work.)
155
303
  const key = `${serverSlug}/${toolName}`;
156
304
  const policy = policies.get(key);
157
305
  if (policy) {
158
306
  return {
159
307
  requiresApproval: policy.requiresApproval,
160
308
  message: resolveApprovalMessage(policy.approvalMessage, toolName, args),
309
+ source: policy.source,
161
310
  };
162
311
  }
163
- return { requiresApproval: false, message: "" };
164
- }
165
-
166
- if (SAFE_PLATFORM_TOOLS.has(toolName)) {
167
- return { requiresApproval: false, message: "" };
312
+ // Absent = cleared by the MCP four-level chain (classifier base) or by a
313
+ // server-scoped lease (dropped upstream).
314
+ return { requiresApproval: false, message: "", source: "classifier_default" };
168
315
  }
169
316
 
170
- const dangerousTemplate = DANGEROUS_PLATFORM_TOOLS.get(toolName);
171
- if (dangerousTemplate) {
317
+ // Built-in/platform tool: gate exactly the mutating categories via the shared
318
+ // classifier. Fail-CLOSED for the mutating set — any built-in classifyTool
319
+ // deems write/edit/delete/shell requires approval. Read-only and unclassified
320
+ // built-ins are not mutating, so they remain fail-open.
321
+ const category = toolApprovalCategory(toolName);
322
+ if (category) {
323
+ // Run-lifetime category lease: the user chose "approve all <category>"
324
+ // earlier in this run, so every built-in of that category is auto-approved
325
+ // for the rest of the run (the scoped successor to auto-approve-all).
326
+ if (leasedCategories.has(category)) {
327
+ return { requiresApproval: false, message: "", source: "approval_lease" };
328
+ }
172
329
  return {
173
330
  requiresApproval: true,
174
- message: resolveApprovalMessage(dangerousTemplate, toolName, args),
331
+ message: resolveApprovalMessage(CATEGORY_APPROVAL_MESSAGE[category], toolName, args),
332
+ source: "builtin_category",
175
333
  };
176
334
  }
177
335
 
178
- return { requiresApproval: false, message: "" };
336
+ return { requiresApproval: false, message: "", source: "builtin_category" };
337
+ }
338
+
339
+ /** Shared empty set so a config without leases allocates nothing per call. */
340
+ const EMPTY_CATEGORY_SET: ReadonlySet<ToolApprovalCategory> = new Set();
341
+
342
+ type AuthorizationSource = "auto_approve" | "approval";
343
+
344
+ /**
345
+ * Emit the shadow ExecutionReceipt when the gateway authorizes a side effect.
346
+ *
347
+ * Scope: only side-effecting actions are recorded — a mutating built-in (a
348
+ * non-empty {@link ToolApprovalCategory}) or any MCP tool (`serverSlug` present).
349
+ * Read-only built-ins are not side effects, so recording them would only dilute
350
+ * the signal. The receipt is a structured log carrying the action's HMAC
351
+ * fingerprint and the authorization source; it is never persisted and crosses no
352
+ * wire (no proto). The fingerprint is omitted (empty) when no per-execution key
353
+ * was supplied (tests / no-secret paths).
354
+ */
355
+ function emitExecutionReceipt(
356
+ config: ApprovalGateConfig,
357
+ toolCall: { id: string; name: string; args: Record<string, unknown> },
358
+ serverSlug: string,
359
+ category: ToolApprovalCategory | undefined,
360
+ source: AuthorizationSource,
361
+ policySource: PolicySource,
362
+ ): void {
363
+ if (!category && !serverSlug) return;
364
+
365
+ const fingerprint = config.fingerprintKey
366
+ ? computeApprovalFingerprint(config.fingerprintKey, {
367
+ toolName: toolCall.name,
368
+ mcpServerSlug: serverSlug,
369
+ args: toolCall.args,
370
+ })
371
+ : "";
372
+
373
+ console.log(
374
+ "[hitl-gateway] receipt " +
375
+ JSON.stringify({
376
+ executionId: config.executionId ?? "",
377
+ toolCallId: toolCall.id,
378
+ toolName: toolCall.name,
379
+ mcpServerSlug: serverSlug,
380
+ category: category ?? "",
381
+ authorization: source,
382
+ policySource,
383
+ policyEngineVersion: POLICY_ENGINE_VERSION,
384
+ fingerprint,
385
+ substrate: "deep-agent",
386
+ }),
387
+ );
179
388
  }
@@ -94,11 +94,11 @@ export interface OtelSpansConfig {
94
94
  readonly toolServerMap: ReadonlyMap<string, string>;
95
95
  }
96
96
 
97
- export interface ApprovalGateConfig {
98
- readonly policies: ReadonlyMap<string, import("../shared/approval-policy.js").MergedToolPolicy>;
99
- readonly autoApproveAll: boolean;
100
- readonly toolServerMap: ReadonlyMap<string, string>;
101
- }
97
+ // Single source of truth lives in approval-gate.ts (the gate consumes it);
98
+ // imported as a type (no runtime cycle) and re-exported so existing importers of
99
+ // `ApprovalGateConfig` from this module keep working.
100
+ import type { ApprovalGateConfig } from "./approval-gate.js";
101
+ export type { ApprovalGateConfig };
102
102
 
103
103
  /**
104
104
  * Top-level configuration for buildMiddlewareStack().
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ normalizeActivityInput,
4
+ type ExecuteActivityInput,
5
+ } from "../activity-input.js";
6
+
7
+ describe("normalizeActivityInput — dual-shape activity boundary", () => {
8
+ it("reads the new typed object (snake_case wire keys from Go/Java)", () => {
9
+ const wire: ExecuteActivityInput = {
10
+ execution_id: "exec-1",
11
+ thread_id: "thread-1",
12
+ invoker_identity_account_id: "acct-1",
13
+ turn_seq: 2,
14
+ };
15
+ expect(normalizeActivityInput(wire)).toEqual({
16
+ executionId: "exec-1",
17
+ threadId: "thread-1",
18
+ turnSeq: 2,
19
+ });
20
+ });
21
+
22
+ it("reads the legacy positional args (old Go control plane: 2 args)", () => {
23
+ expect(normalizeActivityInput("exec-2", "thread-2")).toEqual({
24
+ executionId: "exec-2",
25
+ threadId: "thread-2",
26
+ turnSeq: 0,
27
+ });
28
+ });
29
+
30
+ it("ignores a legacy 3rd positional arg (old Java sent invokerId as arg2)", () => {
31
+ // The runner only declares (arg0, arg1); a 3rd positional arg is dropped by
32
+ // JS, so the old Java (executionId, threadId, invokerId) call still works.
33
+ expect(normalizeActivityInput("exec-3", "thread-3")).toEqual({
34
+ executionId: "exec-3",
35
+ threadId: "thread-3",
36
+ turnSeq: 0,
37
+ });
38
+ });
39
+
40
+ it("treats an empty thread_id (first run / new harness state) as empty", () => {
41
+ expect(
42
+ normalizeActivityInput({
43
+ execution_id: "exec-4",
44
+ thread_id: "",
45
+ invoker_identity_account_id: "acct-4",
46
+ }),
47
+ ).toEqual({ executionId: "exec-4", threadId: "", turnSeq: 0 });
48
+
49
+ expect(normalizeActivityInput("exec-5")).toEqual({
50
+ executionId: "exec-5",
51
+ threadId: "",
52
+ turnSeq: 0,
53
+ });
54
+ });
55
+
56
+ it("defaults turn_seq to 0 when the object omits it (first turn / older control plane)", () => {
57
+ expect(
58
+ normalizeActivityInput({
59
+ execution_id: "exec-7",
60
+ thread_id: "thread-7",
61
+ invoker_identity_account_id: "acct-7",
62
+ }),
63
+ ).toEqual({ executionId: "exec-7", threadId: "thread-7", turnSeq: 0 });
64
+ });
65
+
66
+ it("deserializes the exact JSON shape the control planes emit", () => {
67
+ // Mirrors the bytes the Go struct / Java record serialize to (snake_case),
68
+ // including the additive turn_seq the cutover threads through.
69
+ const fromWire = JSON.parse(
70
+ '{"execution_id":"exec-6","thread_id":"thread-6","invoker_identity_account_id":"acct-6","turn_seq":3}',
71
+ ) as ExecuteActivityInput;
72
+ expect(normalizeActivityInput(fromWire)).toEqual({
73
+ executionId: "exec-6",
74
+ threadId: "thread-6",
75
+ turnSeq: 3,
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ canonicalJson,
6
+ canonicalToolActionJson,
7
+ canonicalizeToolAction,
8
+ type ToolActionInput,
9
+ } from "../approval-canonicalize.js";
10
+
11
+ interface Vector {
12
+ name: string;
13
+ input: ToolActionInput;
14
+ expected: string;
15
+ }
16
+
17
+ // Single source of truth, shared with the future Go/Java implementations.
18
+ const vectorsPath = fileURLToPath(
19
+ new URL("../../../../../../apis/testdata/hitl/canonicalization/vectors.json", import.meta.url),
20
+ );
21
+ const corpus = JSON.parse(readFileSync(vectorsPath, "utf-8")) as { vectors: Vector[] };
22
+
23
+ describe("approval canonicalization vector corpus", () => {
24
+ it("loads a non-trivial corpus", () => {
25
+ expect(corpus.vectors.length).toBeGreaterThanOrEqual(10);
26
+ });
27
+
28
+ for (const v of corpus.vectors) {
29
+ it(`vector: ${v.name}`, () => {
30
+ expect(canonicalToolActionJson(v.input)).toBe(v.expected);
31
+ });
32
+ }
33
+ });
34
+
35
+ describe("canonicalJson", () => {
36
+ it("sorts object keys by UTF-16 code unit", () => {
37
+ expect(canonicalJson({ b: 1, a: 2, A: 3 })).toBe('{"A":3,"a":2,"b":1}');
38
+ });
39
+
40
+ it("preserves array order", () => {
41
+ expect(canonicalJson([3, 1, 2])).toBe("[3,1,2]");
42
+ });
43
+
44
+ it("drops undefined-valued keys", () => {
45
+ expect(canonicalJson({ a: 1, b: undefined })).toBe('{"a":1}');
46
+ });
47
+
48
+ it("rejects non-integer numbers (outside the canonical domain)", () => {
49
+ expect(() => canonicalJson({ x: 1.5 })).toThrow(/non-integer/);
50
+ });
51
+
52
+ it("rejects non-finite numbers", () => {
53
+ expect(() => canonicalJson({ x: Infinity })).toThrow(/non-finite/);
54
+ });
55
+ });
56
+
57
+ describe("canonicalizeToolAction normalization", () => {
58
+ it("is idempotent (canonicalizing the canonical form is a fixed point)", () => {
59
+ const input: ToolActionInput = {
60
+ toolName: "Write",
61
+ paths: ["/ws/src/b.ts", "/ws/src/a.ts"],
62
+ workspaceRoot: "/ws",
63
+ args: { flag: true },
64
+ };
65
+ const once = canonicalToolActionJson(input);
66
+ const twice = canonicalToolActionJson(input);
67
+ expect(twice).toBe(once);
68
+ });
69
+
70
+ it("redacts secret values to a stable digest without cleartext", () => {
71
+ const input: ToolActionInput = {
72
+ toolName: "create_secret",
73
+ args: { name: "db", value: "super-secret-password" },
74
+ secretKeys: ["value"],
75
+ };
76
+ const out = canonicalizeToolAction(input);
77
+ const redacted = out.args.value as string;
78
+
79
+ expect(redacted).toMatch(/^sha256:[0-9a-f]{64}$/);
80
+ expect(redacted).not.toContain("super-secret-password");
81
+ expect(canonicalToolActionJson(input)).not.toContain("super-secret-password");
82
+ });
83
+
84
+ it("redaction is stable for the same secret and distinct for different secrets", () => {
85
+ const base = (value: string): ToolActionInput => ({
86
+ toolName: "t",
87
+ args: { value },
88
+ secretKeys: ["value"],
89
+ });
90
+ const a1 = canonicalizeToolAction(base("secret-a")).args.value;
91
+ const a2 = canonicalizeToolAction(base("secret-a")).args.value;
92
+ const b = canonicalizeToolAction(base("secret-b")).args.value;
93
+
94
+ expect(a1).toBe(a2);
95
+ expect(a1).not.toBe(b);
96
+ });
97
+
98
+ it("makes a workspace-absolute path relative and normalizes separators", () => {
99
+ const out = canonicalizeToolAction({
100
+ toolName: "Write",
101
+ paths: ["C:\\ws\\src\\main.ts"],
102
+ workspaceRoot: "C:\\ws",
103
+ });
104
+ expect(out.paths).toEqual(["src/main.ts"]);
105
+ });
106
+ });