@stigmer/runner 3.0.9-dev.20260615153829 → 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 (329) 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 +16 -3
  74. package/dist/activities/execute-deep-agent/hitl.js +11 -3
  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 +393 -33
  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 +17 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +44 -3
  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 +32 -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 +284 -0
  243. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
  244. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  245. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  246. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  247. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
  248. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  249. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  251. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  252. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  253. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  254. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  255. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  256. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  257. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  258. package/src/activities/execute-deep-agent/hitl.ts +23 -7
  259. package/src/activities/execute-deep-agent/index.ts +463 -40
  260. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  261. package/src/activities/execute-deep-agent/setup.ts +223 -125
  262. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  263. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  264. package/src/activities/execute-deep-agent/status-builder.ts +63 -8
  265. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  266. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  267. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  268. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  269. package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
  270. package/src/claimcheck/payload-codec.ts +8 -8
  271. package/src/client/stigmer-client.ts +9 -1
  272. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  273. package/src/middleware/approval-gate.ts +247 -38
  274. package/src/middleware/types.ts +5 -5
  275. package/src/shared/__tests__/activity-input.test.ts +78 -0
  276. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  277. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  278. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  279. package/src/shared/__tests__/args-preview.test.ts +78 -0
  280. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  281. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  282. package/src/shared/__tests__/file-change.test.ts +85 -0
  283. package/src/shared/__tests__/file-tools.test.ts +90 -0
  284. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  285. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  286. package/src/shared/__tests__/model-client.test.ts +162 -0
  287. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  288. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  289. package/src/shared/__tests__/status-offload.test.ts +573 -16
  290. package/src/shared/__tests__/status.test.ts +4 -5
  291. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  292. package/src/shared/__tests__/tool-row.test.ts +221 -0
  293. package/src/shared/activity-input.ts +57 -0
  294. package/src/shared/approval-canonicalize.ts +159 -0
  295. package/src/shared/approval-fingerprint.ts +148 -0
  296. package/src/shared/approval-policy.ts +303 -27
  297. package/src/shared/args-preview.ts +98 -0
  298. package/src/shared/artifact-storage.ts +62 -11
  299. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  300. package/src/shared/file-change.ts +64 -0
  301. package/src/shared/file-tools.ts +169 -0
  302. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  303. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  304. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  305. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  306. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  307. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  308. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  309. package/src/shared/filereview/capture.ts +727 -0
  310. package/src/shared/filereview/cas-substrate.ts +401 -0
  311. package/src/shared/filereview/digest.ts +83 -0
  312. package/src/shared/filereview/events.ts +449 -0
  313. package/src/shared/filereview/git-substrate.ts +555 -0
  314. package/src/shared/filereview/index.ts +60 -0
  315. package/src/shared/filereview/secret-paths.ts +121 -0
  316. package/src/shared/fingerprint-secret.ts +53 -0
  317. package/src/shared/model-client.ts +122 -0
  318. package/src/shared/plan-artifact.ts +0 -2
  319. package/src/shared/status-offload.ts +433 -77
  320. package/src/shared/status.ts +13 -0
  321. package/src/shared/tool-kind.ts +33 -0
  322. package/src/shared/tool-row.ts +135 -0
  323. package/src/shared/workspace/platform-dir.ts +41 -2
  324. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  325. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  326. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  327. package/src/workflows/call-agent-orchestrator.ts +53 -6
  328. package/src/workflows/connect-mcp-server.ts +179 -24
  329. package/src/workflows/types.ts +8 -0
@@ -67,6 +67,20 @@ describe("LocalArtifactStorage", () => {
67
67
  });
68
68
  });
69
69
 
70
+ describe("download", () => {
71
+ it("round-trips uploaded content byte-exact", async () => {
72
+ await storage.upload("exec-1/file.txt", Buffer.from("round-trip"));
73
+ const got = await storage.download("exec-1/file.txt");
74
+ expect(got.toString("utf-8")).toBe("round-trip");
75
+ });
76
+
77
+ it("throws a key-scoped error for a missing key", async () => {
78
+ await expect(storage.download("missing.txt")).rejects.toThrow(
79
+ /Artifact not found for key 'missing\.txt'/,
80
+ );
81
+ });
82
+ });
83
+
70
84
  describe("exists", () => {
71
85
  it("returns true after upload", async () => {
72
86
  await storage.upload("present.txt", Buffer.from("data"));
@@ -140,24 +154,62 @@ describe("ProxyArtifactStorage", () => {
140
154
  expect(url).toBe("https://r2.example.com/download/key.txt");
141
155
  });
142
156
 
143
- it("exists returns true when presign succeeds", async () => {
157
+ it("download resolves a presigned URL then fetches the bytes", async () => {
144
158
  const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
145
159
 
146
- globalThis.fetch = vi.fn().mockResolvedValueOnce({
147
- ok: true,
148
- json: async () => ({ url: "https://r2.example.com/dl" }),
149
- } as any);
160
+ globalThis.fetch = vi.fn()
161
+ .mockResolvedValueOnce({
162
+ ok: true,
163
+ json: async () => ({ url: "https://r2.example.com/dl/key.txt" }),
164
+ } as any)
165
+ .mockResolvedValueOnce({
166
+ ok: true,
167
+ arrayBuffer: async () => new TextEncoder().encode("proxy-bytes").buffer,
168
+ } as any);
169
+
170
+ const got = await storage.download("key.txt");
171
+ expect(got.toString("utf-8")).toBe("proxy-bytes");
172
+ });
173
+
174
+ it("download throws with HTTP status and key on a failed fetch", async () => {
175
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
176
+
177
+ globalThis.fetch = vi.fn()
178
+ .mockResolvedValueOnce({
179
+ ok: true,
180
+ json: async () => ({ url: "https://r2.example.com/dl/missing.txt" }),
181
+ } as any)
182
+ .mockResolvedValueOnce({
183
+ ok: false,
184
+ status: 404,
185
+ text: async () => "not found",
186
+ } as any);
187
+
188
+ await expect(storage.download("missing.txt")).rejects.toThrow(
189
+ /Artifact download failed \(HTTP 404\) for key 'missing\.txt'/,
190
+ );
191
+ });
192
+
193
+ it("exists returns true when the object is present (presign + ranged GET)", async () => {
194
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
195
+
196
+ // Two-step probe: presign mints the URL, then a ranged GET hits the object.
197
+ // Presign success alone is NOT existence — the object fetch is authoritative.
198
+ globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
199
+ const url = typeof input === "string" ? input : input.toString();
200
+ if (url.includes("presigned-download-url")) {
201
+ return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
202
+ }
203
+ return new Response(Buffer.from("x"), { status: 206 });
204
+ }) as typeof fetch;
150
205
 
151
206
  expect(await storage.exists("key.txt")).toBe(true);
152
207
  });
153
208
 
154
- it("exists returns false when presign fails", async () => {
209
+ it("exists returns false when the presign endpoint fails", async () => {
155
210
  const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
156
211
 
157
- globalThis.fetch = vi.fn().mockResolvedValueOnce({
158
- ok: false,
159
- status: 404,
160
- } as any);
212
+ globalThis.fetch = vi.fn(async () => new Response("nope", { status: 404 })) as typeof fetch;
161
213
 
162
214
  expect(await storage.exists("missing.txt")).toBe(false);
163
215
  });
@@ -65,6 +65,38 @@ describe("LocalArtifactStorage", () => {
65
65
  const written = await readFile(join(tempDir, key));
66
66
  expect(written.toString()).toBe("v2");
67
67
  });
68
+
69
+ it("downloads uploaded text bytes byte-exact (inverse of upload)", async () => {
70
+ const key = "artifacts/exec-1/report.txt";
71
+ await storage.upload(key, Buffer.from("hello world"));
72
+ const got = await storage.download(key);
73
+ expect(got.toString()).toBe("hello world");
74
+ });
75
+
76
+ it("downloads binary bytes (incl. NUL) byte-exact", async () => {
77
+ const key = "artifacts/exec-1/blob.bin";
78
+ const bytes = Buffer.from([0x00, 0x01, 0xff, 0xfe, 0x00, 0x89, 0x50]);
79
+ await storage.upload(key, bytes);
80
+ const got = await storage.download(key);
81
+ expect(Buffer.compare(got, bytes)).toBe(0);
82
+ });
83
+
84
+ it("throws a key-scoped error when downloading a missing key", async () => {
85
+ await expect(storage.download("artifacts/exec-1/missing.bin")).rejects.toThrow(
86
+ /Artifact not found for key 'artifacts\/exec-1\/missing\.bin'/,
87
+ );
88
+ });
89
+
90
+ it("reads directly off disk even with an unreachable serve URL (no HTTP dependency)", async () => {
91
+ // A bogus, unroutable serve base: if `download` fetched over HTTP this would
92
+ // hang/throw. Reading straight off disk proves the runner's read-back does
93
+ // not depend on the serve URL being set or reachable.
94
+ const s = new LocalArtifactStorage(tempDir, "http://127.0.0.1:0");
95
+ const key = "artifacts/exec-1/offline.txt";
96
+ await s.upload(key, Buffer.from("served-from-disk"));
97
+ const got = await s.download(key);
98
+ expect(got.toString()).toBe("served-from-disk");
99
+ });
68
100
  });
69
101
 
70
102
  // ── ProxyArtifactStorage ─────────────────────────────────────────────
@@ -126,7 +158,7 @@ describe("ProxyArtifactStorage", () => {
126
158
  }
127
159
  // Build a real Headers from what the runner passed, so concatenation /
128
160
  // case-folding behavior matches a live PUT.
129
- resolve(new Headers(init?.headers as HeadersInit));
161
+ resolve(new Headers(init?.headers as ConstructorParameters<typeof Headers>[0]));
130
162
  return new Response(null, { status: 200 });
131
163
  }) as typeof fetch;
132
164
  });
@@ -219,32 +251,112 @@ describe("ProxyArtifactStorage", () => {
219
251
  expect(url).toBe("https://r2.example.com/dl");
220
252
  });
221
253
 
222
- it("exists returns true on 200", async () => {
223
- globalThis.fetch = vi.fn(async () =>
224
- new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 }),
225
- ) as typeof fetch;
254
+ it("downloads by resolving a presigned URL then fetching it", async () => {
255
+ globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
256
+ const url = typeof input === "string" ? input : input.toString();
257
+ if (url.includes("presigned-download-url")) {
258
+ return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
259
+ }
260
+ if (url === "https://r2.example.com/dl") {
261
+ return new Response(Buffer.from("payload-bytes"), { status: 200 });
262
+ }
263
+ return new Response("not found", { status: 404 });
264
+ }) as typeof fetch;
265
+
266
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
267
+ const got = await storage.download("artifacts/exec-1/f.txt");
268
+ expect(got.toString()).toBe("payload-bytes");
269
+ });
270
+
271
+ it("throws with the HTTP status and key when the download fetch fails", async () => {
272
+ globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
273
+ const url = typeof input === "string" ? input : input.toString();
274
+ if (url.includes("presigned-download-url")) {
275
+ return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
276
+ }
277
+ return new Response("gone", { status: 404 });
278
+ }) as typeof fetch;
279
+
280
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
281
+ await expect(storage.download("artifacts/exec-1/missing.txt")).rejects.toThrow(
282
+ /Artifact download failed \(HTTP 404\) for key 'artifacts\/exec-1\/missing\.txt'/,
283
+ );
284
+ });
285
+
286
+ // exists() is a TWO-step probe: presign the download URL, then a 1-byte ranged
287
+ // GET against the object. The presign endpoint mints a URL for ANY key, so the
288
+ // OBJECT fetch — not the presign — is the source of truth for existence.
289
+ function mockProxyFetch(objectResponse: () => Response) {
290
+ globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
291
+ const url = typeof input === "string" ? input : input.toString();
292
+ if (url.includes("presigned-download-url")) {
293
+ return new Response(JSON.stringify({ url: "https://r2.example.com/obj" }), { status: 200 });
294
+ }
295
+ return objectResponse();
296
+ }) as typeof fetch;
297
+ }
226
298
 
299
+ it("exists returns true when the object GET is 200", async () => {
300
+ mockProxyFetch(() => new Response(Buffer.from("x"), { status: 200 }));
227
301
  const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
228
302
  expect(await storage.exists("key")).toBe(true);
229
303
  });
230
304
 
231
- it("exists returns false on 404", async () => {
232
- globalThis.fetch = vi.fn(async () =>
233
- new Response("not found", { status: 404 }),
234
- ) as typeof fetch;
305
+ it("exists returns true when the object GET is 206 (ranged)", async () => {
306
+ mockProxyFetch(() => new Response(Buffer.from("x"), { status: 206 }));
307
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
308
+ expect(await storage.exists("key")).toBe(true);
309
+ });
235
310
 
311
+ it("exists returns true on 416 (0-byte object, range unsatisfiable but present)", async () => {
312
+ mockProxyFetch(() => new Response("", { status: 416 }));
313
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
314
+ expect(await storage.exists("key")).toBe(true);
315
+ });
316
+
317
+ it("exists returns false when presign succeeds but the object 404s (regression: presign != existence)", async () => {
318
+ // The exact proxy bug: the presign endpoint mints a URL for a nonexistent key;
319
+ // only the object fetch reveals it is absent. The old exists() returned true
320
+ // here (presign.ok), which crashed the file-review reconcile on the R2 404.
321
+ mockProxyFetch(() => new Response("<Error><Code>NoSuchKey</Code></Error>", { status: 404 }));
236
322
  const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
237
323
  expect(await storage.exists("key")).toBe(false);
238
324
  });
239
325
 
240
- it("exists returns false on network error", async () => {
326
+ it("exists probes the object with a 1-byte ranged GET (never a HEAD, which breaks the presign signature)", async () => {
327
+ const seen: Array<{ url: string; range: string | null; method: string }> = [];
328
+ globalThis.fetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
329
+ const url = typeof input === "string" ? input : input.toString();
330
+ if (url.includes("presigned-download-url")) {
331
+ return new Response(JSON.stringify({ url: "https://r2.example.com/obj" }), { status: 200 });
332
+ }
333
+ seen.push({
334
+ url,
335
+ range: new Headers(init?.headers).get("range"),
336
+ method: (init?.method ?? "GET").toUpperCase(),
337
+ });
338
+ return new Response(Buffer.from("x"), { status: 206 });
339
+ }) as typeof fetch;
340
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
341
+ await storage.exists("key");
342
+ expect(seen).toEqual([{ url: "https://r2.example.com/obj", range: "bytes=0-0", method: "GET" }]);
343
+ });
344
+
345
+ it("exists returns false when the presign endpoint is unreachable", async () => {
241
346
  globalThis.fetch = vi.fn(async () => {
242
347
  throw new Error("network unreachable");
243
348
  }) as typeof fetch;
244
-
245
349
  const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
246
350
  expect(await storage.exists("key")).toBe(false);
247
351
  });
352
+
353
+ it("exists throws on an unexpected object status (a fault, not an existence answer)", async () => {
354
+ mockProxyFetch(() => new Response("boom", { status: 500 }));
355
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
356
+ await expect(storage.exists("key")).rejects.toThrow(
357
+ /Artifact existence check failed \(HTTP 500\) for key 'key'/,
358
+ );
359
+ });
248
360
  });
249
361
 
250
362
  // ── Factory ──────────────────────────────────────────────────────────
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ looksBinary,
4
+ bytesLookBinary,
5
+ resolveWorkspacePath,
6
+ } from "../file-change.js";
7
+
8
+ describe("looksBinary", () => {
9
+ it("flags content containing a NUL byte", () => {
10
+ expect(looksBinary("PNG\u0000\u0000IHDR")).toBe(true);
11
+ });
12
+
13
+ it("does not flag plain UTF-8 text, including unicode and empty", () => {
14
+ expect(looksBinary("hello world")).toBe(false);
15
+ expect(looksBinary("héllo — 世界 \n\t")).toBe(false);
16
+ expect(looksBinary("")).toBe(false);
17
+ });
18
+ });
19
+
20
+ describe("bytesLookBinary", () => {
21
+ it("flags raw bytes containing a NUL", () => {
22
+ expect(bytesLookBinary(Buffer.from([0x89, 0x50, 0x00, 0x4e]))).toBe(true);
23
+ expect(bytesLookBinary(new Uint8Array([0x00]))).toBe(true);
24
+ });
25
+
26
+ it("does not flag NUL-free bytes (text, or high bytes without a NUL)", () => {
27
+ expect(bytesLookBinary(Buffer.from("hello world", "utf8"))).toBe(false);
28
+ expect(bytesLookBinary(Buffer.from("héllo — 世界", "utf8"))).toBe(false);
29
+ expect(bytesLookBinary(Buffer.from([0xff, 0xfe, 0x80]))).toBe(false);
30
+ expect(bytesLookBinary(Buffer.alloc(0))).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("resolveWorkspacePath", () => {
35
+ const root = "/work/space";
36
+
37
+ describe("native (virtualRoot=true)", () => {
38
+ it("strips a leading slash and joins under the root", () => {
39
+ // "/src/app.ts" is the deepagents virtual-root convention; the display
40
+ // path drops the leading slash and the absolute path lives under root.
41
+ expect(resolveWorkspacePath("/src/app.ts", root, true)).toEqual({
42
+ path: "src/app.ts",
43
+ absolutePath: "/work/space/src/app.ts",
44
+ });
45
+ });
46
+
47
+ it("passes a bare relative path through and joins it", () => {
48
+ expect(resolveWorkspacePath("src/app.ts", root, true)).toEqual({
49
+ path: "src/app.ts",
50
+ absolutePath: "/work/space/src/app.ts",
51
+ });
52
+ });
53
+
54
+ it("normalizes a leading ./ segment", () => {
55
+ expect(resolveWorkspacePath("./README.md", root, true)).toEqual({
56
+ path: "README.md",
57
+ absolutePath: "/work/space/README.md",
58
+ });
59
+ });
60
+ });
61
+
62
+ describe("cursor (virtualRoot=false)", () => {
63
+ it("makes an absolute path under the root relative for display", () => {
64
+ expect(resolveWorkspacePath("/work/space/src/app.ts", root, false)).toEqual({
65
+ path: "src/app.ts",
66
+ absolutePath: "/work/space/src/app.ts",
67
+ });
68
+ });
69
+
70
+ it("displays an absolute path outside the root as-is (never an escaping ../)", () => {
71
+ const outside = "/etc/hosts";
72
+ expect(resolveWorkspacePath(outside, root, false)).toEqual({
73
+ path: outside,
74
+ absolutePath: outside,
75
+ });
76
+ });
77
+
78
+ it("treats a relative path the same as the virtual case", () => {
79
+ expect(resolveWorkspacePath("src/app.ts", root, false)).toEqual({
80
+ path: "src/app.ts",
81
+ absolutePath: "/work/space/src/app.ts",
82
+ });
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Unit tests for the unified file-modifying-tool introspection.
3
+ *
4
+ * This module is the single source of truth both harnesses' gate capture now
5
+ * shares, so these pin the cross-taxonomy field coverage AND the load-bearing
6
+ * guardrail: whole-file content (`content`/`contents`/`file_content`) must stay
7
+ * strictly separate from an edit's replacement (`new_string`/…). Conflating them
8
+ * would make an edit look like a whole-file capture and read the wrong `before`.
9
+ */
10
+
11
+ import { describe, it, expect } from "vitest";
12
+ import {
13
+ isFileModifyingTool,
14
+ extractFilePath,
15
+ extractWriteContent,
16
+ extractEditOldString,
17
+ extractEditNewString,
18
+ } from "../file-tools.js";
19
+
20
+ describe("isFileModifyingTool", () => {
21
+ it("recognizes the native (deepagents) taxonomy", () => {
22
+ for (const name of ["write_file", "edit_file", "create_file", "write", "edit", "create", "str_replace_editor"]) {
23
+ expect(isFileModifyingTool(name), name).toBe(true);
24
+ }
25
+ });
26
+
27
+ it("recognizes the Cursor taxonomy (hook + SDK names)", () => {
28
+ for (const name of ["Write", "StrReplace", "EditNotebook"]) {
29
+ expect(isFileModifyingTool(name), name).toBe(true);
30
+ }
31
+ });
32
+
33
+ it("excludes read-only, delete, and unknown tools (no content/hunk to preview)", () => {
34
+ for (const name of ["read", "Read", "delete", "Delete", "shell", "Shell", "grep", "mystery"]) {
35
+ expect(isFileModifyingTool(name), name).toBe(false);
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("extractFilePath", () => {
41
+ it("spans both taxonomies' path fields, including notebook target", () => {
42
+ expect(extractFilePath({ path: "a.ts" })).toBe("a.ts");
43
+ expect(extractFilePath({ file_path: "b.ts" })).toBe("b.ts");
44
+ expect(extractFilePath({ target_notebook: "nb.ipynb" })).toBe("nb.ipynb");
45
+ });
46
+
47
+ it("returns null when no recognized path key is present", () => {
48
+ expect(extractFilePath({ content: "no path here" })).toBeNull();
49
+ expect(extractFilePath({})).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe("extractWriteContent (whole-file body)", () => {
54
+ it("reads the whole-file content fields and preserves an empty file", () => {
55
+ expect(extractWriteContent({ content: "x" })).toBe("x");
56
+ expect(extractWriteContent({ contents: "y" })).toBe("y");
57
+ expect(extractWriteContent({ file_content: "z" })).toBe("z");
58
+ expect(extractWriteContent({ content: "" })).toBe(""); // empty file is real
59
+ });
60
+
61
+ it("GUARDRAIL: never treats an edit's new_string as whole-file content", () => {
62
+ // An edit fragment must NOT be picked up here, or the gate would read the
63
+ // wrong before and mislabel an edit as a whole-file rewrite.
64
+ expect(extractWriteContent({ new_string: "frag" })).toBeNull();
65
+ expect(extractWriteContent({ old_string: "a", new_string: "b" })).toBeNull();
66
+ expect(extractWriteContent({ replacement: "r" })).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe("extractEditOldString / extractEditNewString (edit fragments)", () => {
71
+ it("reads the old/new replacement fields across variants", () => {
72
+ expect(extractEditOldString({ old_string: "a" })).toBe("a");
73
+ expect(extractEditOldString({ old_text: "a" })).toBe("a");
74
+ expect(extractEditOldString({ oldText: "a" })).toBe("a");
75
+ expect(extractEditNewString({ new_string: "b" })).toBe("b");
76
+ expect(extractEditNewString({ new_text: "b" })).toBe("b");
77
+ expect(extractEditNewString({ newText: "b" })).toBe("b");
78
+ expect(extractEditNewString({ replacement: "b" })).toBe("b");
79
+ });
80
+
81
+ it("preserves an empty string (insertion / deletion fragments)", () => {
82
+ expect(extractEditOldString({ old_string: "" })).toBe(""); // insertion
83
+ expect(extractEditNewString({ new_string: "" })).toBe(""); // deletion
84
+ });
85
+
86
+ it("returns null when the fragment is absent", () => {
87
+ expect(extractEditOldString({ content: "whole" })).toBeNull();
88
+ expect(extractEditNewString({ content: "whole" })).toBeNull();
89
+ });
90
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+
3
+ const ENV_VAR = "STIGMER_RUNNER_HITL_SECRET";
4
+
5
+ // The module memoizes its secret, so each test re-imports a fresh module copy via
6
+ // vi.resetModules() to exercise the source-precedence + fallback paths cleanly.
7
+ async function freshSecretModule() {
8
+ vi.resetModules();
9
+ return import("../fingerprint-secret.js");
10
+ }
11
+
12
+ describe("getRunnerHitlMasterSecret", () => {
13
+ const original = process.env[ENV_VAR];
14
+
15
+ beforeEach(() => {
16
+ delete process.env[ENV_VAR];
17
+ });
18
+
19
+ afterEach(() => {
20
+ if (original === undefined) delete process.env[ENV_VAR];
21
+ else process.env[ENV_VAR] = original;
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ it("uses the env var (UTF-8) when set", async () => {
26
+ process.env[ENV_VAR] = "my-stable-secret";
27
+ const { getRunnerHitlMasterSecret } = await freshSecretModule();
28
+ expect(Buffer.from(getRunnerHitlMasterSecret() as Buffer).toString("utf-8")).toBe("my-stable-secret");
29
+ });
30
+
31
+ it("falls back to a per-process random secret and warns once when unset", async () => {
32
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
33
+ const { getRunnerHitlMasterSecret } = await freshSecretModule();
34
+
35
+ const a = Buffer.from(getRunnerHitlMasterSecret() as Buffer);
36
+ const b = Buffer.from(getRunnerHitlMasterSecret() as Buffer);
37
+
38
+ expect(a.length).toBe(32);
39
+ expect(a.equals(b)).toBe(true); // memoized: stable within the process
40
+ expect(warn).toHaveBeenCalledTimes(1); // warned once, not per call
41
+ });
42
+
43
+ it("two separate processes (module loads) get different random fallbacks", async () => {
44
+ vi.spyOn(console, "warn").mockImplementation(() => {});
45
+ const m1 = await freshSecretModule();
46
+ const s1 = Buffer.from(m1.getRunnerHitlMasterSecret() as Buffer);
47
+ const m2 = await freshSecretModule();
48
+ const s2 = Buffer.from(m2.getRunnerHitlMasterSecret() as Buffer);
49
+ expect(s1.equals(s2)).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cross-edition lease-scope derivation parity.
3
+ *
4
+ * Loads the shared corpus (apis/testdata/hitl/lease-scope/vectors.json) and
5
+ * asserts the TS {@link deriveLeaseScope} agrees with it. The Go
6
+ * (lease_scope_corpus_test.go) and Java (LeaseScopeFixtureTest) editions load
7
+ * the same file, so a drift in any edition fails one of the three suites — the
8
+ * guarantee that an APPROVE_ALL clicked in one edition leases the same class
9
+ * everywhere.
10
+ */
11
+
12
+ import { describe, it, expect } from "vitest";
13
+ import { readFileSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { deriveLeaseScope } from "../approval-policy.js";
16
+
17
+ interface LeaseScopeVector {
18
+ name: string;
19
+ input: { toolName: string; mcpServerSlug?: string };
20
+ expected: { category: string } | { server: string } | null;
21
+ }
22
+
23
+ const vectorsPath = fileURLToPath(
24
+ new URL(
25
+ "../../../../../../apis/testdata/hitl/lease-scope/vectors.json",
26
+ import.meta.url,
27
+ ),
28
+ );
29
+ const corpus = JSON.parse(readFileSync(vectorsPath, "utf-8")) as {
30
+ vectors: LeaseScopeVector[];
31
+ };
32
+
33
+ /** Normalize the discriminated union to the corpus's plain JSON shape. */
34
+ function asExpected(
35
+ scope: ReturnType<typeof deriveLeaseScope>,
36
+ ): { category: string } | { server: string } | null {
37
+ if (!scope) return null;
38
+ return scope.kind === "server"
39
+ ? { server: scope.server }
40
+ : { category: scope.category };
41
+ }
42
+
43
+ describe("lease-scope derivation vector corpus", () => {
44
+ it("loads a non-trivial corpus", () => {
45
+ expect(corpus.vectors.length).toBeGreaterThanOrEqual(10);
46
+ });
47
+
48
+ for (const v of corpus.vectors) {
49
+ it(`vector: ${v.name}`, () => {
50
+ const actual = asExpected(
51
+ deriveLeaseScope(v.input.toolName, v.input.mcpServerSlug ?? ""),
52
+ );
53
+ expect(actual).toEqual(v.expected);
54
+ });
55
+ }
56
+ });