@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
@@ -9,21 +9,36 @@
9
9
  * State file format (JSON):
10
10
  * {
11
11
  * "autoApproveAll": false,
12
+ * "leasedCategories": ["shell"],
12
13
  * "mcpToolPolicies": {
13
14
  * "apply_cloud_resource": { "requiresApproval": true, "message": "..." }
14
15
  * },
15
- * "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt" }],
16
- * "approvedGrantTokens": ["d3JpdGUKYS50eHQ="]
16
+ * "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt", "contentDigest": "<sha256>" }],
17
+ * "approvedGrantTokens": ["<base64(key\nsalient[\ncontentDigest])>"]
17
18
  * }
18
19
  *
20
+ * A grant token is the action's PRIMARY token: base64(key \n salient \n digest)
21
+ * for a content-identified file edit (so a DIFFERENT edit to the same file does
22
+ * not match), or base64(key \n salient) for shell/delete/MCP and the rare
23
+ * content-less fallback. See {@link primaryToken}.
24
+ *
19
25
  * The hook gates the dangerous built-in set and the MCP tools that require
20
26
  * approval (mcpToolPolicies, which by construction holds only require-approval
21
27
  * entries); every other tool is allowed. The gated built-in set and its
22
28
  * name->category mapping are baked into the generated hook script (from
23
29
  * approval-policy.ts), not carried in the state file — only the dynamic inputs
24
- * (autoApproveAll, mcpToolPolicies, approvedGrantTokens) live here. This mirrors
25
- * the native harness and avoids denying auto-approved MCP tools, which are
26
- * absent from the policy map and indistinguishable from unknown tools by name.
30
+ * (autoApproveAll, leasedCategories, mcpToolPolicies, approvedGrantTokens) live
31
+ * here. This mirrors the native harness and avoids denying auto-approved MCP
32
+ * tools, which are absent from the policy map and indistinguishable from unknown
33
+ * tools by name.
34
+ *
35
+ * Approval leases (the scoped successor to autoApproveAll): `autoApproveAll` is
36
+ * now ONLY the pre-armed spec.auto_approve_all global bypass. An interactive
37
+ * "approve all" of a given class becomes a run-lifetime lease: a built-in
38
+ * category lease is listed in `leasedCategories` (the hook allows any built-in of
39
+ * that category), and an MCP-server lease is applied upstream by dropping the
40
+ * server's tools from mcpToolPolicies (so the hook allows them as auto-approved)
41
+ * — the hook is not server-aware, so omission is the lever there.
27
42
  *
28
43
  * Why grants instead of tool-call ids: a resumed Cursor agent re-issues the
29
44
  * approved tool with a BRAND NEW call id, so matching on the original call id
@@ -51,15 +66,21 @@
51
66
  * token next turn.
52
67
  */
53
68
 
54
- import { writeFile, readFile, mkdir } from "node:fs/promises";
69
+ import { watch } from "node:fs";
70
+ import { writeFile, readFile, mkdir, rename, rm } from "node:fs/promises";
55
71
  import { join } from "node:path";
56
72
  import { create } from "@bufbuild/protobuf";
57
73
  import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
58
74
  import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
59
75
  import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
60
76
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
61
- import type { MergedToolPolicy } from "./approval-policy.js";
62
- import { extractArgKey, approvalCategory } from "./approval-policy.js";
77
+ import type { MergedToolPolicy, ApprovalCategory } from "./approval-policy.js";
78
+ import { extractArgKey, approvalCategory, POLICY_ENGINE_VERSION } from "./approval-policy.js";
79
+ import { contentDigest } from "../../shared/file-tools.js";
80
+ import {
81
+ fingerprintCoarseIdentity,
82
+ type FingerprintKey,
83
+ } from "../../shared/approval-fingerprint.js";
63
84
 
64
85
  export interface McpToolPolicyEntry {
65
86
  requiresApproval: boolean;
@@ -115,13 +136,78 @@ export interface ApprovalGrant {
115
136
  mcpServerSlug: string;
116
137
  key: string;
117
138
  salient: string;
139
+ /**
140
+ * Content digest of the approved edit (see {@link contentDigest}), or "" when
141
+ * the action is not content-identified (shell/delete/MCP, or a content-less
142
+ * fallback). When present, the grant authorizes the {@link contentToken} so a
143
+ * DIFFERENT edit to the same path does NOT match; when "", it authorizes the
144
+ * coarse {@link grantToken} (the documented degrade).
145
+ */
146
+ contentDigest: string;
147
+ /**
148
+ * Id of the adjudicated tool call this grant was minted from — the transcript
149
+ * row carrying the SERVER-authored approval_action. The approved-command
150
+ * auto-keep provenance (DD-28) cites this row as the consent the backend can
151
+ * verify; the hook itself never reads it.
152
+ */
153
+ sourceToolCallId: string;
118
154
  }
119
155
 
120
156
  export interface ApprovalStateFile {
157
+ /** Pre-armed spec.auto_approve_all: the whole-run global bypass. */
121
158
  autoApproveAll: boolean;
159
+ /**
160
+ * Built-in approval categories with a run-lifetime lease (the scoped successor
161
+ * to a global "approve all"). The hook allows any built-in whose category is
162
+ * listed. MCP-server leases are NOT listed here — they are applied by dropping
163
+ * the server's tools from mcpToolPolicies, since the hook is not server-aware.
164
+ */
165
+ leasedCategories: string[];
122
166
  mcpToolPolicies: Record<string, McpToolPolicyEntry>;
123
167
  approvedGrants: ApprovalGrant[];
124
168
  approvedGrantTokens: string[];
169
+ /**
170
+ * Capture mode (git workspaces): when true, the hook ALLOWS file mutations
171
+ * (write/edit/delete) to flow during the turn, because the runner captures the
172
+ * whole change set with git at the turn boundary and gates it per-file for
173
+ * review (see shared/filereview/git-substrate.ts). The ONLY exception is a
174
+ * write/delete whose
175
+ * path is gitignored — the git snapshot cannot capture or revert it, so the
176
+ * hook keeps gating those (it runs `git check-ignore`). shell and MCP stay
177
+ * gated as always. False (the default) keeps the classic deny-gate behavior
178
+ * for every file mutation (non-git workspaces / the fallback path).
179
+ */
180
+ captureMode: boolean;
181
+ /**
182
+ * CAS capture for gitignored writes (the deep-agent parity switch). When true,
183
+ * a non-secret gitignored write/edit no longer stays on the deny-gate: the hook
184
+ * stages its pre-write bytes into the runner-owned cas-observations sidecar and
185
+ * ALLOWS it to flow, and the turn boundary captures it into content-addressable
186
+ * storage as a `GIT_IGNORED_CAPTURED` change for per-file review (mirroring the
187
+ * deep-agent `CasCaptureFilesystemBackend` observer). A secret-like gitignored
188
+ * path is instead hard-blocked (denied, nothing written) and recorded as an
189
+ * unreviewable observation, so its bytes never reach durable storage.
190
+ *
191
+ * Set only when `captureMode` AND an artifact storage is configured — CAS blob
192
+ * persistence requires it. When false, gitignored writes keep the classic
193
+ * deny-gate behavior (gitignored deletes and shell/MCP always do). Independent
194
+ * of `autoApproveAll`: capture is a property of the turn, not authorization, so
195
+ * the hook honors this switch even under the global bypass.
196
+ */
197
+ captureIgnored: boolean;
198
+ /**
199
+ * Whether the primary workspace is a git work tree (Slice 2c). Selects the
200
+ * hook's capture substrate:
201
+ * - `true` (default): git tree — git-tracked write/edit/delete flow freely
202
+ * (the runner captures them from the git diff at the turn boundary); only a
203
+ * GITIGNORED write is CAS-staged (when `captureIgnored`), a gitignored delete
204
+ * stays gated.
205
+ * - `false`: non-git workspace — there is no git snapshot, so EVERY file write
206
+ * is CAS-staged and flowed for review (`captureIgnored` is on in this mode),
207
+ * a delete stays gated (no CAS delete-capture path, parity with the
208
+ * deep-agent), and shell/MCP gate as always.
209
+ */
210
+ gitWorkspace: boolean;
125
211
  }
126
212
 
127
213
  /**
@@ -134,6 +220,87 @@ export function grantToken(key: string, salient: string): string {
134
220
  return Buffer.from(`${key}\n${salient}`, "utf-8").toString("base64");
135
221
  }
136
222
 
223
+ /**
224
+ * The content-exact wire token: `base64(key \n salient \n contentDigest)`. This
225
+ * is the exact-identity grant the hook matches for a file-mutating tool, so an
226
+ * approval of one edit does NOT authorize a DIFFERENT edit to the same file (a
227
+ * different digest yields a different token). The hook recomputes the identical
228
+ * token from `tool_input` (it appends the same `\n<digest>` only when the digest
229
+ * is non-empty — see hook-script.ts), so this encoding must stay byte-identical
230
+ * to the hook's, exactly as {@link grantToken} already is.
231
+ */
232
+ export function contentToken(key: string, salient: string, contentDigest: string): string {
233
+ return Buffer.from(`${key}\n${salient}\n${contentDigest}`, "utf-8").toString("base64");
234
+ }
235
+
236
+ /**
237
+ * The single token a tool call authorizes (as a grant) and is recorded under (as
238
+ * a denial): the {@link contentToken} when a content digest is present (file
239
+ * edits/writes), else the coarse {@link grantToken} (shell, delete, MCP, or a
240
+ * content-less grep-fallback). One definition, used by the runner for both grant
241
+ * building and denial correlation and mirrored by the hook, so the deny-time and
242
+ * reinvoke-time identities can never drift.
243
+ */
244
+ export function primaryToken(key: string, salient: string, contentDigest: string): string {
245
+ return contentDigest ? contentToken(key, salient, contentDigest) : grantToken(key, salient);
246
+ }
247
+
248
+ /**
249
+ * The shared HMAC coarse fingerprint of an approval grant.
250
+ *
251
+ * This is the SAME canonical coarse identity the wire token (grantToken) encodes
252
+ * — `(key, salient)` plus the MCP slug — run through the one shared HMAC+canonical
253
+ * path ({@link fingerprintCoarseIdentity}). It is NOT the hook's wire-match value:
254
+ * the hook matches on the mechanically-reproducible base64 token (a bash script
255
+ * can recompute base64 but not a keyed HMAC over a workspace-root-normalized
256
+ * salient). This fingerprint is the cross-substrate, anti-forgery identity used
257
+ * for the runner-side shadow receipt today and as the successor wire token once a
258
+ * lease becomes a server-issued bearer token (Phase 7). Because it shares the
259
+ * exact category + salient the token uses, the hook-side and stream-side
260
+ * fingerprints of one action are equal by construction (see the parity tests).
261
+ */
262
+ export function grantFingerprint(key: FingerprintKey, grant: ApprovalGrant): string {
263
+ return fingerprintCoarseIdentity(key, {
264
+ tool: grant.key,
265
+ mcpServerSlug: grant.mcpServerSlug,
266
+ salient: grant.salient,
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Emit a best-effort shadow ExecutionReceipt for each grant the runner issues
272
+ * this turn (Phase 2, mirror of the deep-agent gateway's receipt).
273
+ *
274
+ * Cursor executes tools out-of-process, so unlike the in-process deep-agent
275
+ * gateway the runner cannot observe the actual side effect — the receipt is
276
+ * issued when the authorization GRANT is written (best-effort, `verified:false`),
277
+ * not at execution. Structured log only: never persisted, no proto. Shares the
278
+ * one HMAC+canonical fingerprint path so the value matches the deep-agent and
279
+ * cross-language corpus definitions.
280
+ */
281
+ export function emitCursorGrantReceipts(
282
+ grants: readonly ApprovalGrant[],
283
+ fingerprintKey: FingerprintKey,
284
+ executionId: string,
285
+ ): void {
286
+ for (const g of grants) {
287
+ console.log(
288
+ "[hitl-gateway] receipt " +
289
+ JSON.stringify({
290
+ executionId,
291
+ toolName: g.toolName,
292
+ mcpServerSlug: g.mcpServerSlug,
293
+ category: g.mcpServerSlug ? "" : g.key,
294
+ authorization: "approval",
295
+ policyEngineVersion: POLICY_ENGINE_VERSION,
296
+ fingerprint: grantFingerprint(fingerprintKey, g),
297
+ substrate: "cursor",
298
+ verified: false,
299
+ }),
300
+ );
301
+ }
302
+ }
303
+
137
304
  /**
138
305
  * Build approval grants from the pending approvals the user adjudicated and
139
306
  * their decisions. Only APPROVE / APPROVE_ALL decisions produce grants. Each
@@ -143,14 +310,15 @@ export function grantToken(key: string, salient: string): string {
143
310
  export function buildApprovalGrants(
144
311
  pendingApprovals: PendingApproval[],
145
312
  decisions: Map<string, ApprovalAction>,
313
+ contentDigests?: Map<string, string>,
146
314
  ): ApprovalGrant[] {
147
315
  const grants: ApprovalGrant[] = [];
148
316
  for (const pa of pendingApprovals) {
149
317
  // Both APPROVE and APPROVE_ALL allow the adjudicated tool through on the
150
- // resumed turn. APPROVE_ALL additionally flips autoApproveAll for the whole
151
- // run (handled by the caller via hasApproveAllDecision), but we still emit a
152
- // grant here so the clicked tool is allowed regardless of how the hook reads
153
- // the state file.
318
+ // resumed turn. APPROVE_ALL additionally grants a run-lifetime lease for the
319
+ // clicked action's class (handled by the caller via deriveActiveLeases ->
320
+ // leasedCategories / dropped MCP server), but we still emit a grant here so
321
+ // the clicked tool itself is allowed regardless of how the hook reads state.
154
322
  const decision = decisions.get(pa.toolCallId);
155
323
  if (decision !== ApprovalAction.APPROVE && decision !== ApprovalAction.APPROVE_ALL) continue;
156
324
 
@@ -160,6 +328,14 @@ export function buildApprovalGrants(
160
328
  mcpServerSlug: pa.mcpServerSlug,
161
329
  key: id.key,
162
330
  salient: id.salient,
331
+ // The content digest comes from the gate's authoritative captured input
332
+ // (carried on the tool call, see reconstructAdjudicatedApprovals) — NOT
333
+ // from argsPreview, which elides heavy edit content. Empty when the gate
334
+ // had no content (shell/delete/MCP) or it was unrecoverable, in which case
335
+ // the grant degrades to the coarse token (a same-file sibling can ride it,
336
+ // the documented bounded residual).
337
+ contentDigest: contentDigests?.get(pa.toolCallId) ?? "",
338
+ sourceToolCallId: pa.toolCallId,
163
339
  });
164
340
  }
165
341
  return grants;
@@ -180,7 +356,10 @@ function parseArgs(argsPreview: string): Record<string, unknown> | undefined {
180
356
  * grants from a previous HITL cycle.
181
357
  *
182
358
  * The state file carries the hook script's DYNAMIC inputs:
183
- * - mcpToolPolicies: per-tool policy for MCP tools requiring approval
359
+ * - globalBypass: the pre-armed spec.auto_approve_all (written as autoApproveAll)
360
+ * - leasedCategories: built-in categories with a run-lifetime lease
361
+ * - mcpToolPolicies: per-tool policy for MCP tools requiring approval (leased
362
+ * servers are already absent — dropped upstream by mergeApprovalPolicies)
184
363
  * - approvedGrants / approvedGrantTokens: tools approved in the current HITL
185
364
  * cycle, allowed through on reinvocation
186
365
  *
@@ -189,8 +368,12 @@ function parseArgs(argsPreview: string): Record<string, unknown> | undefined {
189
368
  */
190
369
  export function buildApprovalState(
191
370
  mergedPolicies: Map<string, MergedToolPolicy>,
192
- autoApproveAll: boolean,
371
+ globalBypass: boolean,
372
+ leasedCategories: ReadonlySet<ApprovalCategory>,
193
373
  grants?: ApprovalGrant[],
374
+ captureMode = false,
375
+ captureIgnored = false,
376
+ gitWorkspace = true,
194
377
  ): ApprovalStateFile {
195
378
  const approvedGrants = grants ?? [];
196
379
 
@@ -203,10 +386,17 @@ export function buildApprovalState(
203
386
  }
204
387
 
205
388
  return {
206
- autoApproveAll,
389
+ autoApproveAll: globalBypass,
390
+ leasedCategories: [...leasedCategories],
207
391
  mcpToolPolicies,
208
392
  approvedGrants,
209
- approvedGrantTokens: approvedGrants.map((g) => grantToken(g.key, g.salient)),
393
+ // The hook matches a tool call's PRIMARY token (content when it can compute a
394
+ // digest from tool_input, else coarse). A content-identified grant authorizes
395
+ // only its exact content; a content-less grant authorizes the coarse token.
396
+ approvedGrantTokens: approvedGrants.map((g) => primaryToken(g.key, g.salient, g.contentDigest)),
397
+ captureMode,
398
+ captureIgnored,
399
+ gitWorkspace,
210
400
  };
211
401
  }
212
402
 
@@ -248,10 +438,21 @@ const DENIAL_LEDGER_FILE = "denials.jsonl";
248
438
  * the same space as grantToken() (base64 of `toolName \n salientArg`), used to
249
439
  * correlate the denial back to the streamed tool call. `toolName` is carried raw
250
440
  * for human-readable debugging of the ledger file.
441
+ *
442
+ * `input` is the authoritative pre-execution tool arguments the hook captured
443
+ * from the Cursor `tool_input` payload (decoded from the ledger's base64 form).
444
+ * It is the cursor analog of the native harness reading the AI-message tool-call
445
+ * args out of graph state at the LangGraph interrupt — the one place the COMPLETE
446
+ * proposed change is in hand before the tool runs. The runner overlays it onto
447
+ * the gated tool call so the approval card can show the proposed content/diff
448
+ * before the user approves. Absent when the hook ran its grep fallback (the Node
449
+ * binary was unavailable), in which case the gate degrades to stream-recovered
450
+ * args exactly as before.
251
451
  */
252
452
  export interface DeniedLedgerEntry {
253
453
  toolName: string;
254
454
  token: string;
455
+ input?: Record<string, unknown>;
255
456
  }
256
457
 
257
458
  /**
@@ -278,6 +479,46 @@ export async function resetDenialLedger(hitlDir: string): Promise<string> {
278
479
  return filePath;
279
480
  }
280
481
 
482
+ /**
483
+ * Watch the denial ledger so the stream loop learns about a hook denial the
484
+ * moment it is written — not on the next tool_call event.
485
+ *
486
+ * The hook appends to denials.jsonl BEFORE Cursor surfaces the failure to the
487
+ * model, so a filesystem notification is the earliest possible signal that a
488
+ * turn must pause. The watcher deliberately does NOT read the file itself: it
489
+ * only flips the caller's dirty flag, and the stream loop confirms with a
490
+ * readDenialLedger() — a notification can fire for the per-turn reset
491
+ * truncation too, and only a read distinguishes "reset to empty" from "denial
492
+ * appended". Watching the DIRECTORY (not the file) survives the file being
493
+ * replaced/truncated between turns.
494
+ *
495
+ * fs.watch is best-effort by platform contract, so this is an accelerator,
496
+ * never the only trigger: the stream loop keeps its tool_call-event read as
497
+ * the backstop. On any watcher error we fall back to that backstop silently.
498
+ *
499
+ * Returns a close function, idempotent and safe to call on every exit path.
500
+ */
501
+ export function watchDenialLedger(hitlDir: string, onDirty: () => void): () => void {
502
+ try {
503
+ const watcher = watch(hitlDir, (_eventType, filename) => {
504
+ if (!filename || filename === DENIAL_LEDGER_FILE) onDirty();
505
+ });
506
+ // Without an error listener a watcher error crashes the process; with one,
507
+ // the watcher just goes quiet and the tool_call backstop takes over.
508
+ watcher.on("error", () => {});
509
+ return () => {
510
+ try {
511
+ watcher.close();
512
+ } catch {
513
+ // Already closed — nothing to release.
514
+ }
515
+ };
516
+ } catch {
517
+ // Watch unsupported on this platform/filesystem — backstop only.
518
+ return () => {};
519
+ }
520
+ }
521
+
281
522
  /**
282
523
  * Read the denial ledger written by the hook during the turn. Missing file →
283
524
  * no denials. Blank or partially-written lines are tolerated (the hook appends
@@ -299,11 +540,12 @@ export async function readDenialLedger(
299
540
  const trimmed = line.trim();
300
541
  if (!trimmed) continue;
301
542
  try {
302
- const obj = JSON.parse(trimmed) as Partial<DeniedLedgerEntry>;
543
+ const obj = JSON.parse(trimmed) as { toolName?: unknown; token?: unknown; input?: unknown };
303
544
  if (typeof obj.token === "string" && obj.token) {
304
545
  entries.push({
305
546
  toolName: typeof obj.toolName === "string" ? obj.toolName : "",
306
547
  token: obj.token,
548
+ input: decodeLedgerInput(obj.input),
307
549
  });
308
550
  }
309
551
  } catch {
@@ -313,6 +555,94 @@ export async function readDenialLedger(
313
555
  return entries;
314
556
  }
315
557
 
558
+ /**
559
+ * Decode the hook's base64(JSON(tool_input)) into the authoritative args object,
560
+ * or undefined when absent/garbage. Tolerant by construction: a bad capture must
561
+ * never drop the denial it rides on — the gate still surfaces, just without the
562
+ * richer preview.
563
+ */
564
+ function decodeLedgerInput(raw: unknown): Record<string, unknown> | undefined {
565
+ if (typeof raw !== "string" || raw.length === 0) return undefined;
566
+ try {
567
+ const json = Buffer.from(raw, "base64").toString("utf-8");
568
+ const parsed = JSON.parse(json);
569
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
570
+ ? (parsed as Record<string, unknown>)
571
+ : undefined;
572
+ } catch {
573
+ return undefined;
574
+ }
575
+ }
576
+
577
+ // ─────────────────────────────────────────────────────────────────────────────
578
+ // Active-turn pointer (runner → stable hook): the per-turn indirection that makes
579
+ // a single, process-cached hook script resolve the CURRENT execution's artifacts
580
+ // ─────────────────────────────────────────────────────────────────────────────
581
+
582
+ const ACTIVE_POINTER_FILE = "active.json";
583
+
584
+ /**
585
+ * The current turn's artifact paths, written by the runner into the WORKSPACE's
586
+ * gate directory and read by the stable hook script on every invocation.
587
+ *
588
+ * Why this exists: the Cursor SDK caches `.cursor/hooks.json` (the hook script
589
+ * PATH) for the runner process, so the script must be STABLE across executions
590
+ * (a per-session script gets cached at the first execution and reused for all
591
+ * later ones — recording their denials to the FIRST session's ledger, leaving the
592
+ * current runner's ledger empty so it completes instead of pausing). The stable
593
+ * script bakes in NO per-session paths; it reads this pointer instead, which the
594
+ * runner repoints every turn to the current execution's state file, denial
595
+ * ledger, and runner PID.
596
+ */
597
+ export interface ActiveTurnPointer {
598
+ /** Absolute path of THIS turn's approval-state file (hook input). */
599
+ stateFile: string;
600
+ /** Absolute path of THIS turn's denial ledger (hook output the runner reads). */
601
+ ledgerFile: string;
602
+ /** PID of the runner that owns THIS turn (the hook's scope-guard anchor). */
603
+ runnerPid: number;
604
+ }
605
+
606
+ /** Absolute path of the active-turn pointer inside a workspace's gate directory. */
607
+ export function activePointerPath(gateDir: string): string {
608
+ return join(gateDir, ACTIVE_POINTER_FILE);
609
+ }
610
+
611
+ /**
612
+ * Atomically point the workspace's stable hook at the current turn's artifacts.
613
+ *
614
+ * Written compactly (the hook's grep fallback parses `"key":"value"` with no
615
+ * spaces) and atomically (write a temp sibling, then rename) so a hook firing
616
+ * concurrently with the write never reads a half-written pointer. Returns the
617
+ * pointer path.
618
+ */
619
+ export async function writeActiveTurnPointer(
620
+ gateDir: string,
621
+ pointer: ActiveTurnPointer,
622
+ ): Promise<string> {
623
+ await mkdir(gateDir, { recursive: true });
624
+ const filePath = activePointerPath(gateDir);
625
+ const tmpPath = `${filePath}.${process.pid}.tmp`;
626
+ await writeFile(tmpPath, JSON.stringify(pointer), "utf-8");
627
+ await rename(tmpPath, filePath);
628
+ return filePath;
629
+ }
630
+
631
+ /**
632
+ * Remove the active-turn pointer on teardown so the gate is INERT between turns:
633
+ * a hook that fires when no turn is active (a leftover cached hooks.json, the
634
+ * user's own IDE) reads no pointer and allows immediately. Best-effort — a
635
+ * teardown failure must never fail the execution, and a stale pointer is itself
636
+ * inert once its runnerPid is gone (the scope guard fails closed to allow).
637
+ */
638
+ export async function removeActiveTurnPointer(gateDir: string): Promise<void> {
639
+ try {
640
+ await rm(activePointerPath(gateDir), { force: true });
641
+ } catch {
642
+ // Already gone or unwritable — nothing to clean.
643
+ }
644
+ }
645
+
316
646
  /**
317
647
  * Reconstruct the adjudicated approvals for a HITL reinvocation directly from
318
648
  * the tool calls in messages.
@@ -331,6 +661,15 @@ export async function readDenialLedger(
331
661
  export interface AdjudicatedApprovals {
332
662
  pendingApprovals: PendingApproval[];
333
663
  decisions: Map<string, ApprovalAction>;
664
+ /**
665
+ * tool-call id -> content digest of the approved edit, for the content-exact
666
+ * grant. Sourced from the persisted `approval_content_digest` field (stable,
667
+ * immune to the size-limit elision that can drop `args`), falling back to a
668
+ * recompute from `args` only when the field is absent (an execution that
669
+ * predates the field). Empty for a non-content tool — the grant then degrades
670
+ * to the coarse token.
671
+ */
672
+ contentDigests: Map<string, string>;
334
673
  }
335
674
 
336
675
  export function reconstructAdjudicatedApprovals(
@@ -338,6 +677,7 @@ export function reconstructAdjudicatedApprovals(
338
677
  ): AdjudicatedApprovals {
339
678
  const pendingApprovals: PendingApproval[] = [];
340
679
  const decisions = new Map<string, ApprovalAction>();
680
+ const contentDigests = new Map<string, string>();
341
681
 
342
682
  for (const msg of messages) {
343
683
  for (const tc of msg.toolCalls) {
@@ -345,6 +685,14 @@ export function reconstructAdjudicatedApprovals(
345
685
  if (tc.approvalAction === ApprovalAction.UNSPECIFIED) continue;
346
686
 
347
687
  decisions.set(tc.id, tc.approvalAction);
688
+ // Prefer the persisted digest (set at the gate from the authoritative
689
+ // captured input, and never elided); recompute from args only for an
690
+ // execution that predates the field.
691
+ contentDigests.set(
692
+ tc.id,
693
+ tc.approvalContentDigest ||
694
+ (tc.args ? contentDigest(tc.args as Record<string, unknown>) : ""),
695
+ );
348
696
  pendingApprovals.push(
349
697
  create(PendingApprovalSchema, {
350
698
  toolCallId: tc.id,
@@ -358,5 +706,5 @@ export function reconstructAdjudicatedApprovals(
358
706
  }
359
707
  }
360
708
 
361
- return { pendingApprovals, decisions };
709
+ return { pendingApprovals, decisions, contentDigests };
362
710
  }