@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248

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 (355) hide show
  1. package/README.md +14 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/call-llm.js +9 -10
  4. package/dist/activities/call-llm.js.map +1 -1
  5. package/dist/activities/classify-tool-approvals.d.ts +2 -1
  6. package/dist/activities/classify-tool-approvals.js +28 -2
  7. package/dist/activities/classify-tool-approvals.js.map +1 -1
  8. package/dist/activities/discover-mcp-server.d.ts +39 -0
  9. package/dist/activities/discover-mcp-server.js +165 -28
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/emit-event.d.ts +14 -2
  12. package/dist/activities/emit-event.js +52 -17
  13. package/dist/activities/emit-event.js.map +1 -1
  14. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  15. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  17. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  18. package/dist/activities/execute-cursor/approval-state.js +7 -1
  19. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  20. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  21. package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
  22. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  24. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  25. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  26. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  27. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  28. package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
  29. package/dist/activities/execute-cursor/error-classifier.js +115 -6
  30. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  31. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  32. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  33. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  34. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  35. package/dist/activities/execute-cursor/hook-script.js +72 -10
  36. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  37. package/dist/activities/execute-cursor/index.d.ts +51 -11
  38. package/dist/activities/execute-cursor/index.js +177 -93
  39. package/dist/activities/execute-cursor/index.js.map +1 -1
  40. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  41. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  42. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  43. package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
  44. package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
  45. package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
  46. package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
  47. package/dist/activities/execute-cursor/prompt-builder.js +49 -7
  48. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  49. package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
  50. package/dist/activities/execute-cursor/turn-recovery.js +193 -0
  51. package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
  52. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  53. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  54. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  55. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  56. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  57. package/dist/activities/execute-deep-agent/environment.js +3 -1
  58. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  59. package/dist/activities/execute-deep-agent/index.js +15 -0
  60. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  62. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  63. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  64. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  65. package/dist/activities/execute-deep-agent/setup.js +65 -24
  66. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
  68. package/dist/activities/execute-deep-agent/shell-env.js +7 -9
  69. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  70. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  71. package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
  72. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  73. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
  74. package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
  75. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  76. package/dist/activities/hydrate-workflow-execution.js +8 -3
  77. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  78. package/dist/activities/run-command.d.ts +5 -2
  79. package/dist/activities/run-command.js +22 -10
  80. package/dist/activities/run-command.js.map +1 -1
  81. package/dist/activities/run-env.d.ts +40 -0
  82. package/dist/activities/run-env.js +66 -0
  83. package/dist/activities/run-env.js.map +1 -0
  84. package/dist/activities/workflow-event-activities.d.ts +28 -10
  85. package/dist/activities/workflow-event-activities.js +87 -58
  86. package/dist/activities/workflow-event-activities.js.map +1 -1
  87. package/dist/bootstrap.js +5 -0
  88. package/dist/bootstrap.js.map +1 -1
  89. package/dist/claimcheck/payload-codec.js +21 -1
  90. package/dist/claimcheck/payload-codec.js.map +1 -1
  91. package/dist/client/stigmer-client.d.ts +83 -21
  92. package/dist/client/stigmer-client.js +106 -29
  93. package/dist/client/stigmer-client.js.map +1 -1
  94. package/dist/encryption/config.d.ts +64 -0
  95. package/dist/encryption/config.js +108 -0
  96. package/dist/encryption/config.js.map +1 -0
  97. package/dist/encryption/index.d.ts +3 -0
  98. package/dist/encryption/index.js +3 -0
  99. package/dist/encryption/index.js.map +1 -0
  100. package/dist/encryption/payload-codec.d.ts +41 -0
  101. package/dist/encryption/payload-codec.js +130 -0
  102. package/dist/encryption/payload-codec.js.map +1 -0
  103. package/dist/middleware/index.d.ts +3 -0
  104. package/dist/middleware/index.js +7 -0
  105. package/dist/middleware/index.js.map +1 -1
  106. package/dist/middleware/path-normalization.d.ts +57 -0
  107. package/dist/middleware/path-normalization.js +109 -0
  108. package/dist/middleware/path-normalization.js.map +1 -0
  109. package/dist/middleware/types.d.ts +8 -0
  110. package/dist/payload-codecs.d.ts +17 -0
  111. package/dist/payload-codecs.js +42 -0
  112. package/dist/payload-codecs.js.map +1 -0
  113. package/dist/preflight.d.ts +31 -0
  114. package/dist/preflight.js +43 -0
  115. package/dist/preflight.js.map +1 -1
  116. package/dist/runner-manager.js +21 -17
  117. package/dist/runner-manager.js.map +1 -1
  118. package/dist/runner.js +11 -18
  119. package/dist/runner.js.map +1 -1
  120. package/dist/shared/approval-policy.d.ts +9 -3
  121. package/dist/shared/approval-policy.js +15 -6
  122. package/dist/shared/approval-policy.js.map +1 -1
  123. package/dist/shared/artifact-storage.d.ts +23 -1
  124. package/dist/shared/artifact-storage.js +51 -10
  125. package/dist/shared/artifact-storage.js.map +1 -1
  126. package/dist/shared/attachment-naming.d.ts +53 -0
  127. package/dist/shared/attachment-naming.js +59 -0
  128. package/dist/shared/attachment-naming.js.map +1 -0
  129. package/dist/shared/caller-identity.d.ts +23 -2
  130. package/dist/shared/caller-identity.js +36 -5
  131. package/dist/shared/caller-identity.js.map +1 -1
  132. package/dist/shared/channel-attachment.js +1 -0
  133. package/dist/shared/channel-attachment.js.map +1 -1
  134. package/dist/shared/checkpointer/http-saver.d.ts +26 -1
  135. package/dist/shared/checkpointer/http-saver.js +54 -8
  136. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  137. package/dist/shared/conversation-attachment.js +1 -0
  138. package/dist/shared/conversation-attachment.js.map +1 -1
  139. package/dist/shared/conversation-catchup.d.ts +9 -2
  140. package/dist/shared/conversation-catchup.js +39 -6
  141. package/dist/shared/conversation-catchup.js.map +1 -1
  142. package/dist/shared/datastore-attachment.d.ts +50 -7
  143. package/dist/shared/datastore-attachment.js +93 -11
  144. package/dist/shared/datastore-attachment.js.map +1 -1
  145. package/dist/shared/grpc-retry.d.ts +5 -1
  146. package/dist/shared/grpc-retry.js +5 -1
  147. package/dist/shared/grpc-retry.js.map +1 -1
  148. package/dist/shared/http-retry.d.ts +85 -0
  149. package/dist/shared/http-retry.js +101 -0
  150. package/dist/shared/http-retry.js.map +1 -0
  151. package/dist/shared/llm-backend.d.ts +275 -0
  152. package/dist/shared/llm-backend.js +425 -0
  153. package/dist/shared/llm-backend.js.map +1 -0
  154. package/dist/shared/llm-proxy.d.ts +8 -0
  155. package/dist/shared/llm-proxy.js +15 -0
  156. package/dist/shared/llm-proxy.js.map +1 -1
  157. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  158. package/dist/shared/mcp-enabled-tools.js +86 -0
  159. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  160. package/dist/shared/mcp-manager.d.ts +11 -5
  161. package/dist/shared/mcp-manager.js +47 -8
  162. package/dist/shared/mcp-manager.js.map +1 -1
  163. package/dist/shared/mcp-resolver.d.ts +39 -2
  164. package/dist/shared/mcp-resolver.js +38 -2
  165. package/dist/shared/mcp-resolver.js.map +1 -1
  166. package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
  167. package/dist/shared/mcp-schema-sanitizer.js +197 -0
  168. package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
  169. package/dist/shared/model-client.d.ts +18 -5
  170. package/dist/shared/model-client.js +149 -18
  171. package/dist/shared/model-client.js.map +1 -1
  172. package/dist/shared/model-error.js +198 -5
  173. package/dist/shared/model-error.js.map +1 -1
  174. package/dist/shared/model-pricing-data.d.ts +2 -0
  175. package/dist/shared/model-pricing-data.js +13 -3
  176. package/dist/shared/model-pricing-data.js.map +1 -1
  177. package/dist/shared/model-registry.js +3 -2
  178. package/dist/shared/model-registry.js.map +1 -1
  179. package/dist/shared/plan-mode-permissions.d.ts +33 -0
  180. package/dist/shared/plan-mode-permissions.js +35 -0
  181. package/dist/shared/plan-mode-permissions.js.map +1 -0
  182. package/dist/shared/registry-endpoint.d.ts +20 -0
  183. package/dist/shared/registry-endpoint.js +25 -0
  184. package/dist/shared/registry-endpoint.js.map +1 -1
  185. package/dist/shared/runner-credential-keys.d.ts +24 -0
  186. package/dist/shared/runner-credential-keys.js +47 -0
  187. package/dist/shared/runner-credential-keys.js.map +1 -0
  188. package/dist/worker.d.ts +2 -1
  189. package/dist/worker.js +2 -4
  190. package/dist/worker.js.map +1 -1
  191. package/dist/workflow-engine/resolve.d.ts +19 -1
  192. package/dist/workflow-engine/resolve.js +37 -2
  193. package/dist/workflow-engine/resolve.js.map +1 -1
  194. package/dist/workflow-engine/types.d.ts +18 -0
  195. package/dist/workflow-engine/types.js.map +1 -1
  196. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  197. package/dist/workflows/call-agent-orchestrator.js +1 -0
  198. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  199. package/dist/workflows/connect-mcp-server.js +9 -0
  200. package/dist/workflows/connect-mcp-server.js.map +1 -1
  201. package/dist/workflows/engine-core.js +23 -2
  202. package/dist/workflows/engine-core.js.map +1 -1
  203. package/dist/workflows/execute-from-execution.d.ts +1 -1
  204. package/dist/workflows/execute-from-execution.js +11 -1
  205. package/dist/workflows/execute-from-execution.js.map +1 -1
  206. package/dist/workflows/types.d.ts +8 -0
  207. package/package.json +9 -4
  208. package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
  209. package/src/__test-utils__/vitest-global-setup.ts +50 -0
  210. package/src/__tests__/bootstrap.test.ts +43 -0
  211. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  212. package/src/__tests__/encryption-codec.test.ts +287 -0
  213. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  214. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  215. package/src/__tests__/preflight.test.ts +65 -2
  216. package/src/activities/__tests__/call-llm.test.ts +75 -0
  217. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  218. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  219. package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
  220. package/src/activities/__tests__/error-classifier.test.ts +94 -2
  221. package/src/activities/__tests__/run-command.test.ts +221 -0
  222. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  223. package/src/activities/call-llm.ts +9 -16
  224. package/src/activities/classify-tool-approvals.ts +34 -4
  225. package/src/activities/discover-mcp-server.ts +203 -31
  226. package/src/activities/emit-event.ts +65 -21
  227. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  228. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  229. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
  230. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
  231. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  232. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  233. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  234. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  235. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
  236. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  237. package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
  238. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  239. package/src/activities/execute-cursor/approval-state.ts +30 -1
  240. package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
  241. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  242. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  243. package/src/activities/execute-cursor/error-classifier.ts +131 -7
  244. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  245. package/src/activities/execute-cursor/hook-script.ts +74 -10
  246. package/src/activities/execute-cursor/index.ts +209 -109
  247. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  248. package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
  249. package/src/activities/execute-cursor/prompt-builder.ts +85 -9
  250. package/src/activities/execute-cursor/turn-recovery.ts +208 -0
  251. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  252. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
  253. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  254. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  255. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  256. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  257. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
  258. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  259. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  260. package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
  261. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
  262. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  263. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  264. package/src/activities/execute-deep-agent/environment.ts +3 -1
  265. package/src/activities/execute-deep-agent/index.ts +20 -0
  266. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  267. package/src/activities/execute-deep-agent/setup.ts +85 -29
  268. package/src/activities/execute-deep-agent/shell-env.ts +8 -9
  269. package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
  270. package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
  271. package/src/activities/hydrate-workflow-execution.ts +8 -3
  272. package/src/activities/run-command.ts +25 -12
  273. package/src/activities/run-env.ts +79 -0
  274. package/src/activities/workflow-event-activities.ts +96 -69
  275. package/src/bootstrap.ts +5 -0
  276. package/src/claimcheck/payload-codec.ts +33 -1
  277. package/src/client/__tests__/stigmer-client.test.ts +110 -11
  278. package/src/client/stigmer-client.ts +150 -37
  279. package/src/encryption/config.ts +155 -0
  280. package/src/encryption/index.ts +3 -0
  281. package/src/encryption/payload-codec.ts +152 -0
  282. package/src/middleware/__tests__/path-normalization.test.ts +140 -0
  283. package/src/middleware/index.ts +8 -0
  284. package/src/middleware/path-normalization.ts +125 -0
  285. package/src/middleware/types.ts +11 -0
  286. package/src/payload-codecs.ts +62 -0
  287. package/src/preflight.ts +45 -0
  288. package/src/runner-manager.ts +26 -26
  289. package/src/runner.ts +12 -27
  290. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  291. package/src/shared/__tests__/artifact-storage.test.ts +145 -2
  292. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  293. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  294. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  295. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  296. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  297. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  298. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  299. package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
  300. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  301. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  302. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  303. package/src/shared/__tests__/http-retry.test.ts +68 -0
  304. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  305. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  306. package/src/shared/__tests__/mcp-manager.test.ts +137 -15
  307. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  308. package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
  309. package/src/shared/__tests__/model-client.test.ts +200 -0
  310. package/src/shared/__tests__/model-error.test.ts +289 -1
  311. package/src/shared/__tests__/model-pricing.test.ts +62 -2
  312. package/src/shared/__tests__/model-registry.test.ts +22 -0
  313. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  314. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  315. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  316. package/src/shared/approval-policy.ts +14 -7
  317. package/src/shared/artifact-storage.ts +75 -10
  318. package/src/shared/attachment-naming.ts +78 -0
  319. package/src/shared/caller-identity.ts +40 -5
  320. package/src/shared/channel-attachment.ts +1 -0
  321. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  322. package/src/shared/checkpointer/http-saver.ts +71 -8
  323. package/src/shared/conversation-attachment.ts +1 -0
  324. package/src/shared/conversation-catchup.ts +39 -6
  325. package/src/shared/datastore-attachment.ts +106 -11
  326. package/src/shared/grpc-retry.ts +5 -1
  327. package/src/shared/http-retry.ts +139 -0
  328. package/src/shared/llm-backend.ts +544 -0
  329. package/src/shared/llm-proxy.ts +15 -0
  330. package/src/shared/mcp-enabled-tools.ts +105 -0
  331. package/src/shared/mcp-manager.ts +56 -8
  332. package/src/shared/mcp-resolver.ts +73 -2
  333. package/src/shared/mcp-schema-sanitizer.ts +224 -0
  334. package/src/shared/model-client.ts +179 -19
  335. package/src/shared/model-error.ts +222 -4
  336. package/src/shared/model-pricing-data.ts +22 -3
  337. package/src/shared/model-registry.ts +7 -2
  338. package/src/shared/plan-mode-permissions.ts +37 -0
  339. package/src/shared/registry-endpoint.ts +27 -0
  340. package/src/shared/runner-credential-keys.ts +46 -0
  341. package/src/worker.ts +4 -5
  342. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
  343. package/src/workflow-engine/resolve.ts +48 -2
  344. package/src/workflow-engine/types.ts +18 -0
  345. package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
  346. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  347. package/src/workflows/call-agent-orchestrator.ts +10 -0
  348. package/src/workflows/connect-mcp-server.ts +9 -0
  349. package/src/workflows/engine-core.ts +23 -2
  350. package/src/workflows/execute-from-execution.ts +12 -2
  351. package/src/workflows/types.ts +8 -0
  352. package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
  353. package/dist/activities/execute-cursor/mcp-config.js +0 -39
  354. package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
  355. package/src/activities/execute-cursor/mcp-config.ts +0 -66
@@ -22,7 +22,6 @@ import {
22
22
  type InjectedSinks,
23
23
  type WorkflowBundleOption,
24
24
  } from "@temporalio/worker";
25
- import type { PayloadCodec } from "@temporalio/common";
26
25
  import type { Config } from "./config.js";
27
26
  import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
28
27
  // Boot marks mirror the static runner's segment names where the work is the
@@ -32,6 +31,7 @@ import { markBoot } from "./shared/cold-start-timing.js";
32
31
  import type { WorkerActivities } from "./worker.js";
33
32
  import { resolveWorkflowSource, OTEL_WORKFLOW_INTERCEPTOR_MODULE } from "./workflow-source.js";
34
33
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
34
+ import { assertLlmBackendsPreflight } from "./preflight.js";
35
35
  import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
36
36
  // Per-task-queue in-flight activity tracking lives in ./in-flight.ts so the
37
37
  // activity interceptor (no manager-closure handle) and unit tests can reach it.
@@ -237,6 +237,8 @@ export async function createStigmerRunnerManager(
237
237
  const runnerTokenRef = { current: options.stigmerToken ?? null };
238
238
  const baseConfig = mapManagerOptionsToConfig(options, tokenRef, runnerTokenRef);
239
239
 
240
+ assertLlmBackendsPreflight(baseConfig.proxyEndpoint);
241
+
240
242
  // Install the Cursor SDK interceptors BEFORE resolving Temporal coordinates.
241
243
  // Coordinate discovery dials the control plane via StigmerClient, which loads
242
244
  // @connectrpc/connect-node and snapshots the node:http2 ESM facade on first
@@ -291,8 +293,10 @@ export async function createStigmerRunnerManager(
291
293
  // Resolve the runner bootstrap after the http2 patch is in place (discovery
292
294
  // dials the control plane through connect-node). An explicit address wins;
293
295
  // otherwise a token triggers control-plane discovery; otherwise localhost.
294
- // Must happen before createAllActivities so runtime activities that dial
295
- // Temporal (e.g. emit-event) see the resolved address too.
296
+ // Must happen before createAllActivities so the activities' Config carries
297
+ // the resolved coordinates. Only the worker connection dials Temporal — no
298
+ // activity does (emit-event, the last one, now routes signals through the
299
+ // server's SendSignal lane; see oss#517).
296
300
  const bootstrap = await resolveRunnerBootstrap({
297
301
  explicitAddress: options.temporalAddress,
298
302
  explicitNamespace: options.temporalNamespace,
@@ -351,7 +355,24 @@ export async function createStigmerRunnerManager(
351
355
  `[runner-manager] Artifact store: could not resolve config for boot log: ${err}`,
352
356
  );
353
357
  }
354
- const payloadCodec = await createPayloadCodec(config);
358
+ const { createPayloadCodecs } = await import("./payload-codecs.js");
359
+ // Server-managed payload-encryption keys (cloud desktop runners) ride the
360
+ // bootstrap response; an env-configured key still wins inside the loader.
361
+ // A cloud server that mints runner tokens but returns no key predates key
362
+ // management — payloads then rest plaintext in cloud Temporal history, which
363
+ // deserves a diagnosable warning rather than silence.
364
+ const payloadCodecs = await createPayloadCodecs(config, bootstrap.payloadEncryption);
365
+ if (
366
+ !bootstrap.payloadEncryption &&
367
+ bootstrap.runnerAccessToken &&
368
+ !process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY
369
+ ) {
370
+ console.warn(
371
+ "[runner-manager] Server minted a runner token but returned no payload " +
372
+ "encryption key; Temporal payloads will be plaintext (server predates " +
373
+ "runner key management?)",
374
+ );
375
+ }
355
376
 
356
377
  const connection = await NativeConnection.connect({
357
378
  address: config.temporalAddress,
@@ -396,9 +417,7 @@ export async function createStigmerRunnerManager(
396
417
  activities,
397
418
  workflowBundle,
398
419
  maxConcurrentActivityTaskExecutions: config.maxConcurrentActivities,
399
- dataConverter: payloadCodec
400
- ? { payloadCodecs: [payloadCodec] }
401
- : undefined,
420
+ dataConverter: payloadCodecs?.length ? { payloadCodecs } : undefined,
402
421
  sinks: interceptorConfig.sinks,
403
422
  interceptors: interceptorConfig.interceptors,
404
423
  });
@@ -739,25 +758,6 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
739
758
  };
740
759
  }
741
760
 
742
- async function createPayloadCodec(
743
- config: Config,
744
- ): Promise<PayloadCodec | undefined> {
745
- const { loadClaimcheckConfig, ClaimcheckPayloadCodec } = await import(
746
- "./claimcheck/index.js"
747
- );
748
- const claimcheckConfig = loadClaimcheckConfig();
749
- if (!claimcheckConfig.enabled) {
750
- return undefined;
751
- }
752
-
753
- const { loadArtifactStorageConfig, createArtifactStorage } = await import(
754
- "./shared/artifact-storage.js"
755
- );
756
- const storageConfig = loadArtifactStorageConfig(config);
757
- const storage = createArtifactStorage(storageConfig);
758
- return new ClaimcheckPayloadCodec(storage, claimcheckConfig);
759
- }
760
-
761
761
  interface InterceptorConfig {
762
762
  sinks: InjectedSinks<any>;
763
763
  interceptors: Record<string, any>;
package/src/runner.ts CHANGED
@@ -15,11 +15,11 @@
15
15
  import { mkdirSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { homedir, tmpdir } from "node:os";
18
- import type { PayloadCodec } from "@temporalio/common";
19
18
  import type { Config } from "./config.js";
20
19
  import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
21
20
  import type { WorkerActivities } from "./worker.js";
22
21
  import { resolveRunnerBootstrap } from "./bootstrap.js";
22
+ import { assertLlmBackendsPreflight } from "./preflight.js";
23
23
  import { markBoot, emitRunnerBootTiming } from "./shared/cold-start-timing.js";
24
24
 
25
25
  /**
@@ -197,6 +197,8 @@ export async function createStigmerRunner(
197
197
 
198
198
  const baseConfig = mapOptionsToConfig(options);
199
199
 
200
+ assertLlmBackendsPreflight(baseConfig.proxyEndpoint);
201
+
200
202
  // Install the Cursor SDK interceptors BEFORE resolving Temporal coordinates.
201
203
  // Coordinate discovery dials the control plane via StigmerClient, which loads
202
204
  // @connectrpc/connect-node and snapshots the node:http2 ESM facade on first
@@ -235,8 +237,9 @@ export async function createStigmerRunner(
235
237
  // Resolve Temporal coordinates after the http2 patch is in place (discovery
236
238
  // dials the control plane through connect-node). Explicit address wins;
237
239
  // otherwise a token triggers control-plane discovery; otherwise localhost.
238
- // Activities that dial Temporal at runtime (e.g. emit-event) read
239
- // config.temporalAddress, so this must precede their creation.
240
+ // Only the worker connection consumes these coordinates no activity dials
241
+ // Temporal directly (emit-event, the last one, now routes signals through
242
+ // the server's SendSignal lane; see oss#517).
240
243
  //
241
244
  // The static runner brings its own already-proxy-valid token (harness/CLI),
242
245
  // so it does not consume the minted runner token from the bootstrap response;
@@ -302,10 +305,14 @@ export async function createStigmerRunner(
302
305
  );
303
306
  }
304
307
 
305
- const payloadCodec = await createPayloadCodec(config);
308
+ const { createPayloadCodecs } = await import("./payload-codecs.js");
309
+ // Server-managed keys from the bootstrap response apply here too (a CLI
310
+ // daemon with a cloud token is desktop-class); cloud sandboxes are
311
+ // unaffected — their env-injected platform key wins inside the loader.
312
+ const payloadCodecs = await createPayloadCodecs(config, coordinates.payloadEncryption);
306
313
 
307
314
  const { startWorker } = await import("./worker.js");
308
- const worker = await startWorker({ config, activities, payloadCodec });
315
+ const worker = await startWorker({ config, activities, payloadCodecs });
309
316
  markBoot("worker_created");
310
317
 
311
318
  return {
@@ -455,28 +462,6 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
455
462
  };
456
463
  }
457
464
 
458
- async function createPayloadCodec(config: Config): Promise<PayloadCodec | undefined> {
459
- const { loadClaimcheckConfig, ClaimcheckPayloadCodec } = await import("./claimcheck/index.js");
460
- const claimcheckConfig = loadClaimcheckConfig();
461
- if (!claimcheckConfig.enabled) {
462
- return undefined;
463
- }
464
-
465
- const { loadArtifactStorageConfig, createArtifactStorage } = await import(
466
- "./shared/artifact-storage.js"
467
- );
468
- const storageConfig = loadArtifactStorageConfig(config);
469
- const storage = createArtifactStorage(storageConfig);
470
-
471
- console.log(
472
- `[runner] Claimcheck enabled (threshold=${claimcheckConfig.thresholdBytes}B, ` +
473
- `compression=${claimcheckConfig.compressionEnabled}, ` +
474
- `storage=${storageConfig.type})`,
475
- );
476
-
477
- return new ClaimcheckPayloadCodec(storage, claimcheckConfig);
478
- }
479
-
480
465
  function resolveDefaultWorkspaceDir(): string {
481
466
  try {
482
467
  const dir = join(homedir(), ".stigmer", "workspaces", "runner");
@@ -19,7 +19,6 @@ import {
19
19
  } from "../approval-policy.js";
20
20
  import type { ToolApprovalCategory } from "../tool-kind.js";
21
21
  import type { ResolvedMcpServer } from "../mcp-resolver.js";
22
- import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
23
22
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
24
23
  import { ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
25
24
 
@@ -83,6 +82,9 @@ function makeServer(
83
82
  slug: string,
84
83
  toolApprovals: Array<{ toolName: string; message?: string }>,
85
84
  pinnedToolApprovals: Array<{ toolName: string; message?: string }> = [],
85
+ // Layer-3 overrides ride the server they were declared on (issue #349) —
86
+ // there is no cross-server override input to the merge anymore.
87
+ toolApprovalOverrides: Array<{ toolName: string; requiresApproval: boolean; message?: string }> = [],
86
88
  ): ResolvedMcpServer {
87
89
  return {
88
90
  slug,
@@ -95,6 +97,11 @@ function makeServer(
95
97
  toolName: a.toolName,
96
98
  message: a.message ?? "",
97
99
  })) as any[],
100
+ toolApprovalOverrides: toolApprovalOverrides.map(o => ({
101
+ toolName: o.toolName,
102
+ requiresApproval: o.requiresApproval,
103
+ message: o.message ?? "",
104
+ })) as any[],
98
105
  discoveredCapabilitiesEmpty: false,
99
106
  };
100
107
  }
@@ -102,7 +109,7 @@ function makeServer(
102
109
  describe("mergeApprovalPolicies", () => {
103
110
  it("returns empty map under a global lease (spec.auto_approve_all)", () => {
104
111
  const servers = [makeServer("github", [{ toolName: "push" }])];
105
- const result = mergeApprovalPolicies(servers, [], leases({ global: true }));
112
+ const result = mergeApprovalPolicies(servers, leases({ global: true }));
106
113
  expect(result.size).toBe(0);
107
114
  });
108
115
 
@@ -111,7 +118,7 @@ describe("mergeApprovalPolicies", () => {
111
118
  makeServer("github", [{ toolName: "push" }]),
112
119
  makeServer("database", [{ toolName: "drop_table" }]),
113
120
  ];
114
- const result = mergeApprovalPolicies(servers, [], leases({ servers: ["github"] }));
121
+ const result = mergeApprovalPolicies(servers, leases({ servers: ["github"] }));
115
122
  expect(result.has("github/push")).toBe(false);
116
123
  expect(result.has("database/drop_table")).toBe(true);
117
124
  });
@@ -122,7 +129,7 @@ describe("mergeApprovalPolicies", () => {
122
129
  { toolName: "create_issue", message: "Create issue?" },
123
130
  ]),
124
131
  ];
125
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
132
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
126
133
 
127
134
  expect(result.size).toBe(1);
128
135
  const policy = result.get("github/create_issue")!;
@@ -139,7 +146,7 @@ describe("mergeApprovalPolicies", () => {
139
146
  [{ toolName: "push", message: "pinned message" }],
140
147
  ),
141
148
  ];
142
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
149
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
143
150
 
144
151
  const policy = result.get("github/push")!;
145
152
  expect(policy.approvalMessage).toBe("pinned message");
@@ -147,34 +154,74 @@ describe("mergeApprovalPolicies", () => {
147
154
 
148
155
  it("agent overrides can disable approval", () => {
149
156
  const servers = [
150
- makeServer("github", [{ toolName: "push" }]),
157
+ makeServer("github", [{ toolName: "push" }], [], [
158
+ { toolName: "push", requiresApproval: false },
159
+ ]),
151
160
  ];
152
- const overrides: ToolApprovalOverride[] = [{
153
- toolName: "push",
154
- requiresApproval: false,
155
- message: "",
156
- }] as any[];
157
161
 
158
- const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
162
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
159
163
  expect(result.size).toBe(0);
160
164
  });
161
165
 
162
166
  it("agent overrides can add new approval requirement", () => {
163
- const servers = [makeServer("github", [])];
164
- const overrides: ToolApprovalOverride[] = [{
165
- toolName: "delete_repo",
166
- requiresApproval: true,
167
- message: "Really delete?",
168
- }] as any[];
169
-
170
- const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
167
+ const servers = [
168
+ makeServer("github", [], [], [
169
+ { toolName: "delete_repo", requiresApproval: true, message: "Really delete?" },
170
+ ]),
171
+ ];
172
+
173
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
171
174
  expect(result.has("github/delete_repo")).toBe(true);
172
175
  });
173
176
 
177
+ describe("override scoping (issue #349 — no cross-server leak)", () => {
178
+ it("a disable override on one server leaves a same-named classifier-gated tool on another server gated", () => {
179
+ // The dangerous direction: before #349 this override silently
180
+ // un-gated database/delete_item too (authorization widening).
181
+ const servers = [
182
+ makeServer("github", [{ toolName: "delete_item" }], [], [
183
+ { toolName: "delete_item", requiresApproval: false },
184
+ ]),
185
+ makeServer("database", [{ toolName: "delete_item" }]),
186
+ ];
187
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
188
+ expect(result.has("github/delete_item")).toBe(false);
189
+ const other = result.get("database/delete_item")!;
190
+ expect(other.requiresApproval).toBe(true);
191
+ expect(other.source).toBe("classifier_default");
192
+ });
193
+
194
+ it("an enable override on one server adds no gate for a same-named tool on another server", () => {
195
+ const servers = [
196
+ makeServer("github", [], [], [
197
+ { toolName: "send_message", requiresApproval: true },
198
+ ]),
199
+ makeServer("slack", []),
200
+ ];
201
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
202
+ expect(result.has("github/send_message")).toBe(true);
203
+ expect(result.has("slack/send_message")).toBe(false);
204
+ });
205
+
206
+ it("a message override on one server does not re-message a same-named gate on another server", () => {
207
+ const servers = [
208
+ makeServer("github", [{ toolName: "push", message: "github classifier" }], [], [
209
+ { toolName: "push", requiresApproval: true, message: "github override" },
210
+ ]),
211
+ makeServer("gitlab", [{ toolName: "push", message: "gitlab classifier" }]),
212
+ ];
213
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
214
+ expect(result.get("github/push")!.approvalMessage).toBe("github override");
215
+ const other = result.get("gitlab/push")!;
216
+ expect(other.approvalMessage).toBe("gitlab classifier");
217
+ expect(other.source).toBe("classifier_default");
218
+ });
219
+ });
220
+
174
221
  describe("provenance (source stamping)", () => {
175
222
  it("stamps classifier_default for a layer-1 tool approval", () => {
176
223
  const servers = [makeServer("github", [{ toolName: "create_issue" }])];
177
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
224
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
178
225
  expect(result.get("github/create_issue")!.source).toBe("classifier_default");
179
226
  });
180
227
 
@@ -182,31 +229,27 @@ describe("mergeApprovalPolicies", () => {
182
229
  const servers = [
183
230
  makeServer("github", [{ toolName: "push" }], [{ toolName: "push" }]),
184
231
  ];
185
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
232
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
186
233
  expect(result.get("github/push")!.source).toBe("pinned_override");
187
234
  });
188
235
 
189
236
  it("stamps agent_override when a per-agent override adds a gate", () => {
190
- const servers = [makeServer("github", [])];
191
- const overrides: ToolApprovalOverride[] = [{
192
- toolName: "delete_repo",
193
- requiresApproval: true,
194
- message: "Really delete?",
195
- }] as any[];
196
- const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
237
+ const servers = [
238
+ makeServer("github", [], [], [
239
+ { toolName: "delete_repo", requiresApproval: true, message: "Really delete?" },
240
+ ]),
241
+ ];
242
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
197
243
  expect(result.get("github/delete_repo")!.source).toBe("agent_override");
198
244
  });
199
245
 
200
246
  it("stamps agent_override when it overrides a classifier/pinned entry", () => {
201
247
  const servers = [
202
- makeServer("github", [{ toolName: "push", message: "classifier" }]),
248
+ makeServer("github", [{ toolName: "push", message: "classifier" }], [], [
249
+ { toolName: "push", requiresApproval: true, message: "agent says gate" },
250
+ ]),
203
251
  ];
204
- const overrides: ToolApprovalOverride[] = [{
205
- toolName: "push",
206
- requiresApproval: true,
207
- message: "agent says gate",
208
- }] as any[];
209
- const result = mergeApprovalPolicies(servers, overrides, NO_LEASES);
252
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
210
253
  const policy = result.get("github/push")!;
211
254
  expect(policy.source).toBe("agent_override");
212
255
  expect(policy.approvalMessage).toBe("agent says gate");
@@ -217,7 +260,7 @@ describe("mergeApprovalPolicies", () => {
217
260
  const servers = [
218
261
  makeServer("github", [{ toolName: "" }]),
219
262
  ];
220
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
263
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
221
264
  expect(result.size).toBe(0);
222
265
  });
223
266
 
@@ -225,7 +268,7 @@ describe("mergeApprovalPolicies", () => {
225
268
  const servers = [
226
269
  makeServer("github", [{ toolName: "push" }]),
227
270
  ];
228
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
271
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
229
272
  expect(result.get("github/push")!.approvalMessage).toContain("Execute tool: push");
230
273
  });
231
274
 
@@ -234,7 +277,7 @@ describe("mergeApprovalPolicies", () => {
234
277
  makeServer("github", [{ toolName: "push" }]),
235
278
  makeServer("database", [{ toolName: "drop_table" }]),
236
279
  ];
237
- const result = mergeApprovalPolicies(servers, [], NO_LEASES);
280
+ const result = mergeApprovalPolicies(servers, NO_LEASES);
238
281
  expect(result.size).toBe(2);
239
282
  expect(result.has("github/push")).toBe(true);
240
283
  expect(result.has("database/drop_table")).toBe(true);
@@ -294,7 +294,13 @@ describe("ProxyArtifactStorage", () => {
294
294
  return new Response("server error", { status: 500 });
295
295
  }) as typeof fetch;
296
296
 
297
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
297
+ // 500 is retryable, so this persistent failure exhausts the budget —
298
+ // delayFn injected to skip the real ~7.75 s of backoff (#468). The
299
+ // degrade contract under test (budget exhaustion still throws the
300
+ // caller-visible error) is unchanged.
301
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
302
+ delayFn: async () => {},
303
+ });
298
304
  await expect(
299
305
  storage.upload("key", Buffer.from("x")),
300
306
  ).rejects.toThrow("Presigned upload failed (HTTP 500)");
@@ -411,11 +417,148 @@ describe("ProxyArtifactStorage", () => {
411
417
 
412
418
  it("exists throws on an unexpected object status (a fault, not an existence answer)", async () => {
413
419
  mockProxyFetch(() => new Response("boom", { status: 500 }));
414
- const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
420
+ // Persistent 500 exhausts the retry budget; delayFn skips the real
421
+ // backoff (#468). The fault-not-an-answer contract is unchanged.
422
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
423
+ delayFn: async () => {},
424
+ });
415
425
  await expect(storage.exists("key")).rejects.toThrow(
416
426
  /Artifact existence check failed \(HTTP 500\) for key 'key'/,
417
427
  );
418
428
  });
429
+
430
+ // The bounded-backoff adoption (stigmer/stigmer#468). Classification policy
431
+ // is covered by shared/__tests__/http-retry.test.ts; these cases pin the
432
+ // loop's behavior at this client's call sites: transient failures recover
433
+ // (each of these fails on pre-#468 code, which threw on the first error),
434
+ // deterministic failures never retry, and a hung request is aborted at the
435
+ // per-request bound instead of stalling forever.
436
+ describe("retry behavior (#468)", () => {
437
+ let recordedDelays: number[];
438
+
439
+ beforeEach(() => {
440
+ recordedDelays = [];
441
+ });
442
+
443
+ function makeStorage() {
444
+ return new ProxyArtifactStorage("https://proxy.example.com", "tok", {
445
+ delayFn: async (ms) => {
446
+ recordedDelays.push(ms);
447
+ },
448
+ });
449
+ }
450
+
451
+ it("upload recovers from a transient presign failure", async () => {
452
+ let presignCalls = 0;
453
+ const fetchSpy = vi.fn(async (input: string | URL | Request) => {
454
+ const url = typeof input === "string" ? input : input.toString();
455
+ if (url.includes("presigned-upload-url")) {
456
+ presignCalls++;
457
+ if (presignCalls === 1) {
458
+ return new Response("bad gateway", { status: 502 });
459
+ }
460
+ return new Response(JSON.stringify({ url: "https://r2.example.com/put" }), { status: 200 });
461
+ }
462
+ return new Response("", { status: 200 });
463
+ });
464
+ globalThis.fetch = fetchSpy as unknown as typeof fetch;
465
+
466
+ const key = await makeStorage().upload("artifacts/e/f.txt", Buffer.from("x"));
467
+
468
+ expect(key).toBe("artifacts/e/f.txt");
469
+ expect(presignCalls).toBe(2);
470
+ expect(recordedDelays).toEqual([250]);
471
+ });
472
+
473
+ it("upload recovers from a transient R2 PUT failure, re-sending identical bytes", async () => {
474
+ const putBodies: string[] = [];
475
+ let putCalls = 0;
476
+ globalThis.fetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
477
+ const url = typeof input === "string" ? input : input.toString();
478
+ if (url.includes("presigned-upload-url")) {
479
+ return new Response(JSON.stringify({ url: "https://r2.example.com/put" }), { status: 200 });
480
+ }
481
+ putCalls++;
482
+ putBodies.push(String(init?.body));
483
+ if (putCalls === 1) {
484
+ return new Response("unavailable", { status: 503 });
485
+ }
486
+ return new Response("", { status: 200 });
487
+ }) as typeof fetch;
488
+
489
+ await makeStorage().upload("k", Buffer.from("same-bytes"));
490
+
491
+ expect(putCalls).toBe(2);
492
+ expect(putBodies[0]).toBe(putBodies[1]);
493
+ expect(recordedDelays).toEqual([250]);
494
+ });
495
+
496
+ it("download recovers from a transient network failure on the R2 GET", async () => {
497
+ let getCalls = 0;
498
+ globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
499
+ const url = typeof input === "string" ? input : input.toString();
500
+ if (url.includes("presigned-download-url")) {
501
+ return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
502
+ }
503
+ getCalls++;
504
+ if (getCalls === 1) {
505
+ // undici's network-failure shape (retryable).
506
+ throw new TypeError("fetch failed");
507
+ }
508
+ return new Response(Buffer.from("payload"), { status: 200 });
509
+ }) as typeof fetch;
510
+
511
+ const got = await makeStorage().download("k");
512
+
513
+ expect(got.toString()).toBe("payload");
514
+ expect(getCalls).toBe(2);
515
+ expect(recordedDelays).toEqual([250]);
516
+ });
517
+
518
+ it("never retries a deterministic presign refusal (403)", async () => {
519
+ const fetchSpy = vi.fn(async () => new Response("forbidden", { status: 403 }));
520
+ globalThis.fetch = fetchSpy as unknown as typeof fetch;
521
+
522
+ await expect(makeStorage().upload("k", Buffer.from("x"))).rejects.toThrow(
523
+ "Failed to get presigned upload URL (HTTP 403)",
524
+ );
525
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
526
+ expect(recordedDelays).toEqual([]);
527
+ });
528
+
529
+ it("aborts a hung request at the per-request bound and retries it", async () => {
530
+ let calls = 0;
531
+ globalThis.fetch = vi.fn((input: string | URL | Request, init?: RequestInit) => {
532
+ calls++;
533
+ if (calls === 1) {
534
+ // Hang until the AbortSignal.timeout fires, then reject the way
535
+ // undici does — proving the bound converts a stall into a
536
+ // retryable attempt instead of waiting forever.
537
+ return new Promise<Response>((_, reject) => {
538
+ init?.signal?.addEventListener("abort", () =>
539
+ reject(init.signal!.reason as Error),
540
+ );
541
+ });
542
+ }
543
+ return Promise.resolve(
544
+ new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 }),
545
+ );
546
+ }) as typeof fetch;
547
+
548
+ const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok", {
549
+ requestTimeoutMs: 20,
550
+ delayFn: async (ms) => {
551
+ recordedDelays.push(ms);
552
+ },
553
+ });
554
+
555
+ const url = await storage.getDownloadUrl("k");
556
+
557
+ expect(url).toBe("https://r2.example.com/dl");
558
+ expect(calls).toBe(2);
559
+ expect(recordedDelays).toEqual([250]);
560
+ });
561
+ });
419
562
  });
420
563
 
421
564
  // ── Factory ──────────────────────────────────────────────────────────