@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
  74. package/dist/activities/execute-deep-agent/hitl.js +11 -3
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +393 -33
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +44 -3
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
  243. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
  244. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  245. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  246. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  247. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
  248. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  249. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  251. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  252. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  253. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  254. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  255. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  256. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  257. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  258. package/src/activities/execute-deep-agent/hitl.ts +23 -7
  259. package/src/activities/execute-deep-agent/index.ts +463 -40
  260. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  261. package/src/activities/execute-deep-agent/setup.ts +223 -125
  262. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  263. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  264. package/src/activities/execute-deep-agent/status-builder.ts +63 -8
  265. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  266. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  267. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  268. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  269. package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
  270. package/src/claimcheck/payload-codec.ts +8 -8
  271. package/src/client/stigmer-client.ts +9 -1
  272. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  273. package/src/middleware/approval-gate.ts +247 -38
  274. package/src/middleware/types.ts +5 -5
  275. package/src/shared/__tests__/activity-input.test.ts +78 -0
  276. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  277. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  278. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  279. package/src/shared/__tests__/args-preview.test.ts +78 -0
  280. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  281. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  282. package/src/shared/__tests__/file-change.test.ts +85 -0
  283. package/src/shared/__tests__/file-tools.test.ts +90 -0
  284. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  285. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  286. package/src/shared/__tests__/model-client.test.ts +162 -0
  287. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  288. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  289. package/src/shared/__tests__/status-offload.test.ts +573 -16
  290. package/src/shared/__tests__/status.test.ts +4 -5
  291. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  292. package/src/shared/__tests__/tool-row.test.ts +221 -0
  293. package/src/shared/activity-input.ts +57 -0
  294. package/src/shared/approval-canonicalize.ts +159 -0
  295. package/src/shared/approval-fingerprint.ts +148 -0
  296. package/src/shared/approval-policy.ts +303 -27
  297. package/src/shared/args-preview.ts +98 -0
  298. package/src/shared/artifact-storage.ts +62 -11
  299. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  300. package/src/shared/file-change.ts +64 -0
  301. package/src/shared/file-tools.ts +169 -0
  302. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  303. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  304. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  305. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  306. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  307. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  308. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  309. package/src/shared/filereview/capture.ts +727 -0
  310. package/src/shared/filereview/cas-substrate.ts +401 -0
  311. package/src/shared/filereview/digest.ts +83 -0
  312. package/src/shared/filereview/events.ts +449 -0
  313. package/src/shared/filereview/git-substrate.ts +555 -0
  314. package/src/shared/filereview/index.ts +60 -0
  315. package/src/shared/filereview/secret-paths.ts +121 -0
  316. package/src/shared/fingerprint-secret.ts +53 -0
  317. package/src/shared/model-client.ts +122 -0
  318. package/src/shared/plan-artifact.ts +0 -2
  319. package/src/shared/status-offload.ts +433 -77
  320. package/src/shared/status.ts +13 -0
  321. package/src/shared/tool-kind.ts +33 -0
  322. package/src/shared/tool-row.ts +135 -0
  323. package/src/shared/workspace/platform-dir.ts +41 -2
  324. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  325. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  326. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  327. package/src/workflows/call-agent-orchestrator.ts +53 -6
  328. package/src/workflows/connect-mcp-server.ts +179 -24
  329. package/src/workflows/types.ts +8 -0
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
  import { dirname, resolve } from "node:path";
12
12
  import { describe, it, expect } from "vitest";
13
13
  import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
- import { classifyTool } from "../tool-kind.js";
14
+ import { classifyTool, toolApprovalCategory } from "../tool-kind.js";
15
15
 
16
16
  const here = dirname(fileURLToPath(import.meta.url));
17
17
  // runner/src/shared/__tests__ -> repo root is five levels up.
@@ -56,3 +56,26 @@ describe("classifyTool", () => {
56
56
  expect(classifyTool("read", "planton")).toBe(ToolKind.FILE_READ);
57
57
  });
58
58
  });
59
+
60
+ describe("toolApprovalCategory", () => {
61
+ it("collapses write AND edit (both taxonomies) onto 'write'", () => {
62
+ for (const name of ["write", "write_file", "Write", "edit", "edit_file", "StrReplace", "EditNotebook"]) {
63
+ expect(toolApprovalCategory(name)).toBe("write");
64
+ }
65
+ });
66
+
67
+ it("maps the delete and shell aliases to their category", () => {
68
+ for (const name of ["delete", "delete_file", "remove_file", "Delete"]) {
69
+ expect(toolApprovalCategory(name)).toBe("delete");
70
+ }
71
+ for (const name of ["shell", "bash", "execute", "execute_command", "run_command", "terminal", "Shell"]) {
72
+ expect(toolApprovalCategory(name)).toBe("shell");
73
+ }
74
+ });
75
+
76
+ it("returns undefined for read-only and unknown built-ins (not gated by category)", () => {
77
+ for (const name of ["read", "ls", "glob", "grep", "search", "think", "totally_unknown"]) {
78
+ expect(toolApprovalCategory(name)).toBeUndefined();
79
+ }
80
+ });
81
+ });
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Unit tests for the shared tool-row file-review presentation helpers:
3
+ * `stampFileEditRow` (the observational-row stamp both harnesses apply to
4
+ * flowed file edits) and `hideToolCallRow`/`isToolCallRowHidden` (the collapse
5
+ * shape kept for denial twins and legacy pre-stamping sessions).
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { create } from "@bufbuild/protobuf";
10
+ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
11
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
12
+ import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
13
+ import { collectSubAgentToolCallIds, hideToolCallRow, isToolCallRowHidden, stampFileEditRow } from "../tool-row.js";
14
+
15
+ describe("stampFileEditRow", () => {
16
+ it("stamps additively: content, status, and identity all survive", () => {
17
+ const tc = create(ToolCallSchema, {
18
+ id: "tc-1",
19
+ name: "edit",
20
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
21
+ result: '{"status":"completed","value":{"diffString":"+hi","linesAdded":1}}',
22
+ argsPreview: '{"path":"src/app.ts"}',
23
+ args: { path: "src/app.ts", old_string: "a", new_string: "b" },
24
+ });
25
+
26
+ stampFileEditRow(tc, "exec-1:0");
27
+
28
+ expect(tc.fileChangeSetId).toBe("exec-1:0");
29
+ // The row is an observational record: everything the streamed card rendered
30
+ // from stays in place (the diff renders from result/args, never argsPreview).
31
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
32
+ expect(tc.args).toEqual({ path: "src/app.ts", old_string: "a", new_string: "b" });
33
+ expect(tc.result).toContain("diffString");
34
+ expect(tc.argsPreview).toBe('{"path":"src/app.ts"}');
35
+ });
36
+
37
+ it("never overwrites an existing stamp (the cross-turn mis-attribution guard)", () => {
38
+ // A resume seeds prior turns' rows into the transcript; the turn-boundary
39
+ // pass re-walks all of them. The stamp must be first-writer-wins or turn N
40
+ // would claim turn N-1's rows.
41
+ const tc = create(ToolCallSchema, {
42
+ id: "tc-old",
43
+ name: "write",
44
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
45
+ args: { path: "notes.md", content: "x" },
46
+ fileChangeSetId: "exec-1:0",
47
+ });
48
+
49
+ stampFileEditRow(tc, "exec-1:1");
50
+
51
+ expect(tc.fileChangeSetId).toBe("exec-1:0");
52
+ });
53
+
54
+ it("withholds content for a secret-like TRACKED path but keeps the path visible (DD-12 D4)", () => {
55
+ // The hook denies secret-like gitignored writes before they flow, but a
56
+ // committed credentials file is outside its scope — the stamp is the last
57
+ // line of defense against persisting its bytes in the transcript.
58
+ const tc = create(ToolCallSchema, {
59
+ id: "tc-secret",
60
+ name: "write",
61
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
62
+ result: "wrote credentials.json",
63
+ argsPreview: '{"path":"config/credentials.json"}',
64
+ args: { path: "config/credentials.json", content: "TOKEN=super-secret" },
65
+ });
66
+
67
+ stampFileEditRow(tc, "exec-1:0");
68
+
69
+ expect(tc.fileChangeSetId).toBe("exec-1:0");
70
+ expect(tc.args).toEqual({ path: "config/credentials.json" }); // path only, no body
71
+ expect(tc.result).toBe("");
72
+ expect(tc.argsPreview).toBe("");
73
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED); // still visible
74
+ });
75
+
76
+ it("fail-closes when the path cannot be determined: content withheld", () => {
77
+ const tc = create(ToolCallSchema, {
78
+ id: "tc-weird",
79
+ name: "write",
80
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
81
+ result: "ok",
82
+ args: { unrecognized_shape: "payload" },
83
+ });
84
+
85
+ stampFileEditRow(tc, "exec-1:0");
86
+
87
+ expect(tc.fileChangeSetId).toBe("exec-1:0");
88
+ expect(tc.args).toBeUndefined();
89
+ expect(tc.result).toBe("");
90
+ });
91
+ });
92
+
93
+ describe("hideToolCallRow", () => {
94
+ it("collapses a completed file-edit row to the hidden shape", () => {
95
+ const tc = create(ToolCallSchema, {
96
+ id: "tc-1",
97
+ name: "write",
98
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
99
+ result: "wrote file",
100
+ argsPreview: "path=src/app.ts",
101
+ args: { file_path: "src/app.ts", content: "console.log('hi')" },
102
+ requiresApproval: true,
103
+ approvalMessage: "Write file",
104
+ });
105
+
106
+ hideToolCallRow(tc);
107
+
108
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
109
+ expect(tc.requiresApproval).toBe(false);
110
+ expect(tc.result).toBe("");
111
+ expect(tc.error).toBe("");
112
+ expect(tc.argsPreview).toBe("");
113
+ expect(tc.approvalMessage).toBe("");
114
+ expect(tc.completedAt).not.toBe("");
115
+ // Identity is preserved (append-only): id + name survive the collapse.
116
+ expect(tc.id).toBe("tc-1");
117
+ expect(tc.name).toBe("write");
118
+ });
119
+
120
+ it("scrubs args so a hidden row carries no content (design doc 12, D4)", () => {
121
+ // A file-mutating tool's args hold the full write body. For a secret-like
122
+ // path this content would otherwise persist into the transcript / Temporal
123
+ // history, defeating the never-persist-secret-contents contract. The hidden
124
+ // row is redundant with the file_change_set, so args must be dropped.
125
+ const tc = create(ToolCallSchema, {
126
+ id: "tc-secret",
127
+ name: "write",
128
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
129
+ args: { file_path: ".env", content: "API_KEY=super-secret-value" },
130
+ });
131
+
132
+ hideToolCallRow(tc);
133
+
134
+ expect(tc.args).toBeUndefined();
135
+ expect(isToolCallRowHidden(tc)).toBe(true);
136
+ });
137
+
138
+ it("is idempotent", () => {
139
+ const tc = create(ToolCallSchema, {
140
+ id: "tc-1",
141
+ name: "edit",
142
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
143
+ result: "ok",
144
+ });
145
+ hideToolCallRow(tc);
146
+ const firstCompletedAt = tc.completedAt;
147
+ hideToolCallRow(tc);
148
+ expect(isToolCallRowHidden(tc)).toBe(true);
149
+ expect(tc.completedAt).toBe(firstCompletedAt); // not overwritten
150
+ });
151
+ });
152
+
153
+ describe("isToolCallRowHidden", () => {
154
+ it("recognizes a hidden row", () => {
155
+ const tc = create(ToolCallSchema, { id: "x", name: "write", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "y" });
156
+ expect(isToolCallRowHidden(tc)).toBe(false);
157
+ hideToolCallRow(tc);
158
+ expect(isToolCallRowHidden(tc)).toBe(true);
159
+ });
160
+
161
+ it("does not treat a live WAITING_APPROVAL row as hidden", () => {
162
+ const tc = create(ToolCallSchema, {
163
+ id: "x",
164
+ name: "shell",
165
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
166
+ requiresApproval: true,
167
+ });
168
+ expect(isToolCallRowHidden(tc)).toBe(false);
169
+ });
170
+
171
+ it("does not treat a SKIPPED row with lingering args as hidden", () => {
172
+ // A row collapsed by an older code path may still carry args; the predicate
173
+ // must report it as not-yet-hidden so it gets re-scrubbed rather than skipped.
174
+ const tc = create(ToolCallSchema, {
175
+ id: "x",
176
+ name: "write",
177
+ status: ToolCallStatus.TOOL_CALL_SKIPPED,
178
+ args: { file_path: ".env", content: "leftover" },
179
+ });
180
+ expect(isToolCallRowHidden(tc)).toBe(false);
181
+ });
182
+ });
183
+
184
+ describe("collectSubAgentToolCallIds", () => {
185
+ function subAgent(id: string, ...toolCallIds: string[]) {
186
+ return create(SubAgentExecutionSchema, {
187
+ id,
188
+ messages: [
189
+ create(AgentMessageSchema, {
190
+ type: 1, // MESSAGE_AI
191
+ toolCalls: toolCallIds.map((tcId) =>
192
+ create(ToolCallSchema, { id: tcId, name: "edit" }),
193
+ ),
194
+ }),
195
+ ],
196
+ });
197
+ }
198
+
199
+ it("returns an empty set for no sub-agents", () => {
200
+ expect(collectSubAgentToolCallIds([]).size).toBe(0);
201
+ });
202
+
203
+ it("flattens tool-call ids across sub-agents and messages", () => {
204
+ const ids = collectSubAgentToolCallIds([
205
+ subAgent("sa-1", "tc-a", "tc-b"),
206
+ subAgent("sa-2", "tc-c"),
207
+ ]);
208
+ expect([...ids].sort()).toEqual(["tc-a", "tc-b", "tc-c"]);
209
+ });
210
+
211
+ it("collects ids across multiple messages of one sub-agent", () => {
212
+ const sa = create(SubAgentExecutionSchema, {
213
+ id: "sa-1",
214
+ messages: [
215
+ create(AgentMessageSchema, { type: 1, toolCalls: [create(ToolCallSchema, { id: "tc-a", name: "edit" })] }),
216
+ create(AgentMessageSchema, { type: 1, toolCalls: [create(ToolCallSchema, { id: "tc-b", name: "write" })] }),
217
+ ],
218
+ });
219
+ expect([...collectSubAgentToolCallIds([sa]).values()].sort()).toEqual(["tc-a", "tc-b"]);
220
+ });
221
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Typed Temporal activity input shared by ExecuteCursor and ExecuteDeepAgent.
3
+ *
4
+ * The Go and Java control planes now invoke these activities with a single
5
+ * object carrying snake_case keys (mirroring the polyglot workflow-input
6
+ * convention) instead of positional string args. This ends the prior drift
7
+ * where Go sent (executionId, threadId) and Java sent
8
+ * (executionId, threadId, invokerIdentityAccountId) while this single runner
9
+ * read only the first two positionally.
10
+ *
11
+ * TRANSITIONAL DUAL-SHAPE: the activity is MaximumAttempts=1 (a failed activity
12
+ * fails the execution with no retry), and the control plane and this runner
13
+ * deploy separately. To make the rollout order-independent, the runner accepts
14
+ * BOTH the new object and the legacy positional args. Deploy the runner first,
15
+ * then the control planes; once both are everywhere, drop the positional branch
16
+ * (and the `string` arms below).
17
+ */
18
+ export interface ExecuteActivityInput {
19
+ readonly execution_id: string;
20
+ readonly thread_id: string;
21
+ /** Carried for cross-edition parity; the runner hydrates the invoker from the DB. */
22
+ readonly invoker_identity_account_id?: string;
23
+ /**
24
+ * Monotonic HITL-cycle index within this execution: 0 on the first invocation,
25
+ * then the workflow's approvalCycle on each reinvocation. The Cursor producer
26
+ * mints its deterministic file-review change-set id ({@code executionId:turnSeq})
27
+ * from it. Absent on the legacy positional wire shape (defaults to 0).
28
+ */
29
+ readonly turn_seq?: number;
30
+ }
31
+
32
+ export interface NormalizedActivityInput {
33
+ readonly executionId: string;
34
+ readonly threadId: string;
35
+ /** HITL-cycle index for this invocation; 0 on the first turn / legacy wire shape. */
36
+ readonly turnSeq: number;
37
+ }
38
+
39
+ /**
40
+ * Normalizes either the new typed object or the legacy positional args into the
41
+ * `{ executionId, threadId, turnSeq }` the inner activity path consumes.
42
+ * `thread_id` is empty on a first invocation (new harness state) and `turn_seq`
43
+ * defaults to 0 (first turn, or the legacy positional shape that never carried it).
44
+ */
45
+ export function normalizeActivityInput(
46
+ arg0: ExecuteActivityInput | string | undefined,
47
+ arg1?: string,
48
+ ): NormalizedActivityInput {
49
+ if (arg0 !== null && typeof arg0 === "object") {
50
+ return {
51
+ executionId: arg0.execution_id ?? "",
52
+ threadId: arg0.thread_id ?? "",
53
+ turnSeq: arg0.turn_seq ?? 0,
54
+ };
55
+ }
56
+ return { executionId: arg0 ?? "", threadId: arg1 ?? "", turnSeq: 0 };
57
+ }
@@ -0,0 +1,159 @@
1
+ // Deterministic canonicalization of a tool action into a byte-stable form.
2
+ // Domain: HITL approval. This is the cross-language contract the approval
3
+ // fingerprint (Phase 2) will hash; Phase 1 *defines and computes* it without
4
+ // hashing, so Go/Java/TS can be pinned to byte-identical output via the shared
5
+ // vector corpus (apis/testdata/hitl/canonicalization/).
6
+ //
7
+ // Why this exists: the March 2026 dedup bug was caused by hashing divergent
8
+ // representations of the same action (display args in one place, raw args in
9
+ // another). The fix is a single, explicit normalization pass — applied at ONE
10
+ // layer — that erases incidental differences (path separators, workspace
11
+ // location, shell whitespace, MCP slug case, secret values) before any hash.
12
+
13
+ import { createHash } from "node:crypto";
14
+ import { posix } from "node:path";
15
+
16
+ export interface ToolActionInput {
17
+ // Engine tool name (Cursor PascalCase or native snake_case). Identity is kept
18
+ // as-given (only trimmed) because ToolKind already normalizes cross-harness
19
+ // naming elsewhere; canonicalization must not silently merge distinct tools.
20
+ toolName: string;
21
+ // MCP server slug, or empty/undefined for built-in tools.
22
+ mcpServerSlug?: string;
23
+ // File paths the action targets (absolute or workspace-relative).
24
+ paths?: string[];
25
+ // Shell command line, when this is a shell action.
26
+ shellCommand?: string;
27
+ // Remaining structured arguments.
28
+ args?: Record<string, unknown>;
29
+ // Top-level keys within `args` whose values are secrets: replaced by a stable
30
+ // digest so the canonical form never carries cleartext.
31
+ secretKeys?: string[];
32
+ // Workspace root used to rewrite absolute paths to workspace-relative ones, so
33
+ // the same edit canonicalizes identically regardless of checkout location.
34
+ workspaceRoot?: string;
35
+ }
36
+
37
+ // The normalized action. Field order here is irrelevant — canonicalJson sorts
38
+ // keys — but the shape is the contract Go/Java must reproduce.
39
+ export interface CanonicalToolAction {
40
+ toolName: string;
41
+ mcpServerSlug: string;
42
+ paths: string[];
43
+ shellCommand: string;
44
+ args: Record<string, unknown>;
45
+ }
46
+
47
+ export function canonicalizeToolAction(input: ToolActionInput): CanonicalToolAction {
48
+ return {
49
+ toolName: input.toolName.trim(),
50
+ // Slugs are case-insensitive identity; lowercase so "GitHub" == "github".
51
+ mcpServerSlug: (input.mcpServerSlug ?? "").trim().toLowerCase(),
52
+ paths: normalizePaths(input.paths ?? [], input.workspaceRoot),
53
+ shellCommand: normalizeShellCommand(input.shellCommand ?? ""),
54
+ args: redactSecrets(input.args ?? {}, new Set(input.secretKeys ?? [])),
55
+ };
56
+ }
57
+
58
+ // canonicalToolActionJson is the single entry point: normalize, then serialize
59
+ // to canonical JSON. The returned string is the byte-stable contract value.
60
+ export function canonicalToolActionJson(input: ToolActionInput): string {
61
+ return canonicalJson(canonicalizeToolAction(input));
62
+ }
63
+
64
+ // canonicalJson serializes a value to RFC 8785-style canonical JSON: object keys
65
+ // sorted by UTF-16 code unit, no insignificant whitespace, UTF-8 output.
66
+ //
67
+ // The input domain is deliberately constrained to strings, booleans, null,
68
+ // integers, arrays, and plain objects — the shape of a normalized tool action.
69
+ // Non-integer numbers are rejected rather than implementing RFC 8785's full
70
+ // number-formatting algorithm, keeping this hand-rolled serializer small and
71
+ // provably cross-language without a third-party dependency. Revisit if the
72
+ // canonical domain ever needs floats.
73
+ export function canonicalJson(value: unknown): string {
74
+ return serialize(value);
75
+ }
76
+
77
+ function serialize(v: unknown): string {
78
+ if (v === null) return "null";
79
+ switch (typeof v) {
80
+ case "boolean":
81
+ return v ? "true" : "false";
82
+ case "number":
83
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
84
+ if (!Number.isInteger(v)) {
85
+ throw new Error("canonicalJson: non-integer numbers are not part of the canonical contract");
86
+ }
87
+ return String(v);
88
+ case "string":
89
+ // JSON.stringify produces the same minimal escaping and raw UTF-8 for
90
+ // non-ASCII that RFC 8785 mandates for these inputs.
91
+ return JSON.stringify(v);
92
+ case "object":
93
+ if (Array.isArray(v)) return "[" + v.map(serialize).join(",") + "]";
94
+ return serializeObject(v as Record<string, unknown>);
95
+ default:
96
+ throw new Error(`canonicalJson: unsupported type ${typeof v}`);
97
+ }
98
+ }
99
+
100
+ function serializeObject(obj: Record<string, unknown>): string {
101
+ // `undefined`-valued keys are dropped (they have no JSON representation);
102
+ // remaining keys are sorted by UTF-16 code unit, which is JS's default string
103
+ // ordering and matches RFC 8785.
104
+ const keys = Object.keys(obj)
105
+ .filter((k) => obj[k] !== undefined)
106
+ .sort(compareUtf16);
107
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + serialize(obj[k]));
108
+ return "{" + parts.join(",") + "}";
109
+ }
110
+
111
+ function compareUtf16(a: string, b: string): number {
112
+ return a < b ? -1 : a > b ? 1 : 0;
113
+ }
114
+
115
+ function normalizePaths(paths: string[], workspaceRoot?: string): string[] {
116
+ return paths.map((p) => normalizeOnePath(p, workspaceRoot)).sort(compareUtf16);
117
+ }
118
+
119
+ function normalizeOnePath(p: string, workspaceRoot?: string): string {
120
+ // Separator-normalize to forward slashes first so Windows and POSIX agree.
121
+ let s = p.replace(/\\/g, "/");
122
+ if (workspaceRoot) {
123
+ const root = workspaceRoot.replace(/\\/g, "/");
124
+ if (s === root) {
125
+ s = "";
126
+ } else if (s.startsWith(root + "/")) {
127
+ s = s.slice(root.length + 1);
128
+ }
129
+ }
130
+ // Collapse `.`/`..` segments deterministically. posix.normalize("") -> ".".
131
+ return s === "" ? "" : posix.normalize(s);
132
+ }
133
+
134
+ function normalizeShellCommand(cmd: string): string {
135
+ // Collapse runs of whitespace to a single space and trim. The argument VALUE
136
+ // (the command) is what matters; incidental spacing is not identity.
137
+ return cmd.replace(/\s+/g, " ").trim();
138
+ }
139
+
140
+ function redactSecrets(
141
+ args: Record<string, unknown>,
142
+ secretKeys: Set<string>,
143
+ ): Record<string, unknown> {
144
+ if (secretKeys.size === 0) return args;
145
+ const out: Record<string, unknown> = {};
146
+ for (const [k, v] of Object.entries(args)) {
147
+ out[k] = secretKeys.has(k) ? redactValue(v) : v;
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function redactValue(v: unknown): string {
153
+ // Redact-but-stable: a SHA-256 digest keeps the canonical form stable across
154
+ // runs without ever placing the secret in cleartext. This is NOT the approval
155
+ // fingerprint — that is an HMAC keyed on a Stigmer secret, introduced in
156
+ // Phase 2. This digest is unkeyed redaction only.
157
+ const material = typeof v === "string" ? v : canonicalJson(v);
158
+ return "sha256:" + createHash("sha256").update(material, "utf8").digest("hex");
159
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Approval fingerprint — the exact-match enforcement identity for the HITL Tool
3
+ * Execution Gateway (Phase 2).
4
+ *
5
+ * A fingerprint is `HMAC-SHA256(key, canonicalForm)` over a canonicalized tool
6
+ * action (see {@link file://./approval-canonicalize.ts}). It answers exactly one
7
+ * question at the moment of side effect: "is *this* action the one the user
8
+ * approved?" An approval is bound to the canonical identity of the action, not to
9
+ * the transient id of the proposing tool call, so a re-driven action that differs
10
+ * from what was approved is re-asked — never silently executed.
11
+ *
12
+ * Why HMAC and not a bare SHA-256 (binding canonical decision,
13
+ * design-decisions/approval-fingerprint-vs-march-rollback.md, Rule 4): the
14
+ * fingerprint is an authorization token, not a correlation key. Keying it under a
15
+ * Stigmer-held secret means a model (or a compromised workspace) cannot forge a
16
+ * value that the gateway will accept. In Phase 2 the fingerprint is
17
+ * recompute-and-compare at one trusted layer (no bearer token crosses a wire), so
18
+ * the anti-forgery property is forward-looking — it earns its keep when a lease
19
+ * later becomes a server-issued bearer token (Phase 7). We pay the small cost now
20
+ * to avoid a migration that re-keys every persisted approval later.
21
+ *
22
+ * Two fidelities share this one canonicalization core, because the two
23
+ * enforcement substrates differ (see the Phase-2 plan, "two enforcement
24
+ * substrates, not one"):
25
+ *
26
+ * - FULL ({@link computeApprovalFingerprint}) — HMAC over the full canonical
27
+ * action (tool + paths + shellCommand + args). Used by the in-process
28
+ * deep-agent gateway, where approve-time and execute-time observe the same
29
+ * arg shape (LangGraph checkpoint replay), so the action can be matched at
30
+ * full fidelity.
31
+ * - COARSE ({@link computeCoarseApprovalFingerprint}) — HMAC over
32
+ * (category, salient) only. Used by the out-of-process Cursor deny-oracle
33
+ * hook, whose stdin payload names the same action with a different taxonomy
34
+ * (`Write` vs `edit`, `file_path` vs `path`) and cannot reproduce the full
35
+ * args. The coarse projection is the documented, substrate-forced coarsening
36
+ * that lets the hook-side and stream-side fingerprints agree; it is the
37
+ * successor identity for execute-cursor's grant token.
38
+ *
39
+ * The fingerprint is enforcement-only and never a correlation key — correlation
40
+ * stays `approval_request_id` + `tool_call_id`.
41
+ */
42
+
43
+ import { createHmac, type BinaryLike } from "node:crypto";
44
+ import {
45
+ canonicalJson,
46
+ canonicalToolActionJson,
47
+ canonicalizeToolAction,
48
+ type ToolActionInput,
49
+ } from "./approval-canonicalize.js";
50
+ import { toolApprovalCategory } from "./tool-kind.js";
51
+
52
+ /**
53
+ * Version tag prefixed to every fingerprint. Bumping it is the migration lever
54
+ * if the canonical form or the MAC primitive ever changes: an old lease and a
55
+ * new computation will not compare equal, so a version skew re-asks (safe) rather
56
+ * than silently mismatching. Keep in lockstep with the Go/Java editions.
57
+ */
58
+ export const APPROVAL_FINGERPRINT_VERSION = "v1";
59
+
60
+ /** The HMAC key. Raw bytes (Buffer) in production; a UTF-8 string in tests. */
61
+ export type FingerprintKey = BinaryLike;
62
+
63
+ /**
64
+ * Full-fidelity fingerprint for the in-process deep-agent gateway. Distinct
65
+ * actions (different tool, paths, command, or args) yield distinct fingerprints;
66
+ * the same action is byte-stable across re-invocations.
67
+ */
68
+ export function computeApprovalFingerprint(key: FingerprintKey, input: ToolActionInput): string {
69
+ return tagged(hmacHex(key, canonicalToolActionJson(input)));
70
+ }
71
+
72
+ /**
73
+ * The substrate-coarsened identity of a tool action — the only fidelity the
74
+ * Cursor deny-oracle hook can reproduce from its stdin payload.
75
+ *
76
+ * Mirrors execute-cursor's `toolIdentity` so the two converge on one definition:
77
+ * - `tool` is the cross-taxonomy {@link toolApprovalCategory} (`write`/`delete`/
78
+ * `shell`) for gated built-ins, the tool name for MCP tools (whose name is
79
+ * stable across layers), or the trimmed tool name for non-gated built-ins
80
+ * (never lease-matched, so the fallback is harmless).
81
+ * - `salient` is the single resource the action targets (a normalized path or the
82
+ * shell command), reusing the full form's normalization so one rule governs
83
+ * both fidelities. Empty for MCP tools, which are matched by `tool` alone.
84
+ */
85
+ export interface CoarseToolIdentity {
86
+ tool: string;
87
+ mcpServerSlug: string;
88
+ salient: string;
89
+ }
90
+
91
+ export function coarseToolIdentity(input: ToolActionInput): CoarseToolIdentity {
92
+ const canonical = canonicalizeToolAction(input);
93
+ if (canonical.mcpServerSlug) {
94
+ return { tool: input.toolName.trim(), mcpServerSlug: canonical.mcpServerSlug, salient: "" };
95
+ }
96
+ const category = toolApprovalCategory(input.toolName);
97
+ return {
98
+ tool: category ?? input.toolName.trim(),
99
+ mcpServerSlug: "",
100
+ salient: canonical.paths[0] || canonical.shellCommand || "",
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Fingerprint an already-reduced {@link CoarseToolIdentity}. Split out from
106
+ * {@link computeCoarseApprovalFingerprint} so a substrate that has *already*
107
+ * reduced a tool call to its (tool, mcpServerSlug, salient) identity — the Cursor
108
+ * harness, whose hook and stream both key on a raw, un-normalized salient that
109
+ * the bash hook can reproduce without a workspace root — can fingerprint that
110
+ * exact identity through the one shared HMAC+canonical-JSON path, instead of
111
+ * re-deriving the salient via {@link canonicalizeToolAction} (which normalizes
112
+ * paths and would diverge from the hook's raw value).
113
+ */
114
+ export function fingerprintCoarseIdentity(key: FingerprintKey, identity: CoarseToolIdentity): string {
115
+ return tagged(hmacHex(key, canonicalJson(identity)));
116
+ }
117
+
118
+ /**
119
+ * Coarse fingerprint for the out-of-process Cursor hook. By construction, two
120
+ * actions that name the same operation in different taxonomies (`Write` vs
121
+ * `edit`) over the same resource collapse to one fingerprint — this is what makes
122
+ * the hook-side (deny/grant) and stream-side (reconciliation) values agree.
123
+ */
124
+ export function computeCoarseApprovalFingerprint(key: FingerprintKey, input: ToolActionInput): string {
125
+ return fingerprintCoarseIdentity(key, coarseToolIdentity(input));
126
+ }
127
+
128
+ /**
129
+ * Derive the per-execution fingerprint key from a runner-held master secret.
130
+ *
131
+ * The key is scoped to one `execution_id`: stable across Temporal re-invocations
132
+ * of the same execution (the gateway approves on one invocation and enforces on
133
+ * the next), and isolated between executions so a fingerprint approved for one
134
+ * cannot be replayed against another. The master-secret source is wired when the
135
+ * gateway first consumes the fingerprint (Slices C/D); until then this is
136
+ * exercised only by tests against a fixed master secret.
137
+ */
138
+ export function deriveExecutionFingerprintKey(masterSecret: FingerprintKey, executionId: string): Buffer {
139
+ return createHmac("sha256", masterSecret).update(executionId, "utf8").digest();
140
+ }
141
+
142
+ function hmacHex(key: FingerprintKey, canonical: string): string {
143
+ return createHmac("sha256", key).update(canonical, "utf8").digest("hex");
144
+ }
145
+
146
+ function tagged(mac: string): string {
147
+ return `${APPROVAL_FINGERPRINT_VERSION}:${mac}`;
148
+ }