@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
@@ -5,7 +5,11 @@
5
5
  * 1. McpServerStatus.tool_approvals — system-generated defaults
6
6
  * 2. McpServerSpec.pinned_tool_approvals — manual overrides
7
7
  * 3. McpServerUsage.tool_approval_overrides — per-agent customization
8
- * 4. AgentExecutionSpec.auto_approve_all — runtime bypass
8
+ * 4. Active approval leases the runtime bypass, now SCOPED: the pre-armed
9
+ * spec.auto_approve_all is a whole-run global bypass, while an interactive
10
+ * APPROVE_ALL ("approve all of this kind") grants a run-lifetime lease for
11
+ * only that action's scope (its built-in category, or its MCP server). See
12
+ * {@link ActiveLeases}.
9
13
  *
10
14
  * Used by both ExecuteCursor (hook-deny model) and ExecuteDeepAgent
11
15
  * (middleware interruptOn model) to determine which tools need approval.
@@ -14,84 +18,340 @@
14
18
  import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
15
19
  import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
16
20
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
17
- import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
+ import { ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
22
+ import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
18
23
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
19
24
 
20
25
  /**
21
- * Returns true if any tool call in the execution history (root or sub-agent)
22
- * carries an APPROVE_ALL decision.
23
- *
24
- * This is the runner-side realization of the APPROVE_ALL contract (see the
25
- * ApprovalAction doc in enum.proto): once a user has chosen "approve and don't
26
- * ask again" at any gate, the rest of THIS execution runs un-gated — exactly as
27
- * if spec.auto_approve_all were true. Both harnesses (native deepagents and
28
- * cursor) call this so the behavior is defined in exactly one place.
26
+ * The set of run-lifetime approval leases active for an execution.
27
+ *
28
+ * A lease is the scoped successor to the old all-or-nothing "approve all". When
29
+ * a user chooses APPROVE_ALL ("approve and don't ask again") at a gate it no
30
+ * longer disables the entire gate — it grants a lease for ONLY that action's
31
+ * scope, for the remainder of THIS execution: a mutating built-in category
32
+ * ({@link ToolApprovalCategory}) for a built-in tool, or an MCP server slug for
33
+ * an MCP tool. A different class of action proposed later is still gated.
34
+ *
35
+ * This is the DERIVED form of the lease — it is not (yet) a persisted proto.
36
+ * Each lease rides the `ToolCall.approval_action == APPROVE_ALL` decision that
37
+ * is already persisted and preserved (Go PreserveApprovalFields / Java
38
+ * ApprovalFieldPreserver), and its scope is recomputed on read from the tool's
39
+ * name + mcp_server_slug. Keeping it derived means one source of truth with
40
+ * nothing to drift; a persisted/transmitted `ApprovalLease` proto is warranted
41
+ * only once a lease must cross a trust boundary (a later phase).
42
+ *
43
+ * `global` is the one remaining UNSCOPED bypass: the deliberate, pre-armed
44
+ * spec.auto_approve_all ("trust this whole run", set before the run via
45
+ * CLI/API/CI). It is intentionally distinct from the interactive scoped leases.
29
46
  */
30
- export function hasApproveAllDecision(execution: AgentExecution): boolean {
31
- const status = execution.status;
32
- if (!status) return false;
47
+ export interface ActiveLeases {
48
+ /** Pre-armed spec.auto_approve_all: the whole gate is inert for the run. */
49
+ readonly global: boolean;
50
+ /** Built-in approval categories with a run-lifetime lease. */
51
+ readonly categories: ReadonlySet<ToolApprovalCategory>;
52
+ /** MCP server slugs with a run-lifetime lease (covers all of the server's tools). */
53
+ readonly servers: ReadonlySet<string>;
54
+ }
33
55
 
34
- for (const message of status.messages) {
35
- for (const tc of message.toolCalls) {
36
- if (tc.approvalAction === ApprovalAction.APPROVE_ALL) return true;
37
- }
56
+ /**
57
+ * The class an APPROVE_ALL leases for a single tool call: an MCP tool leases its
58
+ * whole `server`, a gated built-in leases its `category`. `undefined` means the
59
+ * tool has no leasable scope (a read-only built-in, an unknown name).
60
+ *
61
+ * A discriminated union (not a `{ category?, server? }` bag) so callers cannot
62
+ * construct or observe the impossible "both set" / "neither set" states.
63
+ */
64
+ export type LeaseScope =
65
+ | { readonly kind: "category"; readonly category: ToolApprovalCategory }
66
+ | { readonly kind: "server"; readonly server: string };
67
+
68
+ /**
69
+ * Reduce a single tool call to the scope its APPROVE_ALL would lease — the core
70
+ * of {@link deriveActiveLeases}, extracted so the cross-edition lease-scope
71
+ * corpus (apis/testdata/hitl/lease-scope) can exercise it directly.
72
+ *
73
+ * The MCP server slug takes precedence over the built-in category and is used
74
+ * RAW (the server's identity, not case-folded), matching the Go
75
+ * {@link DeriveLeaseScope} and Java {@link LeaseScope.deriveKey} byte-for-byte.
76
+ * The category lookup reuses {@link toolApprovalCategory}, the shared oracle, so
77
+ * a built-in resolves to write/delete/shell (read-only built-ins are ungated and
78
+ * return `undefined`).
79
+ */
80
+ export function deriveLeaseScope(
81
+ toolName: string,
82
+ mcpServerSlug: string,
83
+ ): LeaseScope | undefined {
84
+ if (mcpServerSlug) {
85
+ return { kind: "server", server: mcpServerSlug };
86
+ }
87
+ const category = toolApprovalCategory(toolName);
88
+ if (category) {
89
+ return { kind: "category", category };
38
90
  }
39
- for (const sa of status.subAgentExecutions) {
40
- for (const message of sa.messages) {
41
- for (const tc of message.toolCalls) {
42
- if (tc.approvalAction === ApprovalAction.APPROVE_ALL) return true;
91
+ return undefined;
92
+ }
93
+
94
+ /**
95
+ * Derive the active approval leases for an execution.
96
+ *
97
+ * The scoped successor to the former all-or-nothing hasApproveAllDecision:
98
+ * instead of "any APPROVE_ALL anywhere disables the whole gate", each
99
+ * APPROVE_ALL decision is reduced (via {@link deriveLeaseScope}) to the SCOPE of
100
+ * the tool it was made on — the built-in category for a built-in tool (read-only
101
+ * tools are never gated, so a built-in lease is always write/delete/shell), or
102
+ * the MCP server slug for an MCP tool — and only that scope is auto-approved for
103
+ * the rest of the run.
104
+ *
105
+ * Scans root and sub-agent tool calls so a lease granted anywhere applies
106
+ * execution-wide (matching the prior cross-sub-agent behavior, now bounded by
107
+ * scope). Both harnesses call this so the contract is defined in exactly one
108
+ * place. The scope derivation reuses {@link toolApprovalCategory}, the same
109
+ * corpus-tested oracle the Go and Java editions mirror, so the backend's
110
+ * scope-aware bulk-approve and this runner-side evaluation can never disagree.
111
+ */
112
+ export function deriveActiveLeases(execution: AgentExecution): ActiveLeases {
113
+ const categories = new Set<ToolApprovalCategory>();
114
+ const servers = new Set<string>();
115
+
116
+ const addLease = (tc: {
117
+ approvalAction: ApprovalAction;
118
+ mcpServerSlug: string;
119
+ name: string;
120
+ }): void => {
121
+ if (tc.approvalAction !== ApprovalAction.APPROVE_ALL) return;
122
+ const scope = deriveLeaseScope(tc.name, tc.mcpServerSlug);
123
+ if (!scope) return;
124
+ if (scope.kind === "server") {
125
+ servers.add(scope.server);
126
+ } else {
127
+ categories.add(scope.category);
128
+ }
129
+ };
130
+
131
+ const status = execution.status;
132
+ if (status) {
133
+ for (const message of status.messages) {
134
+ for (const tc of message.toolCalls) addLease(tc);
135
+ }
136
+ for (const sa of status.subAgentExecutions) {
137
+ for (const message of sa.messages) {
138
+ for (const tc of message.toolCalls) addLease(tc);
43
139
  }
44
140
  }
45
141
  }
46
- return false;
142
+
143
+ return {
144
+ global: execution.spec?.autoApproveAll ?? false,
145
+ categories,
146
+ servers,
147
+ };
47
148
  }
48
149
 
150
+ /**
151
+ * Provenance of a gate decision: which policy layer (or decision point) is
152
+ * responsible for the final requires-approval verdict.
153
+ *
154
+ * Mirrors the proto {@link ApprovalPolicySource} one for one (see
155
+ * {@link toProtoPolicySource}); persisted on `ToolCall.approval_policy_source`
156
+ * so every authorization is auditable, and still stamped on the shadow
157
+ * ExecutionReceipt as a defense-in-depth audit signal.
158
+ *
159
+ * `annotation_destructive_tighten` is first-class: the connect-time tightener
160
+ * (see applyDestructiveHintTightener) marks its force-gated entries with
161
+ * `ToolApprovalPolicy.from_destructive_hint`, which {@link mergeApprovalPolicies}
162
+ * reads to attribute the gate to the annotation rather than collapsing it into
163
+ * the classifier default.
164
+ */
165
+ export type PolicySource =
166
+ | "classifier_default" // Layer 1: McpServerStatus.tool_approvals (connect-time classifier)
167
+ | "pinned_override" // Layer 2: McpServerSpec.pinned_tool_approvals
168
+ | "agent_override" // Layer 3: Agent McpServerUsage.tool_approval_overrides
169
+ | "auto_approve_all" // Layer 4: pre-armed spec.auto_approve_all (whole-run global bypass)
170
+ | "approval_lease" // Layer 4: a run-lifetime scoped lease cleared this action
171
+ | "builtin_category" // Non-MCP built-in gated by the shared tool taxonomy
172
+ | "file_capture" // Capture mode: a git-tracked built-in file edit flows, reviewed post-hoc via the file_review ledger (not gated; audit-only on the shadow receipt)
173
+ | "annotation_destructive_tighten"; // Layer 1 sub-case: connect-time destructiveHint tightener force-gated this MCP tool
174
+
175
+ /**
176
+ * Monotonic identifier of the policy-engine logic that produced a decision,
177
+ * persisted on `ToolCall.policy_engine_version`. Bumped when the
178
+ * merge/classification semantics change so decisions made by different engine
179
+ * versions remain distinguishable in audits. Phase 7 made
180
+ * `annotation_destructive_tighten` a distinct, persisted source.
181
+ */
182
+ export const POLICY_ENGINE_VERSION = "phase-7";
183
+
184
+ /**
185
+ * Map the runner-internal {@link PolicySource} to the persisted proto
186
+ * {@link ApprovalPolicySource}. `undefined` (a tool no policy layer governs —
187
+ * e.g. a read-only built-in) maps to UNSPECIFIED, so the persisted field is left
188
+ * at its default exactly as an unclassified `tool_kind` is. The 1:1 mapping keeps
189
+ * the runner's union and the proto enum from drifting (asserted by the
190
+ * cross-edition corpus).
191
+ */
192
+ export function toProtoPolicySource(source: PolicySource | undefined): ApprovalPolicySource {
193
+ switch (source) {
194
+ case "classifier_default":
195
+ return ApprovalPolicySource.CLASSIFIER_DEFAULT;
196
+ case "pinned_override":
197
+ return ApprovalPolicySource.PINNED_OVERRIDE;
198
+ case "agent_override":
199
+ return ApprovalPolicySource.AGENT_OVERRIDE;
200
+ case "auto_approve_all":
201
+ return ApprovalPolicySource.AUTO_APPROVE_ALL;
202
+ case "approval_lease":
203
+ return ApprovalPolicySource.APPROVAL_LEASE;
204
+ case "builtin_category":
205
+ return ApprovalPolicySource.BUILTIN_CATEGORY;
206
+ case "annotation_destructive_tighten":
207
+ return ApprovalPolicySource.ANNOTATION_DESTRUCTIVE_TIGHTEN;
208
+ case "file_capture":
209
+ // Capture-mode flow is never persisted on a gated tool call (the file tool
210
+ // is not gated — it has no WAITING_APPROVAL row); it exists only on the
211
+ // audit receipt. Map to UNSPECIFIED for the proto-persisted field.
212
+ return ApprovalPolicySource.UNSPECIFIED;
213
+ case undefined:
214
+ return ApprovalPolicySource.UNSPECIFIED;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Derive the authorization provenance — which policy layer governs this tool —
220
+ * for persisting on `ToolCall.approval_policy_source`.
221
+ *
222
+ * This is the read-side twin of the gate's decision logic: same layered
223
+ * precedence, but it answers "which layer governs this call?" for EVERY tool
224
+ * (gated or auto-approved), so the StatusBuilders can stamp provenance on the
225
+ * tool call exactly where they stamp `tool_kind`. It returns `undefined` for a
226
+ * plain read-only built-in that no policy layer touches (the proto's
227
+ * APPROVAL_POLICY_SOURCE_UNSPECIFIED).
228
+ *
229
+ * Precedence:
230
+ * 1. Whole-run global bypass (pre-armed auto_approve_all) governs everything —
231
+ * it is *why* anything ran ungated, so it wins.
232
+ * 2. MCP tool: the merged policy carries the responsible layer when gated; an
233
+ * absent entry means the four-level chain cleared it (classifier base). A
234
+ * server-scoped lease also surfaces as an absent entry — distinguishing it
235
+ * would need the lease set threaded here and is deferred with the rest of the
236
+ * per-server lease provenance, so a lease-cleared MCP tool reads
237
+ * classifier_default (matching the gate).
238
+ * 3. Built-in: a mutating category is governed (leased → approval_lease, else
239
+ * builtin_category); a read-only built-in is governed by no layer → undefined.
240
+ */
241
+ export function resolveApprovalProvenance(
242
+ toolName: string,
243
+ serverSlug: string,
244
+ policies: ReadonlyMap<string, MergedToolPolicy>,
245
+ leasedCategories: ReadonlySet<ToolApprovalCategory>,
246
+ globalBypass: boolean,
247
+ ): PolicySource | undefined {
248
+ if (globalBypass) return "auto_approve_all";
249
+
250
+ if (serverSlug) {
251
+ const policy = policies.get(`${serverSlug}/${toolName}`);
252
+ if (policy) return policy.source;
253
+ return "classifier_default";
254
+ }
255
+
256
+ const category = toolApprovalCategory(toolName);
257
+ if (!category) return undefined;
258
+ if (leasedCategories.has(category)) return "approval_lease";
259
+ return "builtin_category";
260
+ }
261
+
262
+ /**
263
+ * A single MCP tool's merged approval decision after evaluating all policy
264
+ * layers. This is the single, canonical shape shared by every harness; the
265
+ * Cursor harness re-exports it from here so the two harnesses can never drift.
266
+ */
49
267
  export interface MergedToolPolicy {
50
268
  toolName: string;
51
269
  mcpServerSlug: string;
52
270
  requiresApproval: boolean;
53
271
  approvalMessage: string;
272
+ /** Which policy layer set this verdict (provenance for the shadow receipt). */
273
+ source: PolicySource;
54
274
  }
55
275
 
56
276
  /**
57
277
  * Merge approval policies from all four levels into a single lookup map.
58
278
  *
59
- * Keys are "serverSlug/toolName" to avoid collisions between servers.
60
- * When autoApproveAll is true, the returned map is empty.
279
+ * Each MCP server contributes its own set of policies, so the map is keyed by
280
+ * "serverSlug/toolName" to avoid collisions between servers.
281
+ *
282
+ * Policy chain (each level overrides the previous):
283
+ * 1. status.toolApprovals — system-generated defaults; presence = requires approval
284
+ * 2. spec.pinnedToolApprovals — manual overrides; presence = requires approval
285
+ * 3. agent tool_approval_overrides — explicit boolean per tool (enable OR disable)
286
+ * 4. active leases — runtime bypass (highest priority), now scoped
287
+ *
288
+ * The map carries ONLY the tools that require approval — a tool's absence means
289
+ * "auto-approved". Leases shape that absence:
290
+ * - On a global pre-arm ({@link ActiveLeases.global}) the map is empty.
291
+ * - A server-scoped lease drops that server's tools from the map entirely. This
292
+ * single omission makes EVERY substrate treat the server as auto-approved with
293
+ * no extra code — the deep-agent gate and StatusBuilder read the map, and the
294
+ * Cursor hook's mcpToolPolicies is built from it (the hook is not itself
295
+ * server-aware, so omission is the only way to lease an MCP server there).
296
+ * Built-in CATEGORY leases are NOT applied here — built-ins are not in this map;
297
+ * they are cleared at the gate (deep-agent) and the hook (Cursor) instead.
298
+ *
299
+ * Used by both ExecuteCursor (hook-deny model) and ExecuteDeepAgent (middleware
300
+ * interruptOn model), so the four-level semantics are defined in exactly one
301
+ * place.
61
302
  */
62
303
  export function mergeApprovalPolicies(
63
304
  resolvedServers: ResolvedMcpServer[],
64
305
  agentOverrides: ToolApprovalOverride[],
65
- autoApproveAll: boolean,
306
+ leases: ActiveLeases,
66
307
  ): Map<string, MergedToolPolicy> {
67
308
  const merged = new Map<string, MergedToolPolicy>();
68
309
 
69
- if (autoApproveAll) return merged;
310
+ if (leases.global) return merged;
70
311
 
71
312
  for (const server of resolvedServers) {
72
- const serverPolicies = new Map<string, { requiresApproval: boolean; message: string }>();
313
+ // Server-scoped lease: a prior APPROVE_ALL on one of this server's tools
314
+ // auto-approves the whole server for the run, so none of its tools enter the
315
+ // require-approval map (identical to how an already-auto-approved tool is
316
+ // absent — every consumer treats it as cleared).
317
+ if (leases.servers.has(server.slug)) continue;
73
318
 
319
+ const serverPolicies = new Map<string, { requiresApproval: boolean; message: string; source: PolicySource }>();
320
+
321
+ // Layer 1: system-generated defaults (presence = requires approval). A tool
322
+ // the connect-time tightener force-gated from its destructiveHint annotation
323
+ // carries that provenance (from_destructive_hint) so it is attributed to the
324
+ // annotation rather than the classifier — the only sub-case within layer 1.
74
325
  for (const policy of server.toolApprovals) {
75
326
  if (!policy.toolName) continue;
76
327
  serverPolicies.set(policy.toolName, {
77
328
  requiresApproval: true,
78
329
  message: policy.message || `Execute tool: ${policy.toolName}`,
330
+ source: policy.fromDestructiveHint
331
+ ? "annotation_destructive_tighten"
332
+ : "classifier_default",
79
333
  });
80
334
  }
81
335
 
336
+ // Layer 2: manual overrides (presence = requires approval, overrides layer 1).
82
337
  for (const pinned of server.pinnedToolApprovals) {
83
338
  if (!pinned.toolName) continue;
84
339
  serverPolicies.set(pinned.toolName, {
85
340
  requiresApproval: true,
86
341
  message: pinned.message || serverPolicies.get(pinned.toolName)?.message || `Execute tool: ${pinned.toolName}`,
342
+ source: "pinned_override",
87
343
  });
88
344
  }
89
345
 
346
+ // Layer 3: per-agent overrides (explicit boolean, can enable or disable).
347
+ // Touching a tool here makes the per-agent layer the responsible source,
348
+ // whether it enables, disables, or re-messages the gate.
90
349
  for (const override of agentOverrides) {
91
350
  if (!override.toolName) continue;
92
351
  const existing = serverPolicies.get(override.toolName);
93
352
  if (existing) {
94
353
  existing.requiresApproval = override.requiresApproval;
354
+ existing.source = "agent_override";
95
355
  if (override.message) {
96
356
  existing.message = override.message;
97
357
  }
@@ -99,10 +359,12 @@ export function mergeApprovalPolicies(
99
359
  serverPolicies.set(override.toolName, {
100
360
  requiresApproval: true,
101
361
  message: override.message || `Execute tool: ${override.toolName}`,
362
+ source: "agent_override",
102
363
  });
103
364
  }
104
365
  }
105
366
 
367
+ // Emit only the tools that still require approval after all layers.
106
368
  for (const [toolName, policy] of serverPolicies) {
107
369
  if (!policy.requiresApproval) continue;
108
370
  const key = `${server.slug}/${toolName}`;
@@ -111,6 +373,7 @@ export function mergeApprovalPolicies(
111
373
  mcpServerSlug: server.slug,
112
374
  requiresApproval: true,
113
375
  approvalMessage: policy.message,
376
+ source: policy.source,
114
377
  });
115
378
  }
116
379
  }
@@ -118,6 +381,14 @@ export function mergeApprovalPolicies(
118
381
  return merged;
119
382
  }
120
383
 
384
+ /**
385
+ * Look up whether an MCP tool requires approval.
386
+ *
387
+ * @param toolName - The actual MCP tool name (e.g., "apply_cloud_resource")
388
+ * @param mcpServerSlug - The MCP server slug (e.g., "planton")
389
+ * @param policies - The merged policy map from {@link mergeApprovalPolicies}
390
+ * @returns The policy if approval is required, undefined if auto-approved
391
+ */
121
392
  export function lookupMcpToolPolicy(
122
393
  toolName: string,
123
394
  mcpServerSlug: string,
@@ -129,6 +400,11 @@ export function lookupMcpToolPolicy(
129
400
  /**
130
401
  * Resolve {{args.field}} placeholders in an approval message using the
131
402
  * tool's actual arguments.
403
+ *
404
+ * Placeholder syntax matches the proto-documented format:
405
+ * - {{args.field_name}} — replaced with the argument value
406
+ * - {{tool_name}} — replaced with the tool name
407
+ * - Missing fields are replaced with "<unknown>"
132
408
  */
133
409
  export function resolveApprovalMessage(
134
410
  template: string,
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Harness-agnostic sanitization of tool arguments for the `args_preview` field.
3
+ *
4
+ * `args_preview` is the human-readable, UI-facing projection of a tool call's
5
+ * arguments. It is surfaced on the approval card, the CLI, and the web console,
6
+ * and — for the Cursor harness — it is the field a resumed turn parses to rebuild
7
+ * an approval grant's salient identity. So it must always be SMALL and VALID
8
+ * JSON: the per-tool offload does not touch it, and the aggregate size backstop
9
+ * would replace an oversized preview with an unparseable marker.
10
+ *
11
+ * This module is the single home for arg sanitization shared by both harnesses
12
+ * (native re-exports {@link sanitizeArgsPreview} from its status builders; the
13
+ * Cursor gate path uses {@link buildElidedArgsPreview}).
14
+ */
15
+
16
+ /**
17
+ * Argument keys whose values are secrets and must never appear in a preview.
18
+ * Matched case-insensitively against the top-level key name.
19
+ */
20
+ export const SENSITIVE_ARG_KEYS: ReadonlySet<string> = new Set([
21
+ "password", "token", "secret", "api_key", "apikey",
22
+ "credentials", "auth", "authorization",
23
+ ]);
24
+
25
+ /** Whole-preview length cap for {@link sanitizeArgsPreview} (native). */
26
+ export const MAX_ARGS_PREVIEW_LENGTH = 500;
27
+
28
+ /**
29
+ * Sanitize args into a compact preview string (native harness behavior).
30
+ *
31
+ * Redacts sensitive keys, then truncates the whole JSON to a fixed length. The
32
+ * truncation can yield invalid JSON, which is acceptable for the native harness
33
+ * (its resume model keys on `tool_call_id`, never on a re-parsed preview). The
34
+ * Cursor gate path must instead use {@link buildElidedArgsPreview}, which keeps
35
+ * the JSON valid and preserves the salient identity fields.
36
+ */
37
+ export function sanitizeArgsPreview(args: Record<string, unknown>): string {
38
+ const sanitized: Record<string, unknown> = {};
39
+ for (const [key, value] of Object.entries(args)) {
40
+ sanitized[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
41
+ }
42
+ try {
43
+ const json = JSON.stringify(sanitized);
44
+ return json.length > MAX_ARGS_PREVIEW_LENGTH
45
+ ? json.slice(0, MAX_ARGS_PREVIEW_LENGTH) + "…"
46
+ : json;
47
+ } catch {
48
+ return "";
49
+ }
50
+ }
51
+
52
+ /** Per-string-value cap for {@link buildElidedArgsPreview}. */
53
+ const MAX_PREVIEW_VALUE_LENGTH = 200;
54
+
55
+ /**
56
+ * Build a compact, ALWAYS-VALID `args_preview` from a tool call's full,
57
+ * authoritative arguments.
58
+ *
59
+ * Unlike {@link sanitizeArgsPreview} (which truncates the whole string, possibly
60
+ * to invalid JSON), this elides oversized string *values* in place — preserving
61
+ * every key and the JSON structure. Two invariants make it safe for the Cursor
62
+ * gate path:
63
+ * - It NEVER elides a salient field (the resume grant's identity — the file
64
+ * path or shell command — is parsed back out of this preview, so it must
65
+ * survive verbatim).
66
+ * - It redacts secret keys.
67
+ *
68
+ * The heavy content (a whole-file body, a large diff) lives on `file_changes`
69
+ * (offloaded to a ref when large) and `args` (bounded by the size backstop), so
70
+ * the preview itself stays small even for a multi-MB write.
71
+ *
72
+ * @param args the full tool arguments
73
+ * @param salientFields keys whose values must be preserved verbatim (identity)
74
+ */
75
+ export function buildElidedArgsPreview(
76
+ args: Record<string, unknown>,
77
+ salientFields: readonly string[],
78
+ ): string {
79
+ const out: Record<string, unknown> = {};
80
+ for (const [key, value] of Object.entries(args)) {
81
+ if (SENSITIVE_ARG_KEYS.has(key.toLowerCase())) {
82
+ out[key] = "[REDACTED]";
83
+ } else if (
84
+ !salientFields.includes(key) &&
85
+ typeof value === "string" &&
86
+ value.length > MAX_PREVIEW_VALUE_LENGTH
87
+ ) {
88
+ out[key] = `[${value.length} chars]`;
89
+ } else {
90
+ out[key] = value;
91
+ }
92
+ }
93
+ try {
94
+ return JSON.stringify(out);
95
+ } catch {
96
+ return "";
97
+ }
98
+ }
@@ -2,8 +2,13 @@
2
2
  * Artifact storage abstraction for the unified runner.
3
3
  *
4
4
  * Two backends:
5
- * - Local: writes to the filesystem, served by stigmer-server (OSS mode).
5
+ * - Local: writes to the filesystem (OSS mode). The runner reads its own
6
+ * artifacts straight back off disk via {@link ArtifactStorage.download} — the
7
+ * exact inverse of {@link ArtifactStorage.upload}. `getDownloadUrl` still
8
+ * returns the stigmer-server serve URL, but that is for OTHER consumers (the
9
+ * web console fetching an artifact for display), not the runner's own reads.
6
10
  * - Proxy: uses presigned URLs from the Stigmer Side-Channel Proxy (cloud mode).
11
+ * Here `download` resolves a presigned URL and fetches it over HTTPS.
7
12
  *
8
13
  * The runner never holds R2/S3 credentials — in cloud mode it calls the proxy
9
14
  * to obtain a presigned upload URL, then PUTs content over plain HTTPS.
@@ -11,7 +16,7 @@
11
16
  * DD-6: No direct R2 backend. Local + Proxy only.
12
17
  */
13
18
 
14
- import { mkdir, writeFile, readFile, access, unlink } from "node:fs/promises";
19
+ import { mkdir, writeFile, readFile, access } from "node:fs/promises";
15
20
  import { dirname, join } from "node:path";
16
21
  import type { Config } from "../config.js";
17
22
 
@@ -20,6 +25,17 @@ import type { Config } from "../config.js";
20
25
  export interface ArtifactStorage {
21
26
  upload(key: string, content: Buffer, contentType?: string): Promise<string>;
22
27
  getDownloadUrl(key: string): Promise<string>;
28
+ /**
29
+ * Read an artifact's raw bytes by key — the inverse of {@link upload} and the
30
+ * single read path for all runner read-back (CAS reconcile, exact-apply,
31
+ * claimcheck decode, attachment injection).
32
+ *
33
+ * Returns the exact stored bytes, or throws a descriptive, key-scoped `Error`
34
+ * when the object is missing or the transport fails. Per-backend semantics:
35
+ * local reads directly off disk; proxy resolves a presigned URL and fetches
36
+ * it. Callers own their own error-wrapping / degradation policy.
37
+ */
38
+ download(key: string): Promise<Buffer>;
23
39
  exists(key: string): Promise<boolean>;
24
40
  }
25
41
 
@@ -47,6 +63,18 @@ export class LocalArtifactStorage implements ArtifactStorage {
47
63
  return `${this.serveUrlBase}/${key}`;
48
64
  }
49
65
 
66
+ async download(key: string): Promise<Buffer> {
67
+ // Direct disk read — the exact inverse of `upload`. The runner wrote these
68
+ // bytes to `basePath`, so it reads them back without a self-HTTP round-trip
69
+ // and without depending on the serve URL being set or reachable.
70
+ try {
71
+ return await readFile(join(this.basePath, key));
72
+ } catch (err) {
73
+ const reason = err instanceof Error ? err.message : String(err);
74
+ throw new Error(`Artifact not found for key '${key}': ${reason}`);
75
+ }
76
+ }
77
+
50
78
  async exists(key: string): Promise<boolean> {
51
79
  try {
52
80
  await access(join(this.basePath, key));
@@ -144,20 +172,43 @@ export class ProxyArtifactStorage implements ArtifactStorage {
144
172
  return data.url;
145
173
  }
146
174
 
175
+ async download(key: string): Promise<Buffer> {
176
+ const url = await this.getDownloadUrl(key);
177
+ const resp = await fetch(url);
178
+ if (!resp.ok) {
179
+ throw new Error(
180
+ `Artifact download failed (HTTP ${resp.status}) for key '${key}': ` +
181
+ await resp.text(),
182
+ );
183
+ }
184
+ return Buffer.from(await resp.arrayBuffer());
185
+ }
186
+
147
187
  async exists(key: string): Promise<boolean> {
188
+ // A presigned URL is minted for ANY key — the presign endpoint does not check
189
+ // the object — so "did presign succeed?" is NOT existence (that bug turned a
190
+ // git-only file-review reconcile into a doomed manifest download + 404 crash).
191
+ // Probe the object itself with a 1-byte ranged GET: the presigned URL is
192
+ // SigV4-signed for GET (a HEAD would break the signature), and `Range:
193
+ // bytes=0-0` transfers at most one byte. Missing => 404; present => 200/206
194
+ // (or 416 for a 0-byte object, whose range is unsatisfiable yet it exists).
195
+ let url: string;
148
196
  try {
149
- const resp = await fetch(`${this.baseUrl}/presigned-download-url`, {
150
- method: "POST",
151
- headers: {
152
- "Authorization": `Bearer ${this.authToken}`,
153
- "Content-Type": "application/json",
154
- },
155
- body: JSON.stringify({ key }),
156
- });
157
- return resp.ok;
197
+ url = await this.getDownloadUrl(key);
158
198
  } catch {
199
+ // Presign endpoint unreachable: report absent. The sole caller (content-
200
+ // addressed CAS blob dedup) then re-uploads, which is idempotent.
159
201
  return false;
160
202
  }
203
+ const resp = await fetch(url, { headers: { Range: "bytes=0-0" } });
204
+ await resp.arrayBuffer().catch(() => undefined); // drain the <=1-byte body
205
+ if (resp.status === 404) return false;
206
+ if (resp.status === 200 || resp.status === 206 || resp.status === 416) return true;
207
+ // Any other status (e.g. 403 expired/misconfigured, 5xx) is a real fault, not
208
+ // an existence answer — surface it rather than silently mis-reporting.
209
+ throw new Error(
210
+ `Artifact existence check failed (HTTP ${resp.status}) for key '${key}'`,
211
+ );
161
212
  }
162
213
  }
163
214
 
@@ -80,7 +80,6 @@ describe("HttpCheckpointSaver", () => {
80
80
  const metadata: CheckpointMetadata = {
81
81
  source: "loop",
82
82
  step: 0,
83
- writes: null,
84
83
  parents: {},
85
84
  };
86
85
 
@@ -133,7 +132,7 @@ describe("HttpCheckpointSaver", () => {
133
132
  channel_versions: {},
134
133
  versions_seen: {},
135
134
  };
136
- const md: CheckpointMetadata = { source: "loop", step: 1, writes: null, parents: {} };
135
+ const md: CheckpointMetadata = { source: "loop", step: 1, parents: {} };
137
136
  const cpSer = await serializeForProxy(cp);
138
137
  const mdSer = await serializeForProxy(md);
139
138