@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
@@ -54,23 +54,45 @@
54
54
  * recorded here correlates to the streamed tool call, and an approval grant
55
55
  * matches the agent's re-attempt on reinvocation.
56
56
  *
57
+ * Content-exact identity (the sibling-hole fix): for a file edit the coarse
58
+ * (category, salient) is not enough — approving one edit to a file must not let
59
+ * a DIFFERENT edit to the SAME file ride through. So the hook ALSO computes a
60
+ * CONTENT token `base64(category \n salient \n contentDigest)`, where
61
+ * contentDigest is a sha256 over the edit content (mirror of file-tools.ts
62
+ * contentDigest; see buildContentDigestScript). It allows a built-in when EITHER
63
+ * the content token (a file edit approved with this exact content) OR the coarse
64
+ * token (shell/delete, or a content-less degrade) is granted, and records the
65
+ * content token as the denial identity. The runner grants the content token when
66
+ * it has the approved content (the persisted approval_content_digest), so a
67
+ * sibling edit re-gates; it degrades to the coarse grant only when the content is
68
+ * unrecoverable.
69
+ *
57
70
  * Policy evaluation order (first match wins). The model is "gate the dangerous
58
71
  * set, allow the rest" — matching the native harness and avoiding denial of
59
72
  * auto-approved MCP tools (which are absent from mcpToolPolicies):
60
73
  * 0. Scope guard: not the runner's own agent → allow (never touch the ledger)
61
- * 1. Missing state file → deny (fail-closed); autoApproveAll allow
74
+ * 1. Missing state file → deny (fail-closed); autoApproveAll (the pre-armed
75
+ * spec.auto_approve_all global bypass) → allow
62
76
  * 2. beforeMCPExecution event → MCP tool present in mcpToolPolicies
63
77
  * (require-approval):
64
78
  * a. name token in approvedGrantTokens → allow (reinvocation grant)
65
79
  * b. otherwise → record denial, deny
66
- * (auto-approved / unlisted MCP tools fall through → allow)
80
+ * (auto-approved / unlisted MCP tools fall through → allow; a server-scoped
81
+ * lease drops the server's tools from mcpToolPolicies, so they fall through)
67
82
  * 3. preToolUse event → gated built-in (category non-empty):
68
83
  * a. identity token in approvedGrantTokens → allow (reinvocation grant)
69
- * b. otherwiserecord denial, deny
84
+ * b. category in leasedCategories allow (run-lifetime scoped lease)
85
+ * c. otherwise → record denial, deny
70
86
  * (read-only / ungated built-ins fall through → allow)
71
87
  */
72
88
 
73
89
  import { SALIENT_ARG_FIELDS, getBuiltInGatedCategories } from "./approval-policy.js";
90
+ import {
91
+ EDIT_OLD_FIELDS,
92
+ EDIT_NEW_FIELDS,
93
+ WRITE_CONTENT_FIELDS,
94
+ } from "../../shared/file-tools.js";
95
+ import { buildObservationStagingScript, CAS_OBSERVATIONS_DIRNAME } from "./cas-observations.js";
74
96
 
75
97
  // Shown to the model when the gate denies a tool call. It must NOT teach the
76
98
  // model to ask for permission in prose or to "stop and wait" — that framing
@@ -83,9 +105,24 @@ import { SALIENT_ARG_FIELDS, getBuiltInGatedCategories } from "./approval-policy
83
105
  // quotes, apostrophes, or backslashes.
84
106
  const APPROVAL_REQUIRED_AGENT_MESSAGE =
85
107
  "This action has been submitted to the user for approval automatically; you " +
86
- "do not need to ask for permission. Do not retry it or attempt a workaround " +
87
- "for this action. The platform will resume you automatically after the user " +
88
- "responds continue with the rest of the task.";
108
+ "do not need to ask for permission. This is the platform approval gate working " +
109
+ "as intended it is not an error and not a Cursor misconfiguration, so never " +
110
+ "tell the user to change Cursor settings or enable hooks. Do not retry it or " +
111
+ "attempt a workaround for this action. The platform will resume you " +
112
+ "automatically after the user responds — continue with the rest of the task.";
113
+
114
+ // Shown to the model when a secret-like gitignored write is hard-blocked (DD-E /
115
+ // DD-18). Unlike APPROVAL_REQUIRED_AGENT_MESSAGE, this must NOT promise a resume:
116
+ // the write is discarded and never captured for review, so the model must move on
117
+ // rather than wait or retry. Same embedding constraint (single-quoted bash echo
118
+ // of a JSON object): no double quotes, apostrophes, or backslashes.
119
+ const SECRET_BLOCKED_AGENT_MESSAGE =
120
+ "This file was blocked for security because its path matches a secret-like " +
121
+ "pattern Stigmer will not capture for review. Nothing was written. This is the " +
122
+ "platform safety gate working as intended — it is not an error and not a Cursor " +
123
+ "misconfiguration, so never tell the user to change Cursor settings or enable " +
124
+ "hooks. Do not retry this write or attempt a workaround; the write will not be " +
125
+ "applied. Continue with the rest of the task.";
89
126
 
90
127
  /**
91
128
  * Build the bash `case` arms that map an incoming hook `tool_name` to its
@@ -107,20 +144,53 @@ function buildCategoryCaseArms(): string {
107
144
  return arms.join("\n");
108
145
  }
109
146
 
147
+ /**
148
+ * Build the inline content-digest extractor — a BYTE-IDENTICAL MIRROR of
149
+ * {@link file://../../shared/file-tools.ts} `contentDigest()`.
150
+ *
151
+ * Computes the same `sha256(JSON.stringify(["w", content]))` /
152
+ * `sha256(JSON.stringify(["e", old, new]))` from the parsed tool_input `a`,
153
+ * using the SAME union field lists injected from file-tools.ts (so the
154
+ * file_path/path & content/contents cross-layer name divergence is normalized
155
+ * identically) and the SAME Node binary as the runner — so the hook-side and
156
+ * runner-side digests agree. Any change to the format here or in
157
+ * file-tools.ts MUST be mirrored in the other. Empty (`dig===""`) for a tool
158
+ * with no edit content (shell/delete/read/MCP).
159
+ *
160
+ * Authored as part of a single-quoted bash string, so the JS must not contain
161
+ * single quotes (JSON.stringify emits double quotes).
162
+ */
163
+ function buildContentDigestScript(): string {
164
+ const wc = JSON.stringify(WRITE_CONTENT_FIELDS);
165
+ const eo = JSON.stringify(EDIT_OLD_FIELDS);
166
+ const en = JSON.stringify(EDIT_NEW_FIELDS);
167
+ return [
168
+ `const pick=(fl)=>{for(const f of fl){const v=a[f];if(typeof v==="string")return v;}return null;};`,
169
+ `const sha=(x)=>require("crypto").createHash("sha256").update(x,"utf8").digest("hex");`,
170
+ `let dig="";`,
171
+ `const _wc=pick(${wc});`,
172
+ `if(_wc!==null){dig=sha(JSON.stringify(["w",_wc]));}`,
173
+ `else{const _o=pick(${eo}),_n=pick(${en});if(_o!==null||_n!==null){dig=sha(JSON.stringify(["e",_o===null?"":_o,_n===null?"":_n]));}}`,
174
+ ].join("");
175
+ }
176
+
110
177
  /**
111
178
  * Build the inline Node.js identity extractor embedded in the hook script.
112
179
  *
113
180
  * Parses the hook's stdin JSON properly (the bash fallback's grep truncates
114
- * string values at the first escaped quote) and emits five lines: tool_name,
115
- * canonical category, identity token, MCP name-token, and hook_event_name (the
116
- * event discriminator: `preToolUse` for built-ins, `beforeMCPExecution` for MCP).
117
- * The token encodings must stay byte-identical to grantToken() in
118
- * approval-state.ts.
181
+ * string values at the first escaped quote) and emits SEVEN lines: tool_name,
182
+ * canonical category, coarse identity token, MCP name-token, hook_event_name
183
+ * (the event discriminator: `preToolUse` for built-ins, `beforeMCPExecution`
184
+ * for MCP), base64(JSON(tool_input)) the authoritative pre-execution args the
185
+ * runner overlays onto the gated tool call for the approval preview — and the
186
+ * CONTENT token (base64(category \n salient \n contentDigest), empty when the
187
+ * tool has no edit content). The token encodings must stay byte-identical to
188
+ * grantToken()/contentToken() in approval-state.ts.
119
189
  *
120
190
  * Authored as a single-quoted bash string, so the JS must not contain single
121
- * quotes. The category map and salient field list are baked from
122
- * approval-policy.ts — the same source the runner uses — so the two sides can
123
- * never disagree.
191
+ * quotes. The category map, salient field list, and edit/content field lists are
192
+ * baked from approval-policy.ts / file-tools.ts — the same source the runner
193
+ * uses — so the two sides can never disagree.
124
194
  */
125
195
  function buildNodeIdentityScript(): string {
126
196
  const categoryMap: Record<string, string> = {};
@@ -133,51 +203,79 @@ function buildNodeIdentityScript(): string {
133
203
  `const t=JSON.parse(require("fs").readFileSync(0,"utf8"));`,
134
204
  `const name=typeof t.tool_name==="string"?t.tool_name:"";`,
135
205
  `const cat=(${categories})[name]||"";`,
136
- `const a=(t.tool_input&&typeof t.tool_input==="object")?t.tool_input:{};`,
206
+ // tool_input is an object for built-ins (preToolUse) but a JSON STRING for
207
+ // MCP tools (beforeMCPExecution). Parse the string form so the captured
208
+ // input is the same object shape on both paths.
209
+ `let a={};`,
210
+ `if(t.tool_input&&typeof t.tool_input==="object"){a=t.tool_input;}`,
211
+ `else if(typeof t.tool_input==="string"){try{const p=JSON.parse(t.tool_input);if(p&&typeof p==="object")a=p;}catch(e){}}`,
137
212
  `let s="";`,
138
213
  `for(const f of ${fields}){const v=a[f];if(typeof v==="string"&&v){s=v;break;}}`,
139
214
  `const b=(x)=>Buffer.from(x,"utf8").toString("base64");`,
215
+ // Content digest of the edit (mirror of file-tools.ts contentDigest); `dig`
216
+ // is "" for a non-edit tool, in which case the content token (line 7) is "".
217
+ buildContentDigestScript(),
140
218
  `const ev=typeof t.hook_event_name==="string"?t.hook_event_name:"";`,
141
- `process.stdout.write(name+"\\n"+cat+"\\n"+b(cat+"\\n"+s)+"\\n"+b(name+"\\n")+"\\n"+ev);`,
219
+ // Line 6 is base64(JSON(tool_input)): the AUTHORITATIVE pre-execution args
220
+ // the runner overlays onto the gated tool call so the approval card can show
221
+ // the proposed change before the user approves. Base64 keeps the bash side
222
+ // free of quoting/escaping concerns even for large multi-line file content.
223
+ // Line 7 is the CONTENT token (empty when no digest) — the exact-identity
224
+ // grant the runner authorizes for a file edit. Line 8 is base64(salient) —
225
+ // the raw resource value (file path / command) capture mode needs to run
226
+ // `git check-ignore` on a file path; base64 keeps newlines/quotes out of the
227
+ // line-oriented bash parse.
228
+ `process.stdout.write(name+"\\n"+cat+"\\n"+b(cat+"\\n"+s)+"\\n"+b(name+"\\n")+"\\n"+ev+"\\n"+b(JSON.stringify(a))+"\\n"+(dig?b(cat+"\\n"+s+"\\n"+dig):"")+"\\n"+b(s));`,
142
229
  ].join("");
143
230
  }
144
231
 
145
232
  /**
146
- * Generates the bash hook script content.
233
+ * Generates the STABLE bash hook script content.
234
+ *
235
+ * The script is STABLE across executions in a runner process — its only inputs
236
+ * are the absolute path of the active-turn pointer (and the runner's Node
237
+ * binary), both constant for a given workspace. This is deliberate and
238
+ * load-bearing: the Cursor SDK loads `<workspace>/.cursor/hooks.json` (the hook
239
+ * script PATH) ONCE per runner process and caches it, ignoring later
240
+ * per-execution rewrites. A per-session script with per-session baked paths
241
+ * therefore gets cached at the FIRST execution and reused for every later one,
242
+ * recording their denials into the FIRST session's ledger while each later runner
243
+ * reads its own (empty) ledger and silently completes — the no-approval-button /
244
+ * "execution completed" regression. Keeping the script stable and resolving the
245
+ * CURRENT turn's artifacts from the pointer (which bash re-reads every
246
+ * invocation; see {@link ActiveTurnPointer}/writeActiveTurnPointer) makes a
247
+ * long-lived multi-session runner correct.
147
248
  *
148
- * The script reads a JSON state file written by the cursor-runner before
149
- * each agent.send() call. The state file is the single source of truth
150
- * for the dynamic approval inputs (autoApproveAll, mcpToolPolicies,
151
- * approvedGrantTokens). The static policy (which built-ins are gated and their
152
- * categories, and which arg fields are salient) is baked into the script at
153
- * generation time from approval-policy.ts.
249
+ * From the pointer the script reads the current turn's approval-state file (the
250
+ * single source of truth for the dynamic inputs: autoApproveAll, leasedCategories,
251
+ * mcpToolPolicies, approvedGrantTokens), denial ledger, and runner PID. The
252
+ * static policy (which built-ins are gated, their categories, the salient arg
253
+ * fields) is baked at generation time from approval-policy.ts.
154
254
  *
155
255
  * The identity token encoding (`base64(key \n salient)`) must stay byte-identical
156
256
  * to grantToken() in approval-state.ts.
157
257
  *
158
258
  * Scope guard (the crux of issue #173): the Cursor SDK loads project hooks from
159
- * `<workspace>/.cursor/hooks.json`, which is the SAME per-repo surface every
160
- * Cursor client reads. When a session runs against the user's real repo, the
161
- * user's own interactive Cursor IDE would otherwise load and run this hook too —
162
- * gating the IDE, polluting the denial ledger with the IDE's tool calls, and (in
163
- * multi-root windows) failing closed. We make the gate apply ONLY to the
164
- * runner's own agent by baking in the runner process PID and checking, on every
165
- * invocation, whether the runner is an ancestor of the hook process. The SDK
166
- * runs hooks in-process via child_process, so the runner's own agent (and its
167
- * delegated sub-agents) spawn the hook as a descendant of the runner; any other
168
- * Cursor client spawns it under a different process tree. A non-descendant
169
- * invocation is allowed immediately and never touches the ledger. This also
170
- * makes a leftover hooks.json self-neutralizing: once the runner exits, no
171
- * invocation can match its (now-dead) PID, so the gate is inert.
259
+ * `<workspace>/.cursor/hooks.json`, the SAME per-repo surface every Cursor client
260
+ * reads. When a session runs against the user's real repo, the user's own
261
+ * interactive Cursor IDE would otherwise load and run this hook too — gating the
262
+ * IDE, polluting the denial ledger, and (in multi-root windows) failing closed.
263
+ * We make the gate apply ONLY to the runner's own agent by checking, on every
264
+ * invocation, whether the runner PID (FROM THE POINTER) is an ancestor of the
265
+ * hook process. The SDK runs hooks in-process via child_process, so the runner's
266
+ * own agent (and its delegated sub-agents) spawn the hook as a descendant of the
267
+ * runner; any other Cursor client spawns it under a different process tree. A
268
+ * non-descendant invocation is allowed immediately and never touches the ledger.
269
+ * Combined with pointer teardown, a leftover hooks.json is self-neutralizing:
270
+ * once the turn ends (pointer removed) or the runner exits (PID dead), no
271
+ * invocation gates, so the gate is inert.
172
272
  */
173
- export function generateHookScript(
174
- stateFilePath: string,
175
- ledgerFilePath: string,
176
- runnerPid: number,
177
- ): string {
273
+ export function generateHookScript(activePointerPath: string, workspaceRoot = ""): string {
178
274
  const salientFields = SALIENT_ARG_FIELDS.join(" ");
179
275
  const categoryCaseArms = buildCategoryCaseArms();
180
276
  const nodeIdentityScript = buildNodeIdentityScript();
277
+ const observationStagingScript = buildObservationStagingScript();
278
+ const nodeBin = process.execPath;
181
279
  return `#!/bin/bash
182
280
  # Stigmer HITL approval hook for Cursor (preToolUse + beforeMCPExecution).
183
281
  # Generated by cursor-runner — do not edit manually.
@@ -193,9 +291,47 @@ set -euo pipefail
193
291
 
194
292
  INPUT=$(cat)
195
293
 
196
- STATE_FILE="${stateFilePath}"
197
- LEDGER_FILE="${ledgerFilePath}"
198
- RUNNER_PID="${runnerPid}"
294
+ NODE_BIN="${nodeBin}"
295
+ ACTIVE_FILE="${activePointerPath}"
296
+ # Baked workspace root for capture-mode's gitignore check (empty in unit tests
297
+ # that don't exercise capture mode; the check then falls back to the path's dir).
298
+ GIT_ROOT="${workspaceRoot}"
299
+
300
+ # --- Resolve the CURRENT turn from the runner-written pointer ----------------
301
+ # The Cursor SDK caches .cursor/hooks.json (the hook script PATH) for the runner
302
+ # process, so THIS script is stable across executions and the per-turn pointer
303
+ # (active.json) — which bash re-reads on every invocation — is the only thing
304
+ # that changes. It names the CURRENT turn's approval-state, denial ledger, and
305
+ # runner PID. This indirection is what makes a long-lived runner correct: a
306
+ # per-session script baked with per-session paths would be cached at the FIRST
307
+ # execution and reused for every later one, recording their denials to the FIRST
308
+ # session's ledger while each later runner reads its own empty ledger and
309
+ # silently completes (the no-approval-button / "completed" regression).
310
+ #
311
+ # A missing/garbled pointer means no active Stigmer turn (between turns, after
312
+ # teardown, or a dead runner) -> allow (inert), matching the
313
+ # leftover-hooks.json-is-inert invariant (issue #173).
314
+ if [ ! -f "$ACTIVE_FILE" ]; then
315
+ echo '{"permission":"allow"}'
316
+ exit 0
317
+ fi
318
+ PTR=$(ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e 'const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write((p.stateFile||"")+"\\n"+(p.ledgerFile||"")+"\\n"+((p.runnerPid==null)?"":String(p.runnerPid)))' "$ACTIVE_FILE" 2>/dev/null || true)
319
+ if [ -n "$PTR" ]; then
320
+ STATE_FILE=$(printf '%s\\n' "$PTR" | sed -n 1p)
321
+ LEDGER_FILE=$(printf '%s\\n' "$PTR" | sed -n 2p)
322
+ RUNNER_PID=$(printf '%s\\n' "$PTR" | sed -n 3p)
323
+ else
324
+ # Node unavailable: the pointer holds plain ~/.stigmer paths and an integer
325
+ # (no JSON-escaped quotes), so grep/cut is reliable here.
326
+ STATE_FILE=$(grep -o '"stateFile":"[^"]*"' "$ACTIVE_FILE" | head -1 | cut -d'"' -f4 || true)
327
+ LEDGER_FILE=$(grep -o '"ledgerFile":"[^"]*"' "$ACTIVE_FILE" | head -1 | cut -d'"' -f4 || true)
328
+ RUNNER_PID=$(grep -o '"runnerPid":[0-9]*' "$ACTIVE_FILE" | head -1 | cut -d: -f2 || true)
329
+ fi
330
+ if [ -z "$RUNNER_PID" ]; then
331
+ # Pointer unreadable -> no scope owner to gate for; stay inert.
332
+ echo '{"permission":"allow"}'
333
+ exit 0
334
+ fi
199
335
 
200
336
  # --- Scope guard: gate ONLY the runner's own agent (issue #173) -------------
201
337
  # The Cursor SDK runs hooks in-process, so the runner's own agent invocations
@@ -230,14 +366,31 @@ if ! __stigmer_is_own_agent; then
230
366
  exit 0
231
367
  fi
232
368
 
369
+ # --- Capture-mode helper: is a path gitignored? -----------------------------
370
+ # In capture mode the runner snapshots the working tree with git and reconciles
371
+ # it to the user's per-file decisions on resume, but a gitignored path (e.g.
372
+ # .env, build output) is invisible to that snapshot — so it can be neither
373
+ # captured for review nor reverted on reject. Such writes/deletes therefore stay
374
+ # on the deny-gate. Returns 0 (true) when the path is ignored. A non-git context
375
+ # or a missing path returns non-zero (treated as not-ignored -> allow).
376
+ __stigmer_is_gitignored() {
377
+ _p="$1"
378
+ [ -z "$_p" ] && return 1
379
+ if [ -n "$GIT_ROOT" ]; then
380
+ git -C "$GIT_ROOT" check-ignore -q -- "$_p" 2>/dev/null
381
+ else
382
+ git -C "$(dirname "$_p")" check-ignore -q -- "$_p" 2>/dev/null
383
+ fi
384
+ }
385
+
233
386
  # --- Canonical identity: tool_name / category / identity token / MCP token ---
234
387
  # Computed by the same Node.js binary that runs the cursor-runner (absolute path
235
388
  # baked at generation time) so JSON string values — file paths and especially
236
389
  # shell commands containing quotes, newlines, or unicode escapes — decode to the
237
390
  # exact bytes the runner sees in the stream event. ELECTRON_RUN_AS_NODE makes
238
391
  # the invocation safe when the runner is embedded in an Electron app (where
239
- # process.execPath is the Electron binary).
240
- NODE_BIN="${process.execPath}"
392
+ # process.execPath is the Electron binary). NODE_BIN is defined once near the top
393
+ # (it also parses the active-turn pointer).
241
394
  IDENTITY=$(printf '%s' "$INPUT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${nodeIdentityScript}' 2>/dev/null || true)
242
395
  if [ -n "$IDENTITY" ]; then
243
396
  TOOL_NAME=$(printf '%s\\n' "$IDENTITY" | sed -n 1p)
@@ -245,6 +398,16 @@ if [ -n "$IDENTITY" ]; then
245
398
  TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 3p)
246
399
  MCP_TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 4p)
247
400
  HOOK_EVENT=$(printf '%s\\n' "$IDENTITY" | sed -n 5p)
401
+ # base64(JSON(tool_input)) — the authoritative args the runner overlays onto
402
+ # the gated tool call. A single unwrapped base64 line (Node does not wrap), so
403
+ # sed reads it whole even for large file content.
404
+ INPUT_B64=$(printf '%s\\n' "$IDENTITY" | sed -n 6p)
405
+ # Content token (base64 of category\\nsalient\\ndigest), empty for a non-edit
406
+ # tool. The exact-identity grant the runner authorizes for a file edit.
407
+ CONTENT_TOKEN=$(printf '%s\\n' "$IDENTITY" | sed -n 7p)
408
+ # Raw salient (base64) — the file path / command. Capture mode decodes it to
409
+ # run git check-ignore on a file path.
410
+ SALIENT=$(printf '%s\\n' "$IDENTITY" | sed -n 8p | base64 -d 2>/dev/null || true)
248
411
  else
249
412
  # Fallback when the Node binary cannot run: grep/cut extraction. Best-effort
250
413
  # only — '"field":"[^"]*"' truncates at the first JSON-escaped quote, so the
@@ -266,6 +429,11 @@ ${categoryCaseArms}
266
429
  esac
267
430
  TOKEN=$(printf '%s\\n%s' "$CATEGORY" "$SALIENT" | base64 | tr -d '\\n')
268
431
  MCP_TOKEN=$(printf '%s\\n' "$TOOL_NAME" | base64 | tr -d '\\n')
432
+ # The grep fallback cannot reliably capture full multi-line tool_input, so the
433
+ # gated call degrades to today's stream-recovered args (no authoritative input)
434
+ # and cannot compute a content digest — the coarse token is the only identity.
435
+ INPUT_B64=""
436
+ CONTENT_TOKEN=""
269
437
  fi
270
438
 
271
439
  # --- Failsafe: missing state file → deny (fail-closed) ---
@@ -276,6 +444,66 @@ fi
276
444
 
277
445
  STATE=$(cat "$STATE_FILE")
278
446
 
447
+ # Capture mode (git workspaces): file mutations flow during the turn and are
448
+ # captured/gated per-file by the runner at the turn boundary (see
449
+ # shared/filereview/git-substrate.ts). Read once; consulted only in the
450
+ # gated-built-in arm below.
451
+ CAPTURE_MODE=false
452
+ if echo "$STATE" | grep -q '"captureMode":true'; then
453
+ CAPTURE_MODE=true
454
+ fi
455
+ # CAS capture of gitignored writes (the deep-agent parity switch). Set only when
456
+ # capture mode is on AND an artifact storage is configured (to persist blobs). It
457
+ # governs whether a non-secret gitignored write is staged+flowed for review vs.
458
+ # kept on the deny-gate. Read here; consulted only in the gitignored-capture arm
459
+ # below, which runs BEFORE auto-approve-all (capture is a turn property).
460
+ CAPTURE_IGNORED=false
461
+ if echo "$STATE" | grep -q '"captureIgnored":true'; then
462
+ CAPTURE_IGNORED=true
463
+ fi
464
+ # gitWorkspace selects the capture substrate (Slice 2c). Default true when the
465
+ # key is absent (older state files). When false the workspace is NOT a git tree:
466
+ # there is no git snapshot, so EVERY file write is CAS-staged below (not only
467
+ # gitignored ones), the git-tracked flow arm is skipped, and a delete stays gated
468
+ # (no CAS delete-capture path, parity with the deep-agent).
469
+ GIT_WORKSPACE=true
470
+ if echo "$STATE" | grep -q '"gitWorkspace":false'; then
471
+ GIT_WORKSPACE=false
472
+ fi
473
+
474
+ # --- Capture mode: observe CAS-owned writes for review ----------------------
475
+ # Runs BEFORE the auto-approve-all shortcut and the grant/lease checks because
476
+ # capture is a property of the TURN, not authorization: a non-secret CAS-owned
477
+ # write must be staged and reviewed even under the global bypass, and a
478
+ # secret-like one must be hard-blocked in every mode. WHICH writes are CAS-owned
479
+ # depends on the substrate: in a git tree it is only the GITIGNORED writes (git
480
+ # captures the tracked ones); in a NON-GIT workspace it is EVERY write (there is
481
+ # no git snapshot). Only a built-in write/edit (category "write") takes this arm;
482
+ # deletes and shell/MCP stay on the deny-gate below (parity with the deep-agent
483
+ # approval gate). The staging runs on the runner's own Node binary (the
484
+ # disk-backed mirror of CasCaptureFilesystemBackend.recordBefore): the salient
485
+ # path rides stdin (no argv escaping), the workspace root and the per-turn
486
+ # cas-observations dir ride argv. "captured" -> allow (apply-then-review);
487
+ # "secret" -> hard-block; "error"/Node-unavailable -> fail closed (deny, since a
488
+ # write we cannot capture cannot be reviewed).
489
+ if [ "$CAPTURE_IGNORED" = "true" ] && [ "$CATEGORY" = "write" ] && [ -n "$SALIENT" ] && { [ "$GIT_WORKSPACE" = "false" ] || __stigmer_is_gitignored "$SALIENT"; }; then
490
+ OBS_DIR="$(dirname "$STATE_FILE")/${CAS_OBSERVATIONS_DIRNAME}"
491
+ OBS_RESULT=$(printf '%s' "$SALIENT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${observationStagingScript}' "$GIT_ROOT" "$OBS_DIR" 2>/dev/null || echo error)
492
+ if [ "$OBS_RESULT" = "captured" ]; then
493
+ echo '{"permission":"allow"}'
494
+ exit 0
495
+ elif [ "$OBS_RESULT" = "secret" ]; then
496
+ echo '{"permission":"deny","agent_message":"${SECRET_BLOCKED_AGENT_MESSAGE}","user_message":"Blocked for security: this file matches a secret-like path and was not written or captured for review."}'
497
+ exit 0
498
+ else
499
+ # Node unavailable or a staging error: fail closed. A gitignored write we
500
+ # cannot stage cannot be captured for review, so keep gating it (today's
501
+ # behavior) rather than letting unreviewable bytes flow.
502
+ echo '{"permission":"deny","agent_message":"${APPROVAL_REQUIRED_AGENT_MESSAGE}","user_message":"Tool requires approval: '"$TOOL_NAME"'"}'
503
+ exit 0
504
+ fi
505
+ fi
506
+
279
507
  # --- 1. Auto-approve all ---
280
508
  if echo "$STATE" | grep -q '"autoApproveAll":true'; then
281
509
  echo '{"permission":"allow"}'
@@ -284,9 +512,13 @@ fi
284
512
 
285
513
  # Append a denial record to the ledger. Best-effort: a ledger write failure must
286
514
  # never abort the decision (the deny still goes out on stdout). toolName is raw
287
- # for human-readable debugging; token drives correlation in the runner.
515
+ # for human-readable debugging; token drives correlation in the runner; input is
516
+ # base64(JSON(tool_input)) — the authoritative pre-execution args the runner
517
+ # overlays for the approval preview (empty on the grep fallback path). Written
518
+ # with printf (a builtin, so no ARG_MAX limit) because the input can be a large
519
+ # multi-MB file body.
288
520
  record_denial() {
289
- echo '{"toolName":"'"$TOOL_NAME"'","token":"'"$1"'"}' >> "$LEDGER_FILE" 2>/dev/null || true
521
+ printf '{"toolName":"%s","token":"%s","input":"%s"}\\n' "$TOOL_NAME" "$1" "$INPUT_B64" >> "$LEDGER_FILE" 2>/dev/null || true
290
522
  }
291
523
 
292
524
  # --- 2. MCP tools (beforeMCPExecution event) ---
@@ -321,12 +553,46 @@ fi
321
553
 
322
554
  # --- 3. Gated built-in tools (preToolUse event, category non-empty) ---
323
555
  if [ -n "$CATEGORY" ]; then
324
- # Reinvocation grant: this exact resource was approved earlier allow.
325
- if echo "$STATE" | grep -qF "\\"$TOKEN\\""; then
556
+ # Capture mode (GIT tree only): a git-tracked file mutation (write/edit/delete)
557
+ # flows freely the runner captures the whole change set with git at the turn
558
+ # boundary and gates it per-file for review. A gitignored path is invisible to
559
+ # that git snapshot: a non-secret gitignored WRITE was already handled above
560
+ # (staged + allowed, or hard-blocked) when captureIgnored is on; here it only
561
+ # reaches the deny-gate when captureIgnored is off (no artifact storage) or it is
562
+ # a gitignored DELETE (no CAS capture path, parity with deep-agent). shell
563
+ # (category "shell") never takes this branch and stays gated as always. In a
564
+ # NON-GIT workspace this arm is skipped entirely (there is no git diff): writes
565
+ # were CAS-staged above and a delete falls through to the deny-gate.
566
+ if [ "$CAPTURE_MODE" = "true" ] && [ "$GIT_WORKSPACE" = "true" ] && { [ "$CATEGORY" = "write" ] || [ "$CATEGORY" = "delete" ]; }; then
567
+ if ! __stigmer_is_gitignored "$SALIENT"; then
568
+ echo '{"permission":"allow"}'
569
+ exit 0
570
+ fi
571
+ fi
572
+ # Reinvocation grant: allow when the CONTENT-exact token (a file edit approved
573
+ # earlier with this exact content) OR the COARSE token (a shell/delete, or the
574
+ # content-less degrade) is in approvedGrantTokens. A sibling edit to the same
575
+ # file has a different content token and no coarse grant, so it re-gates.
576
+ if { [ -n "$CONTENT_TOKEN" ] && echo "$STATE" | grep -qF "\\"$CONTENT_TOKEN\\""; } || echo "$STATE" | grep -qF "\\"$TOKEN\\""; then
326
577
  echo '{"permission":"allow"}'
327
578
  exit 0
328
579
  fi
329
- record_denial "$TOKEN"
580
+ # Run-lifetime category lease: the user chose "approve all <category>" earlier
581
+ # in this run (the scoped successor to autoApproveAll), so every built-in of
582
+ # this category is allowed for the rest of the run. Matched within the extracted
583
+ # leasedCategories array so a category word elsewhere in the state can't grant.
584
+ LEASED_CATEGORIES=$(echo "$STATE" | grep -o '"leasedCategories":\\[[^]]*\\]' | head -1 || true)
585
+ if [ -n "$LEASED_CATEGORIES" ] && echo "$LEASED_CATEGORIES" | grep -q "\\"$CATEGORY\\""; then
586
+ echo '{"permission":"allow"}'
587
+ exit 0
588
+ fi
589
+ # Record the PRIMARY token (content-exact when available, else coarse) so the
590
+ # runner's denial correlation keys on the SAME identity it grants on approval.
591
+ if [ -n "$CONTENT_TOKEN" ]; then
592
+ record_denial "$CONTENT_TOKEN"
593
+ else
594
+ record_denial "$TOKEN"
595
+ fi
330
596
  echo '{"permission":"deny","agent_message":"${APPROVAL_REQUIRED_AGENT_MESSAGE}","user_message":"Tool requires approval: '"$TOOL_NAME"'"}'
331
597
  exit 0
332
598
  fi