@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
@@ -10,6 +10,14 @@ vi.mock("../../idle-watchdog.js", () => ({
10
10
  activityFinished: vi.fn(),
11
11
  }));
12
12
 
13
+ // The factory starts a Temporal heartbeat loop; outside an activity context
14
+ // its ticks would throw, so replace it with a stop-spy and assert the
15
+ // start/stop contract instead.
16
+ const mockHeartbeatStop = vi.fn();
17
+ vi.mock("../../shared/heartbeat.js", () => ({
18
+ startHeartbeat: vi.fn(() => ({ stop: mockHeartbeatStop, cancelled: false, workerShutdown: false })),
19
+ }));
20
+
13
21
  const mockInitializeConnections = vi.fn();
14
22
  const mockGetClient = vi.fn();
15
23
  const mockClose = vi.fn().mockResolvedValue(undefined);
@@ -408,7 +416,45 @@ describe("DiscoverMcpServer activity", () => {
408
416
  );
409
417
 
410
418
  expect(result.tools).toEqual([]);
411
- expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith("ctx-abc");
419
+ // No payload-carried token -> the read goes out without a per-call
420
+ // credential (the cloud shape, where the ambient credential applies).
421
+ expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith("ctx-abc", undefined);
422
+ });
423
+
424
+ it("presents the payload-carried EC token on the ExecutionContext read (oss#535)", async () => {
425
+ const { discoverMcpServer } = await import("../discover-mcp-server.js");
426
+
427
+ const mockClient = makeMockStigmerClient({
428
+ mcpServer: makeMcpServer({
429
+ metadata: { slug: "with-token" },
430
+ spec: makeStdioSpec("npx", ["server"]),
431
+ }),
432
+ executionContext: {
433
+ spec: {
434
+ data: {
435
+ API_KEY: { value: "secret-123", isSecret: true },
436
+ },
437
+ },
438
+ },
439
+ });
440
+
441
+ const mockMcpClient = makeMockMcpClient({ tools: [] });
442
+ mockInitializeConnections.mockResolvedValue({});
443
+ mockGetClient.mockResolvedValue(mockMcpClient);
444
+
445
+ await discoverMcpServer(
446
+ {
447
+ mcpServerId: "mcp-env",
448
+ executionContextId: "ctx-abc",
449
+ executionContextToken: "scoped-ec-token",
450
+ },
451
+ { stigmerClient: mockClient as any, transportPosture: "stdio-allowed" },
452
+ );
453
+
454
+ expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith(
455
+ "ctx-abc",
456
+ "scoped-ec-token",
457
+ );
412
458
  });
413
459
 
414
460
  it("discovers a caller-identity-templating server via the anonymous sentinel", async () => {
@@ -600,6 +646,122 @@ describe("DiscoverMcpServer activity", () => {
600
646
  expect(result.newToolsFingerprint).toBe("");
601
647
  });
602
648
 
649
+ it("fails closed when the EC read errors and the server requires credentials", async () => {
650
+ // Issue #239: limping ahead with an empty env either died later as an
651
+ // opaque PlaceholderResolutionError or dialed the endpoint with a
652
+ // garbage credential and wedged in the 4xx → SSE-fallback limbo. The
653
+ // failure must name the root cause: credential delivery.
654
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
655
+
656
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
657
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
658
+ const mockClient = makeMockStigmerClient({
659
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
660
+ });
661
+ mockClient.getExecutionContextByExecutionId = vi
662
+ .fn()
663
+ .mockRejectedValue(new Error("PERMISSION_DENIED: not scope-bound"));
664
+
665
+ await expect(
666
+ discoverMcpServer(
667
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-1" },
668
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
669
+ ),
670
+ ).rejects.toThrow(CredentialResolutionError);
671
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
672
+ });
673
+
674
+ it("keeps the lenient path when the EC read errors but no env is declared", async () => {
675
+ const { discoverMcpServer } = await import("../discover-mcp-server.js");
676
+
677
+ const mockClient = makeMockStigmerClient({
678
+ mcpServer: makeMcpServer({
679
+ metadata: { slug: "no-env" },
680
+ spec: makeHttpSpec("https://mcp.example.com"),
681
+ }),
682
+ });
683
+ mockClient.getExecutionContextByExecutionId = vi
684
+ .fn()
685
+ .mockRejectedValue(new Error("transient"));
686
+
687
+ mockInitializeConnections.mockResolvedValue({});
688
+ mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
689
+
690
+ const result = await discoverMcpServer(
691
+ { mcpServerId: "mcp-lenient", executionContextId: "ctx-2" },
692
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
693
+ );
694
+ expect(result.tools).toEqual([]);
695
+ });
696
+
697
+ it("keeps the lenient path when all declared env vars are optional", async () => {
698
+ // Caller-identity-style servers declare only optional keys; discovery
699
+ // legitimately proceeds without an EC for them (the injections below
700
+ // resolveEnvVarsForDiscovery supply the values).
701
+ const { discoverMcpServer } = await import("../discover-mcp-server.js");
702
+
703
+ const spec = makeHttpSpec("https://mcp.example.com");
704
+ spec.env = { STIGMER_CALLER_IDENTITY_KIND: { optional: true } };
705
+ const mockClient = makeMockStigmerClient({
706
+ mcpServer: makeMcpServer({ metadata: { slug: "optional-only" }, spec }),
707
+ });
708
+ mockClient.getExecutionContextByExecutionId = vi
709
+ .fn()
710
+ .mockRejectedValue(new Error("transient"));
711
+
712
+ mockInitializeConnections.mockResolvedValue({});
713
+ mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
714
+
715
+ const result = await discoverMcpServer(
716
+ { mcpServerId: "mcp-optional", executionContextId: "ctx-3" },
717
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
718
+ );
719
+ expect(result.tools).toEqual([]);
720
+ });
721
+
722
+ it("fails closed when the EC is empty and the server requires credentials", async () => {
723
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
724
+
725
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
726
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
727
+ const mockClient = makeMockStigmerClient({
728
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
729
+ executionContext: { spec: { data: {} } },
730
+ });
731
+
732
+ const promise = discoverMcpServer(
733
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-4" },
734
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
735
+ );
736
+ await expect(promise).rejects.toThrow(CredentialResolutionError);
737
+ await expect(promise).rejects.toThrow(/delivered no credentials/);
738
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
739
+ });
740
+
741
+ it("fails closed when a delivered credential is the redaction sentinel", async () => {
742
+ // A redacted value means the server-side decrypt gate refused this
743
+ // runner's credential class/scope — dialing with the literal sentinel
744
+ // can only produce a misleading 401 (the issue-#239 M2 mechanism).
745
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
746
+
747
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
748
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
749
+ const mockClient = makeMockStigmerClient({
750
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
751
+ executionContext: {
752
+ spec: { data: { MONDAY_ACCESS_TOKEN: { value: "***REDACTED***", isSecret: true } } },
753
+ },
754
+ });
755
+
756
+ const promise = discoverMcpServer(
757
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-5" },
758
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
759
+ );
760
+ await expect(promise).rejects.toThrow(CredentialResolutionError);
761
+ await expect(promise).rejects.toThrow(/MONDAY_ACCESS_TOKEN.*delivered redacted|delivered redacted.*MONDAY_ACCESS_TOKEN/s);
762
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
763
+ });
764
+
603
765
  it("closes MCP client even when discovery fails", async () => {
604
766
  const { discoverMcpServer } = await import("../discover-mcp-server.js");
605
767
 
@@ -620,6 +782,85 @@ describe("DiscoverMcpServer activity", () => {
620
782
  });
621
783
  });
622
784
 
785
+ // ─────────────────────────────────────────────────────────────────────────
786
+ // Transport-aware init bounds (issue #239)
787
+ // ─────────────────────────────────────────────────────────────────────────
788
+
789
+ describe("transport-aware init bounds", () => {
790
+ it("bounds HTTP endpoints at 30s and stdio at 270s", async () => {
791
+ // HTTP has no cold-start excuse: a healthy endpoint completes the MCP
792
+ // handshake in seconds, so a short bound converts the silent-SSE hang
793
+ // into a fast actionable failure. stdio keeps the cold-start allowance
794
+ // (issue #243). The OSS server's connect-workflow budget (connectTimeout,
795
+ // controller/connect.go — pinned there) is derived from the stdio bound
796
+ // + the classification floor, so both bounds stay reachable under it.
797
+ const { initTimeoutMsFor } = await import("../discover-mcp-server.js");
798
+ expect(initTimeoutMsFor("http")).toBe(30_000);
799
+ expect(initTimeoutMsFor("sse")).toBe(30_000);
800
+ expect(initTimeoutMsFor("stdio")).toBe(270_000);
801
+ });
802
+
803
+ it("names the endpoint URL in the HTTP timeout message", async () => {
804
+ const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
805
+ const message = initTimeoutMessageFor("monday", {
806
+ slug: "monday",
807
+ connectionType: "http",
808
+ url: "https://mcp.monday.com/mcp",
809
+ toolApprovals: [],
810
+ pinnedToolApprovals: [],
811
+ toolApprovalOverrides: [],
812
+ discoveredCapabilitiesEmpty: true,
813
+ });
814
+ expect(message).toContain("https://mcp.monday.com/mcp");
815
+ expect(message).toContain("30s");
816
+ expect(message).toContain("SSE fallback");
817
+ });
818
+
819
+ it("names the command in the stdio timeout message", async () => {
820
+ const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
821
+ const message = initTimeoutMessageFor("filesystem", {
822
+ slug: "filesystem",
823
+ connectionType: "stdio",
824
+ command: "npx",
825
+ toolApprovals: [],
826
+ pinnedToolApprovals: [],
827
+ toolApprovalOverrides: [],
828
+ discoveredCapabilitiesEmpty: true,
829
+ });
830
+ expect(message).toContain("npx");
831
+ expect(message).toContain("270s");
832
+ expect(message).toContain("cold start");
833
+ });
834
+ });
835
+
836
+ // ─────────────────────────────────────────────────────────────────────────
837
+ // Heartbeat contract (issue #239)
838
+ // ─────────────────────────────────────────────────────────────────────────
839
+
840
+ describe("heartbeat contract", () => {
841
+ it("starts a heartbeat for the activity and stops it on completion", async () => {
842
+ // The connect workflow proxies this activity with a 60s heartbeatTimeout.
843
+ // Without a heartbeat loop, any discovery slower than 60s was killed by
844
+ // Temporal with an opaque heartbeat timeout — never reaching the
845
+ // actionable init-timeout errors (issues #239/#243).
846
+ const { createDiscoverMcpServerActivities } = await import("../discover-mcp-server.js");
847
+ const { startHeartbeat } = await import("../../shared/heartbeat.js");
848
+
849
+ vi.mocked(startHeartbeat).mockClear();
850
+ mockHeartbeatStop.mockClear();
851
+
852
+ const { DiscoverMcpServerCapabilities } = createDiscoverMcpServerActivities(makeConfig());
853
+ try {
854
+ await DiscoverMcpServerCapabilities({ mcpServerId: "test" });
855
+ } catch {
856
+ // Expected — no real backend behind the factory's own client.
857
+ }
858
+
859
+ expect(startHeartbeat).toHaveBeenCalledTimes(1);
860
+ expect(mockHeartbeatStop).toHaveBeenCalledTimes(1);
861
+ });
862
+ });
863
+
623
864
  // ─────────────────────────────────────────────────────────────────────────
624
865
  // Idle Watchdog Integration
625
866
  // ─────────────────────────────────────────────────────────────────────────
@@ -7,7 +7,13 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
10
- import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "../execute-cursor/error-classifier.js";
10
+ import {
11
+ synthesizeError,
12
+ formatClassifiedError,
13
+ shouldRetryWithFreshAgent,
14
+ DETAIL_FREE_FALLBACK_USER_PREFIX,
15
+ TRANSPORT_TIMEOUT_USER_PREFIX,
16
+ } from "../execute-cursor/error-classifier.js";
11
17
 
12
18
  describe("synthesizeError", () => {
13
19
  let consoleLogSpy: ReturnType<typeof vi.spyOn>;
@@ -213,7 +219,7 @@ describe("synthesizeError", () => {
213
219
 
214
220
  expect(result.category).toBe("unknown");
215
221
  expect(result.source).toBe("fallback");
216
- expect(result.message).toContain("no detail from SDK");
222
+ expect(result.message).toContain(DETAIL_FREE_FALLBACK_USER_PREFIX);
217
223
  expect(result.message).toContain("claude-sonnet-4");
218
224
  });
219
225
 
@@ -261,6 +267,92 @@ describe("synthesizeError", () => {
261
267
  expect(result.message).toContain("agentId=agent-123");
262
268
  });
263
269
 
270
+ // The exact prod shape from oss#492: the Composer 2.5 capacity incident
271
+ // rejected every run with a bare { status: "error" } — all five detail
272
+ // channels empty, fresh agent (the poisoned-handle retry produces this
273
+ // same shape with isResumedHandle: false). End users read this message
274
+ // verbatim in embedded surfaces, so it must lead with actionable copy,
275
+ // keep the diagnostic parenthetical for operators, and be retryable
276
+ // (provider capacity is transient).
277
+ it("detail-free failure on a fresh agent leads with user-facing copy and is retryable (oss#492)", () => {
278
+ const result = synthesizeError({
279
+ sdkError: undefined,
280
+ sdkResultFields: undefined,
281
+ streamErrorMessage: undefined,
282
+ capturedRejection: undefined,
283
+ conversationErrorText: undefined,
284
+ isResumedHandle: false,
285
+ fallbackContext: { model: "composer-2.5", mode: "local", agentId: "agent-abc" },
286
+ });
287
+
288
+ expect(result.category).toBe("unknown");
289
+ expect(result.source).toBe("fallback");
290
+ expect(result.retryable).toBe(true);
291
+ expect(result.message.startsWith(DETAIL_FREE_FALLBACK_USER_PREFIX)).toBe(true);
292
+ // Diagnostic context survives for operators (and the env integration
293
+ // test's Model= matcher).
294
+ expect(result.message).toContain("Model=composer-2.5");
295
+ expect(result.message).toContain("mode=local");
296
+ expect(result.message).toContain("agentId=agent-abc");
297
+ });
298
+
299
+ // sdk-react's isInterruptedError reframes any error containing the phrase
300
+ // "retry or resume" as a neutral resumable notice instead of a failure
301
+ // alert. A capacity failure must render as a failure — guard the copy
302
+ // (formatted end-to-end, tail included) against ever matching that regex.
303
+ it("user-facing fallback copy never trips the sdk-react interrupted-error reframe", () => {
304
+ const interruptedReframe = /\[StallTimeoutError\]|execution interrupted|retry or resume/i;
305
+ for (const prefix of [DETAIL_FREE_FALLBACK_USER_PREFIX, TRANSPORT_TIMEOUT_USER_PREFIX]) {
306
+ expect(prefix).not.toMatch(interruptedReframe);
307
+ }
308
+ const formatted = formatClassifiedError(synthesizeError({
309
+ sdkResultFields: undefined,
310
+ streamErrorMessage: undefined,
311
+ capturedRejection: undefined,
312
+ isResumedHandle: false,
313
+ fallbackContext,
314
+ }));
315
+ expect(formatted).not.toMatch(interruptedReframe);
316
+ });
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Transport-timeout heuristic (0 messages, ~default SDK timeout)
320
+ // ---------------------------------------------------------------------------
321
+
322
+ it("classifies a 0-message ~30s failure as retryable network with user-facing copy", () => {
323
+ const result = synthesizeError({
324
+ sdkResultFields: undefined,
325
+ streamErrorMessage: undefined,
326
+ capturedRejection: undefined,
327
+ isResumedHandle: false,
328
+ fallbackContext,
329
+ durationMs: 30012,
330
+ messageCount: 0,
331
+ });
332
+
333
+ expect(result.category).toBe("network");
334
+ expect(result.source).toBe("fallback");
335
+ expect(result.retryable).toBe(true);
336
+ expect(result.message.startsWith(TRANSPORT_TIMEOUT_USER_PREFIX)).toBe(true);
337
+ expect(result.message).toContain("30012ms");
338
+ expect(result.message).toContain("Model=claude-sonnet-4");
339
+ });
340
+
341
+ it("does not apply the transport heuristic outside the default-timeout window", () => {
342
+ const result = synthesizeError({
343
+ sdkResultFields: undefined,
344
+ streamErrorMessage: undefined,
345
+ capturedRejection: undefined,
346
+ isResumedHandle: false,
347
+ fallbackContext,
348
+ durationMs: 120000,
349
+ messageCount: 0,
350
+ });
351
+
352
+ expect(result.category).toBe("unknown");
353
+ expect(result.message.startsWith(DETAIL_FREE_FALLBACK_USER_PREFIX)).toBe(true);
354
+ });
355
+
264
356
  // ---------------------------------------------------------------------------
265
357
  // Source priority
266
358
  // ---------------------------------------------------------------------------
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Env contract tests for the workflow `run` task (oss#384).
3
+ *
4
+ * These spawn REAL subprocesses through the RunShell / RunScript
5
+ * activities and inspect the environment the child actually received —
6
+ * the leak-reproduction style established by the #256 fix (PR #383).
7
+ * Subprocess output is env variable NAMES only, never values.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
+ import { createRunCommandActivities } from "../run-command.js";
12
+ import { buildRunEnv, RUN_ENV_BASE_KEYS } from "../run-env.js";
13
+ import { RuntimePlaceholderResolutionError } from "../../workflow-engine/resolve.js";
14
+
15
+ /**
16
+ * Credentials planted into the runner process env before each test.
17
+ * Includes a name outside every known denylist to prove the contract
18
+ * is declare-to-receive, not deny-known-names.
19
+ */
20
+ const PLANTED_SENTINELS: Record<string, string> = {
21
+ STIGMER_RUNNER_HITL_SECRET: "sentinel-hitl",
22
+ STIGMER_TOKEN: "sentinel-token",
23
+ CURSOR_API_KEY: "sentinel-cursor",
24
+ ANTHROPIC_API_KEY: "sentinel-anthropic",
25
+ OPERATOR_PRIVATE_CREDENTIAL: "sentinel-arbitrary",
26
+ };
27
+
28
+ const PRINT_ENV_KEYS_SHELL = `node -p 'JSON.stringify(Object.keys(process.env))'`;
29
+ const PRINT_ENV_KEYS_SCRIPT = `console.log(JSON.stringify(Object.keys(process.env)))`;
30
+
31
+ const activities = createRunCommandActivities();
32
+
33
+ async function childEnvKeysViaShell(
34
+ environment?: Record<string, string>,
35
+ runtimeEnv: Record<string, unknown> = {},
36
+ ): Promise<string[]> {
37
+ const stdout = await activities.RunShell({
38
+ mode: "shell",
39
+ command: PRINT_ENV_KEYS_SHELL,
40
+ environment,
41
+ runtimeEnv,
42
+ });
43
+ return JSON.parse(String(stdout)) as string[];
44
+ }
45
+
46
+ async function childEnvKeysViaScript(
47
+ environment?: Record<string, string>,
48
+ runtimeEnv: Record<string, unknown> = {},
49
+ ): Promise<string[]> {
50
+ const stdout = await activities.RunScript({
51
+ mode: "script",
52
+ language: "js",
53
+ code: PRINT_ENV_KEYS_SCRIPT,
54
+ environment,
55
+ runtimeEnv,
56
+ });
57
+ return JSON.parse(String(stdout)) as string[];
58
+ }
59
+
60
+ describe("run task env contract (oss#384)", () => {
61
+ const saved: Record<string, string | undefined> = {};
62
+
63
+ beforeEach(() => {
64
+ for (const [key, value] of Object.entries(PLANTED_SENTINELS)) {
65
+ saved[key] = process.env[key];
66
+ process.env[key] = value;
67
+ }
68
+ });
69
+
70
+ afterEach(() => {
71
+ for (const key of Object.keys(PLANTED_SENTINELS)) {
72
+ if (saved[key] === undefined) {
73
+ delete process.env[key];
74
+ } else {
75
+ process.env[key] = saved[key];
76
+ }
77
+ }
78
+ });
79
+
80
+ describe("RunShell", () => {
81
+ it("does not leak runner process credentials to the child", async () => {
82
+ const keys = await childEnvKeysViaShell();
83
+
84
+ for (const sentinel of Object.keys(PLANTED_SENTINELS)) {
85
+ expect(keys, `runner credential "${sentinel}" leaked to shell child`)
86
+ .not.toContain(sentinel);
87
+ }
88
+ });
89
+
90
+ it("provides the minimal base env and the declared overlay", async () => {
91
+ const keys = await childEnvKeysViaShell({ DECLARED_VAR: "value" });
92
+
93
+ for (const base of RUN_ENV_BASE_KEYS) {
94
+ if (process.env[base] !== undefined) {
95
+ expect(keys, `base key "${base}" missing from shell child`).toContain(base);
96
+ }
97
+ }
98
+ expect(keys).toContain("DECLARED_VAR");
99
+ });
100
+
101
+ it("resolves ${.secrets.KEY} placeholders in declared values just-in-time", async () => {
102
+ const stdout = await activities.RunShell({
103
+ mode: "shell",
104
+ command: `node -p 'process.env.API_KEY'`,
105
+ environment: { API_KEY: "${.secrets.TEST_API_KEY}" },
106
+ runtimeEnv: { TEST_API_KEY: "resolved-secret-value" },
107
+ });
108
+
109
+ expect(stdout).toBe("resolved-secret-value");
110
+ });
111
+
112
+ it("fails non-retryably, naming the variable, when a placeholder key is missing", async () => {
113
+ await expect(
114
+ activities.RunShell({
115
+ mode: "shell",
116
+ command: "true",
117
+ environment: { API_KEY: "${.secrets.NOT_PROVIDED}" },
118
+ runtimeEnv: {},
119
+ }),
120
+ ).rejects.toMatchObject({
121
+ nonRetryable: true,
122
+ type: "RUN_ENV_UNRESOLVED_PLACEHOLDER",
123
+ message: expect.stringContaining("NOT_PROVIDED"),
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("RunScript", () => {
129
+ it("does not leak runner process credentials to the child", async () => {
130
+ const keys = await childEnvKeysViaScript();
131
+
132
+ for (const sentinel of Object.keys(PLANTED_SENTINELS)) {
133
+ expect(keys, `runner credential "${sentinel}" leaked to script child`)
134
+ .not.toContain(sentinel);
135
+ }
136
+ });
137
+
138
+ it("provides the minimal base env and the declared overlay", async () => {
139
+ const keys = await childEnvKeysViaScript({ DECLARED_VAR: "value" });
140
+
141
+ for (const base of RUN_ENV_BASE_KEYS) {
142
+ if (process.env[base] !== undefined) {
143
+ expect(keys, `base key "${base}" missing from script child`).toContain(base);
144
+ }
145
+ }
146
+ expect(keys).toContain("DECLARED_VAR");
147
+ });
148
+
149
+ it("resolves ${.env_vars.KEY} placeholders in declared values just-in-time", async () => {
150
+ const stdout = await activities.RunScript({
151
+ mode: "script",
152
+ language: "js",
153
+ code: "console.log(process.env.REGION)",
154
+ environment: { REGION: "${.env_vars.DEPLOY_REGION}" },
155
+ runtimeEnv: { DEPLOY_REGION: "us-east-1" },
156
+ });
157
+
158
+ expect(stdout).toBe("us-east-1");
159
+ });
160
+ });
161
+ });
162
+
163
+ describe("buildRunEnv", () => {
164
+ it("copies only the base keys present in the runner env", () => {
165
+ const base = {
166
+ PATH: "/usr/bin",
167
+ HOME: "/home/runner",
168
+ TERM: "xterm",
169
+ };
170
+
171
+ const env = buildRunEnv(undefined, {}, base);
172
+
173
+ expect(env).toEqual({ PATH: "/usr/bin", HOME: "/home/runner", TERM: "xterm" });
174
+ });
175
+
176
+ it("never copies undeclared runner variables — the oss#384 tripwire", () => {
177
+ // Guards against a future reintroduction of `{ ...process.env }`:
178
+ // credentials must be structurally absent, whatever their names.
179
+ const base = {
180
+ PATH: "/usr/bin",
181
+ STIGMER_RUNNER_HITL_SECRET: "secret",
182
+ ANY_FUTURE_CREDENTIAL: "secret",
183
+ };
184
+
185
+ const env = buildRunEnv({ DECLARED: "yes" }, {}, base);
186
+
187
+ expect(Object.keys(env).sort()).toEqual(["DECLARED", "PATH"]);
188
+ });
189
+
190
+ it("lets a declared variable override a base variable", () => {
191
+ const base = { PATH: "/usr/bin" };
192
+
193
+ const env = buildRunEnv({ PATH: "/custom/bin" }, {}, base);
194
+
195
+ expect(env.PATH).toBe("/custom/bin");
196
+ });
197
+
198
+ it("passes non-placeholder declared values through untouched", () => {
199
+ const env = buildRunEnv(
200
+ { LITERAL: "plain-value", TEMPLATED: "prefix-${.secrets.KEY}-suffix" },
201
+ { KEY: "mid" },
202
+ {},
203
+ );
204
+
205
+ expect(env.LITERAL).toBe("plain-value");
206
+ expect(env.TEMPLATED).toBe("prefix-mid-suffix");
207
+ });
208
+
209
+ it("throws a named error for a missing placeholder key", () => {
210
+ expect(() =>
211
+ buildRunEnv({ API_KEY: "${.secrets.MISSING}" }, {}, {}),
212
+ ).toThrowError(RuntimePlaceholderResolutionError);
213
+
214
+ try {
215
+ buildRunEnv({ API_KEY: "${.secrets.MISSING}" }, {}, {});
216
+ } catch (err) {
217
+ expect((err as RuntimePlaceholderResolutionError).variableName).toBe("MISSING");
218
+ expect((err as Error).message).toContain('environment "API_KEY"');
219
+ }
220
+ });
221
+ });