@stigmer/runner 3.9.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +12 -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 +32 -0
  9. package/dist/activities/discover-mcp-server.js +162 -27
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  12. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  13. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  15. package/dist/activities/execute-cursor/approval-state.js +7 -1
  16. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  17. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  18. package/dist/activities/execute-cursor/attachment-resolver.js +25 -4
  19. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  20. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  21. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  22. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  24. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  25. package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
  26. package/dist/activities/execute-cursor/error-classifier.js +81 -3
  27. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  28. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  29. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  30. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  31. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  32. package/dist/activities/execute-cursor/hook-script.js +72 -10
  33. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  34. package/dist/activities/execute-cursor/index.d.ts +5 -1
  35. package/dist/activities/execute-cursor/index.js +59 -58
  36. package/dist/activities/execute-cursor/index.js.map +1 -1
  37. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  38. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  39. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  40. package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
  41. package/dist/activities/execute-cursor/prompt-builder.js +12 -7
  42. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  43. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  44. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  45. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  46. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  47. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  48. package/dist/activities/execute-deep-agent/environment.js +3 -1
  49. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/index.js +15 -0
  51. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  52. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  53. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  54. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  56. package/dist/activities/execute-deep-agent/setup.js +65 -26
  57. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  58. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  59. package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
  60. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
  62. package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
  63. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  64. package/dist/activities/hydrate-workflow-execution.js +3 -1
  65. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  66. package/dist/activities/workflow-event-activities.d.ts +28 -10
  67. package/dist/activities/workflow-event-activities.js +87 -58
  68. package/dist/activities/workflow-event-activities.js.map +1 -1
  69. package/dist/claimcheck/payload-codec.js +21 -1
  70. package/dist/claimcheck/payload-codec.js.map +1 -1
  71. package/dist/client/stigmer-client.d.ts +9 -4
  72. package/dist/client/stigmer-client.js +28 -15
  73. package/dist/client/stigmer-client.js.map +1 -1
  74. package/dist/encryption/config.d.ts +32 -0
  75. package/dist/encryption/config.js +68 -0
  76. package/dist/encryption/config.js.map +1 -0
  77. package/dist/encryption/index.d.ts +3 -0
  78. package/dist/encryption/index.js +3 -0
  79. package/dist/encryption/index.js.map +1 -0
  80. package/dist/encryption/payload-codec.d.ts +41 -0
  81. package/dist/encryption/payload-codec.js +130 -0
  82. package/dist/encryption/payload-codec.js.map +1 -0
  83. package/dist/payload-codecs.d.ts +16 -0
  84. package/dist/payload-codecs.js +38 -0
  85. package/dist/payload-codecs.js.map +1 -0
  86. package/dist/preflight.d.ts +31 -0
  87. package/dist/preflight.js +43 -0
  88. package/dist/preflight.js.map +1 -1
  89. package/dist/runner-manager.js +5 -15
  90. package/dist/runner-manager.js.map +1 -1
  91. package/dist/runner.js +5 -16
  92. package/dist/runner.js.map +1 -1
  93. package/dist/shared/approval-policy.d.ts +9 -3
  94. package/dist/shared/approval-policy.js +15 -6
  95. package/dist/shared/approval-policy.js.map +1 -1
  96. package/dist/shared/attachment-naming.d.ts +53 -0
  97. package/dist/shared/attachment-naming.js +59 -0
  98. package/dist/shared/attachment-naming.js.map +1 -0
  99. package/dist/shared/attachment-vision.d.ts +43 -2
  100. package/dist/shared/attachment-vision.js +72 -6
  101. package/dist/shared/attachment-vision.js.map +1 -1
  102. package/dist/shared/caller-identity.d.ts +23 -2
  103. package/dist/shared/caller-identity.js +36 -5
  104. package/dist/shared/caller-identity.js.map +1 -1
  105. package/dist/shared/channel-attachment.js +1 -0
  106. package/dist/shared/channel-attachment.js.map +1 -1
  107. package/dist/shared/checkpointer/http-saver.d.ts +42 -1
  108. package/dist/shared/checkpointer/http-saver.js +96 -8
  109. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  110. package/dist/shared/conversation-attachment.js +1 -0
  111. package/dist/shared/conversation-attachment.js.map +1 -1
  112. package/dist/shared/datastore-attachment.d.ts +50 -7
  113. package/dist/shared/datastore-attachment.js +93 -11
  114. package/dist/shared/datastore-attachment.js.map +1 -1
  115. package/dist/shared/http-retry.d.ts +43 -0
  116. package/dist/shared/http-retry.js +50 -0
  117. package/dist/shared/http-retry.js.map +1 -0
  118. package/dist/shared/llm-backend.d.ts +275 -0
  119. package/dist/shared/llm-backend.js +425 -0
  120. package/dist/shared/llm-backend.js.map +1 -0
  121. package/dist/shared/llm-proxy.d.ts +8 -0
  122. package/dist/shared/llm-proxy.js +15 -0
  123. package/dist/shared/llm-proxy.js.map +1 -1
  124. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  125. package/dist/shared/mcp-enabled-tools.js +86 -0
  126. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  127. package/dist/shared/mcp-manager.d.ts +17 -2
  128. package/dist/shared/mcp-manager.js +37 -25
  129. package/dist/shared/mcp-manager.js.map +1 -1
  130. package/dist/shared/mcp-resolver.d.ts +39 -2
  131. package/dist/shared/mcp-resolver.js +38 -2
  132. package/dist/shared/mcp-resolver.js.map +1 -1
  133. package/dist/shared/model-client.d.ts +12 -5
  134. package/dist/shared/model-client.js +138 -18
  135. package/dist/shared/model-client.js.map +1 -1
  136. package/dist/shared/model-error.js +198 -5
  137. package/dist/shared/model-error.js.map +1 -1
  138. package/dist/shared/model-registry.d.ts +20 -2
  139. package/dist/shared/model-registry.js +37 -2
  140. package/dist/shared/model-registry.js.map +1 -1
  141. package/dist/shared/plan-mode-permissions.d.ts +26 -0
  142. package/dist/shared/plan-mode-permissions.js +28 -0
  143. package/dist/shared/plan-mode-permissions.js.map +1 -0
  144. package/dist/worker.d.ts +2 -1
  145. package/dist/worker.js +2 -4
  146. package/dist/worker.js.map +1 -1
  147. package/dist/workflow-engine/types.d.ts +18 -0
  148. package/dist/workflow-engine/types.js.map +1 -1
  149. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  150. package/dist/workflows/call-agent-orchestrator.js +1 -0
  151. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  152. package/dist/workflows/connect-mcp-server.js +7 -0
  153. package/dist/workflows/connect-mcp-server.js.map +1 -1
  154. package/dist/workflows/engine-core.js +23 -2
  155. package/dist/workflows/engine-core.js.map +1 -1
  156. package/dist/workflows/execute-from-execution.d.ts +1 -1
  157. package/dist/workflows/execute-from-execution.js +11 -1
  158. package/dist/workflows/execute-from-execution.js.map +1 -1
  159. package/package.json +8 -2
  160. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  161. package/src/__tests__/encryption-codec.test.ts +234 -0
  162. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  163. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  164. package/src/__tests__/preflight.test.ts +50 -2
  165. package/src/activities/__tests__/call-llm.test.ts +75 -0
  166. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  167. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  168. package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
  169. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  170. package/src/activities/call-llm.ts +9 -16
  171. package/src/activities/classify-tool-approvals.ts +34 -4
  172. package/src/activities/discover-mcp-server.ts +190 -32
  173. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  174. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  175. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
  176. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
  177. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  178. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  179. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  180. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  181. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  182. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  183. package/src/activities/execute-cursor/approval-state.ts +30 -1
  184. package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
  185. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  186. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  187. package/src/activities/execute-cursor/error-classifier.ts +91 -4
  188. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  189. package/src/activities/execute-cursor/hook-script.ts +74 -10
  190. package/src/activities/execute-cursor/index.ts +63 -72
  191. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  192. package/src/activities/execute-cursor/prompt-builder.ts +34 -9
  193. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  194. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
  195. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  196. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  197. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  198. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  199. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  200. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  201. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
  202. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  203. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  204. package/src/activities/execute-deep-agent/environment.ts +3 -1
  205. package/src/activities/execute-deep-agent/index.ts +20 -0
  206. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  207. package/src/activities/execute-deep-agent/setup.ts +86 -31
  208. package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
  209. package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
  210. package/src/activities/hydrate-workflow-execution.ts +3 -1
  211. package/src/activities/workflow-event-activities.ts +96 -69
  212. package/src/claimcheck/payload-codec.ts +33 -1
  213. package/src/client/__tests__/stigmer-client.test.ts +8 -8
  214. package/src/client/stigmer-client.ts +32 -18
  215. package/src/encryption/config.ts +91 -0
  216. package/src/encryption/index.ts +3 -0
  217. package/src/encryption/payload-codec.ts +152 -0
  218. package/src/payload-codecs.ts +56 -0
  219. package/src/preflight.ts +45 -0
  220. package/src/runner-manager.ts +6 -24
  221. package/src/runner.ts +6 -25
  222. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  223. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  224. package/src/shared/__tests__/attachment-vision.test.ts +97 -0
  225. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  226. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  227. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  228. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  229. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  230. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  231. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  232. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  233. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  234. package/src/shared/__tests__/http-retry.test.ts +67 -0
  235. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  236. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  237. package/src/shared/__tests__/mcp-manager.test.ts +170 -2
  238. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  239. package/src/shared/__tests__/model-client.test.ts +154 -0
  240. package/src/shared/__tests__/model-error.test.ts +289 -1
  241. package/src/shared/__tests__/model-registry.test.ts +71 -0
  242. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  243. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  244. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  245. package/src/shared/approval-policy.ts +14 -7
  246. package/src/shared/attachment-naming.ts +78 -0
  247. package/src/shared/attachment-vision.ts +92 -9
  248. package/src/shared/caller-identity.ts +40 -5
  249. package/src/shared/channel-attachment.ts +1 -0
  250. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  251. package/src/shared/checkpointer/http-saver.ts +117 -9
  252. package/src/shared/conversation-attachment.ts +1 -0
  253. package/src/shared/datastore-attachment.ts +106 -11
  254. package/src/shared/http-retry.ts +50 -0
  255. package/src/shared/llm-backend.ts +544 -0
  256. package/src/shared/llm-proxy.ts +15 -0
  257. package/src/shared/mcp-enabled-tools.ts +105 -0
  258. package/src/shared/mcp-manager.ts +43 -26
  259. package/src/shared/mcp-resolver.ts +73 -2
  260. package/src/shared/model-client.ts +161 -19
  261. package/src/shared/model-error.ts +222 -4
  262. package/src/shared/model-registry.ts +50 -2
  263. package/src/shared/plan-mode-permissions.ts +30 -0
  264. package/src/worker.ts +4 -5
  265. package/src/workflow-engine/types.ts +18 -0
  266. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  267. package/src/workflows/call-agent-orchestrator.ts +10 -0
  268. package/src/workflows/connect-mcp-server.ts +7 -0
  269. package/src/workflows/engine-core.ts +23 -2
  270. package/src/workflows/execute-from-execution.ts +12 -2
@@ -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);
@@ -600,6 +608,122 @@ describe("DiscoverMcpServer activity", () => {
600
608
  expect(result.newToolsFingerprint).toBe("");
601
609
  });
602
610
 
611
+ it("fails closed when the EC read errors and the server requires credentials", async () => {
612
+ // Issue #239: limping ahead with an empty env either died later as an
613
+ // opaque PlaceholderResolutionError or dialed the endpoint with a
614
+ // garbage credential and wedged in the 4xx → SSE-fallback limbo. The
615
+ // failure must name the root cause: credential delivery.
616
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
617
+
618
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
619
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
620
+ const mockClient = makeMockStigmerClient({
621
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
622
+ });
623
+ mockClient.getExecutionContextByExecutionId = vi
624
+ .fn()
625
+ .mockRejectedValue(new Error("PERMISSION_DENIED: not scope-bound"));
626
+
627
+ await expect(
628
+ discoverMcpServer(
629
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-1" },
630
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
631
+ ),
632
+ ).rejects.toThrow(CredentialResolutionError);
633
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
634
+ });
635
+
636
+ it("keeps the lenient path when the EC read errors but no env is declared", async () => {
637
+ const { discoverMcpServer } = await import("../discover-mcp-server.js");
638
+
639
+ const mockClient = makeMockStigmerClient({
640
+ mcpServer: makeMcpServer({
641
+ metadata: { slug: "no-env" },
642
+ spec: makeHttpSpec("https://mcp.example.com"),
643
+ }),
644
+ });
645
+ mockClient.getExecutionContextByExecutionId = vi
646
+ .fn()
647
+ .mockRejectedValue(new Error("transient"));
648
+
649
+ mockInitializeConnections.mockResolvedValue({});
650
+ mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
651
+
652
+ const result = await discoverMcpServer(
653
+ { mcpServerId: "mcp-lenient", executionContextId: "ctx-2" },
654
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
655
+ );
656
+ expect(result.tools).toEqual([]);
657
+ });
658
+
659
+ it("keeps the lenient path when all declared env vars are optional", async () => {
660
+ // Caller-identity-style servers declare only optional keys; discovery
661
+ // legitimately proceeds without an EC for them (the injections below
662
+ // resolveEnvVarsForDiscovery supply the values).
663
+ const { discoverMcpServer } = await import("../discover-mcp-server.js");
664
+
665
+ const spec = makeHttpSpec("https://mcp.example.com");
666
+ spec.env = { STIGMER_CALLER_IDENTITY_KIND: { optional: true } };
667
+ const mockClient = makeMockStigmerClient({
668
+ mcpServer: makeMcpServer({ metadata: { slug: "optional-only" }, spec }),
669
+ });
670
+ mockClient.getExecutionContextByExecutionId = vi
671
+ .fn()
672
+ .mockRejectedValue(new Error("transient"));
673
+
674
+ mockInitializeConnections.mockResolvedValue({});
675
+ mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
676
+
677
+ const result = await discoverMcpServer(
678
+ { mcpServerId: "mcp-optional", executionContextId: "ctx-3" },
679
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
680
+ );
681
+ expect(result.tools).toEqual([]);
682
+ });
683
+
684
+ it("fails closed when the EC is empty and the server requires credentials", async () => {
685
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
686
+
687
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
688
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
689
+ const mockClient = makeMockStigmerClient({
690
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
691
+ executionContext: { spec: { data: {} } },
692
+ });
693
+
694
+ const promise = discoverMcpServer(
695
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-4" },
696
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
697
+ );
698
+ await expect(promise).rejects.toThrow(CredentialResolutionError);
699
+ await expect(promise).rejects.toThrow(/delivered no credentials/);
700
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
701
+ });
702
+
703
+ it("fails closed when a delivered credential is the redaction sentinel", async () => {
704
+ // A redacted value means the server-side decrypt gate refused this
705
+ // runner's credential class/scope — dialing with the literal sentinel
706
+ // can only produce a misleading 401 (the issue-#239 M2 mechanism).
707
+ const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
708
+
709
+ const spec = makeHttpSpec("https://mcp.monday.com/mcp");
710
+ spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
711
+ const mockClient = makeMockStigmerClient({
712
+ mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
713
+ executionContext: {
714
+ spec: { data: { MONDAY_ACCESS_TOKEN: { value: "***REDACTED***", isSecret: true } } },
715
+ },
716
+ });
717
+
718
+ const promise = discoverMcpServer(
719
+ { mcpServerId: "mcp-monday", executionContextId: "ctx-5" },
720
+ { stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
721
+ );
722
+ await expect(promise).rejects.toThrow(CredentialResolutionError);
723
+ await expect(promise).rejects.toThrow(/MONDAY_ACCESS_TOKEN.*delivered redacted|delivered redacted.*MONDAY_ACCESS_TOKEN/s);
724
+ expect(mockInitializeConnections).not.toHaveBeenCalled();
725
+ });
726
+
603
727
  it("closes MCP client even when discovery fails", async () => {
604
728
  const { discoverMcpServer } = await import("../discover-mcp-server.js");
605
729
 
@@ -620,6 +744,85 @@ describe("DiscoverMcpServer activity", () => {
620
744
  });
621
745
  });
622
746
 
747
+ // ─────────────────────────────────────────────────────────────────────────
748
+ // Transport-aware init bounds (issue #239)
749
+ // ─────────────────────────────────────────────────────────────────────────
750
+
751
+ describe("transport-aware init bounds", () => {
752
+ it("bounds HTTP endpoints at 30s and stdio at 270s", async () => {
753
+ // HTTP has no cold-start excuse: a healthy endpoint completes the MCP
754
+ // handshake in seconds, so a short bound converts the silent-SSE hang
755
+ // into a fast actionable failure. stdio keeps the cold-start allowance
756
+ // (issue #243). The OSS server's connect-workflow budget (connectTimeout,
757
+ // controller/connect.go — pinned there) is derived from the stdio bound
758
+ // + the classification floor, so both bounds stay reachable under it.
759
+ const { initTimeoutMsFor } = await import("../discover-mcp-server.js");
760
+ expect(initTimeoutMsFor("http")).toBe(30_000);
761
+ expect(initTimeoutMsFor("sse")).toBe(30_000);
762
+ expect(initTimeoutMsFor("stdio")).toBe(270_000);
763
+ });
764
+
765
+ it("names the endpoint URL in the HTTP timeout message", async () => {
766
+ const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
767
+ const message = initTimeoutMessageFor("monday", {
768
+ slug: "monday",
769
+ connectionType: "http",
770
+ url: "https://mcp.monday.com/mcp",
771
+ toolApprovals: [],
772
+ pinnedToolApprovals: [],
773
+ toolApprovalOverrides: [],
774
+ discoveredCapabilitiesEmpty: true,
775
+ });
776
+ expect(message).toContain("https://mcp.monday.com/mcp");
777
+ expect(message).toContain("30s");
778
+ expect(message).toContain("SSE fallback");
779
+ });
780
+
781
+ it("names the command in the stdio timeout message", async () => {
782
+ const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
783
+ const message = initTimeoutMessageFor("filesystem", {
784
+ slug: "filesystem",
785
+ connectionType: "stdio",
786
+ command: "npx",
787
+ toolApprovals: [],
788
+ pinnedToolApprovals: [],
789
+ toolApprovalOverrides: [],
790
+ discoveredCapabilitiesEmpty: true,
791
+ });
792
+ expect(message).toContain("npx");
793
+ expect(message).toContain("270s");
794
+ expect(message).toContain("cold start");
795
+ });
796
+ });
797
+
798
+ // ─────────────────────────────────────────────────────────────────────────
799
+ // Heartbeat contract (issue #239)
800
+ // ─────────────────────────────────────────────────────────────────────────
801
+
802
+ describe("heartbeat contract", () => {
803
+ it("starts a heartbeat for the activity and stops it on completion", async () => {
804
+ // The connect workflow proxies this activity with a 60s heartbeatTimeout.
805
+ // Without a heartbeat loop, any discovery slower than 60s was killed by
806
+ // Temporal with an opaque heartbeat timeout — never reaching the
807
+ // actionable init-timeout errors (issues #239/#243).
808
+ const { createDiscoverMcpServerActivities } = await import("../discover-mcp-server.js");
809
+ const { startHeartbeat } = await import("../../shared/heartbeat.js");
810
+
811
+ vi.mocked(startHeartbeat).mockClear();
812
+ mockHeartbeatStop.mockClear();
813
+
814
+ const { DiscoverMcpServerCapabilities } = createDiscoverMcpServerActivities(makeConfig());
815
+ try {
816
+ await DiscoverMcpServerCapabilities({ mcpServerId: "test" });
817
+ } catch {
818
+ // Expected — no real backend behind the factory's own client.
819
+ }
820
+
821
+ expect(startHeartbeat).toHaveBeenCalledTimes(1);
822
+ expect(mockHeartbeatStop).toHaveBeenCalledTimes(1);
823
+ });
824
+ });
825
+
623
826
  // ─────────────────────────────────────────────────────────────────────────
624
827
  // Idle Watchdog Integration
625
828
  // ─────────────────────────────────────────────────────────────────────────
@@ -7,11 +7,15 @@ import type { WorkflowEventDescriptor } from "../../workflow-engine/types.js";
7
7
 
8
8
  const mockGetEventLogHighWaterMark = vi.fn<(executionId: string) => Promise<bigint>>();
9
9
  const mockGetWorkflowExecution = vi.fn<(executionId: string) => Promise<unknown>>();
10
+ const mockUpdateStatus = vi.fn<(input: unknown) => Promise<unknown>>();
10
11
 
11
12
  vi.mock("../../client/stigmer-client.js", () => ({
12
13
  StigmerClient: vi.fn().mockImplementation(() => ({
13
14
  getEventLogHighWaterMark: (...args: unknown[]) => mockGetEventLogHighWaterMark(...(args as [string])),
14
15
  getWorkflowExecution: (...args: unknown[]) => mockGetWorkflowExecution(...(args as [string])),
16
+ workflowExecutionCommand: {
17
+ updateStatus: (...args: unknown[]) => mockUpdateStatus(args[0]),
18
+ },
15
19
  })),
16
20
  }));
17
21
 
@@ -29,8 +33,9 @@ describe("initSequenceFromEventLog", () => {
29
33
  mockGetEventLogHighWaterMark.mockReset();
30
34
  });
31
35
 
32
- it("sets counter to 0 when executionId is empty", async () => {
33
- await initSequenceFromEventLog("");
36
+ it("returns 0 and resets the legacy counter when executionId is empty", async () => {
37
+ const highWaterMark = await initSequenceFromEventLog("");
38
+ expect(highWaterMark).toBe(0);
34
39
 
35
40
  const evt = toProtoEvent({
36
41
  type: "task_started",
@@ -43,10 +48,11 @@ describe("initSequenceFromEventLog", () => {
43
48
  expect(mockGetEventLogHighWaterMark).not.toHaveBeenCalled();
44
49
  });
45
50
 
46
- it("sets counter to 0 when server returns no events", async () => {
51
+ it("returns 0 when server has no events", async () => {
47
52
  mockGetEventLogHighWaterMark.mockResolvedValue(BigInt(0));
48
53
 
49
- await initSequenceFromEventLog("wfx_test-1");
54
+ const highWaterMark = await initSequenceFromEventLog("wfx_test-1");
55
+ expect(highWaterMark).toBe(0);
50
56
 
51
57
  const evt = toProtoEvent({
52
58
  type: "task_started",
@@ -58,7 +64,15 @@ describe("initSequenceFromEventLog", () => {
58
64
  expect(evt.sequenceNumber).toBe(BigInt(1));
59
65
  });
60
66
 
61
- it("continues from high-water mark when events exist", async () => {
67
+ it("returns the high-water mark as a plain number (Temporal payload converter cannot carry BigInt)", async () => {
68
+ mockGetEventLogHighWaterMark.mockResolvedValue(BigInt(42));
69
+
70
+ const highWaterMark = await initSequenceFromEventLog("wfx_recovery-1");
71
+ expect(highWaterMark).toBe(42);
72
+ expect(typeof highWaterMark).toBe("number");
73
+ });
74
+
75
+ it("seeds the legacy counter so pre-patch replays continue from N+1", async () => {
62
76
  mockGetEventLogHighWaterMark.mockResolvedValue(BigInt(42));
63
77
 
64
78
  await initSequenceFromEventLog("wfx_recovery-1");
@@ -87,7 +101,8 @@ describe("initSequenceFromEventLog", () => {
87
101
  it("handles large sequence numbers", async () => {
88
102
  mockGetEventLogHighWaterMark.mockResolvedValue(BigInt(999999));
89
103
 
90
- await initSequenceFromEventLog("wfx_large-seq");
104
+ const highWaterMark = await initSequenceFromEventLog("wfx_large-seq");
105
+ expect(highWaterMark).toBe(999999);
91
106
 
92
107
  const evt = toProtoEvent({
93
108
  type: "task_started",
@@ -112,7 +127,52 @@ describe("toProtoEvent", () => {
112
127
  });
113
128
 
114
129
  describe("sequence numbering", () => {
115
- it("assigns monotonically increasing sequence numbers", () => {
130
+ it("honors the workflow-assigned sequenceNumber when stamped on the descriptor", () => {
131
+ const evt = toProtoEvent({
132
+ type: "task_started",
133
+ taskName: "t1",
134
+ occurredAt: NOW,
135
+ taskKind: "set",
136
+ attemptNumber: 1,
137
+ sequenceNumber: 77,
138
+ });
139
+
140
+ expect(evt.sequenceNumber).toBe(BigInt(77));
141
+ });
142
+
143
+ it("workflow-assigned sequences are stable across conversions (retry idempotency)", () => {
144
+ const desc: WorkflowEventDescriptor = {
145
+ type: "task_started",
146
+ taskName: "t1",
147
+ occurredAt: NOW,
148
+ taskKind: "set",
149
+ attemptNumber: 1,
150
+ sequenceNumber: 5,
151
+ };
152
+
153
+ // Simulates an activity retry re-converting the same descriptors:
154
+ // the sequence must not change between attempts.
155
+ expect(toProtoEvent(desc).sequenceNumber).toBe(BigInt(5));
156
+ expect(toProtoEvent(desc).sequenceNumber).toBe(BigInt(5));
157
+ });
158
+
159
+ it("a stamped descriptor does not consume the legacy counter", () => {
160
+ const unstamped: WorkflowEventDescriptor = {
161
+ type: "task_started",
162
+ taskName: "t",
163
+ occurredAt: NOW,
164
+ taskKind: "set",
165
+ attemptNumber: 1,
166
+ };
167
+
168
+ const e1 = toProtoEvent(unstamped);
169
+ toProtoEvent({ ...unstamped, sequenceNumber: 99 });
170
+ const e2 = toProtoEvent(unstamped);
171
+
172
+ expect(e2.sequenceNumber).toBe(e1.sequenceNumber + BigInt(1));
173
+ });
174
+
175
+ it("legacy path: assigns monotonically increasing sequence numbers when unstamped", () => {
116
176
  const desc: WorkflowEventDescriptor = {
117
177
  type: "task_started",
118
178
  taskName: "t1",
@@ -130,7 +190,7 @@ describe("toProtoEvent", () => {
130
190
  expect(e3.sequenceNumber).toBe(BigInt(3));
131
191
  });
132
192
 
133
- it("resets to 1 after re-initializing with empty executionId", async () => {
193
+ it("legacy path: resets to 1 after re-initializing with empty executionId", async () => {
134
194
  toProtoEvent({
135
195
  type: "task_started",
136
196
  taskName: "t",
@@ -654,8 +714,13 @@ describe("toProtoEvent", () => {
654
714
  });
655
715
 
656
716
  describe("emitWorkflowEvents", () => {
717
+ beforeEach(() => {
718
+ mockUpdateStatus.mockReset();
719
+ });
720
+
657
721
  it("returns silently for empty events array", async () => {
658
722
  await expect(emitWorkflowEvents("exec-1", [])).resolves.toBeUndefined();
723
+ expect(mockUpdateStatus).not.toHaveBeenCalled();
659
724
  });
660
725
 
661
726
  it("returns silently for empty executionId", async () => {
@@ -667,6 +732,40 @@ describe("emitWorkflowEvents", () => {
667
732
  attemptNumber: 1,
668
733
  }];
669
734
  await expect(emitWorkflowEvents("", events)).resolves.toBeUndefined();
735
+ expect(mockUpdateStatus).not.toHaveBeenCalled();
736
+ });
737
+
738
+ it("sends workflow-assigned sequence numbers through to the RPC", async () => {
739
+ mockUpdateStatus.mockResolvedValue({});
740
+
741
+ await emitWorkflowEvents("exec-1", [{
742
+ type: "task_started",
743
+ taskName: "t",
744
+ occurredAt: NOW,
745
+ taskKind: "set",
746
+ attemptNumber: 1,
747
+ sequenceNumber: 12,
748
+ }]);
749
+
750
+ expect(mockUpdateStatus).toHaveBeenCalledTimes(1);
751
+ const input = mockUpdateStatus.mock.calls[0][0] as { events: { sequenceNumber: bigint }[] };
752
+ expect(input.events).toHaveLength(1);
753
+ expect(input.events[0].sequenceNumber).toBe(BigInt(12));
754
+ });
755
+
756
+ it("propagates RPC errors so the local activity retry policy fires", async () => {
757
+ mockUpdateStatus.mockRejectedValue(new Error("server unavailable"));
758
+
759
+ const events: WorkflowEventDescriptor[] = [{
760
+ type: "task_started",
761
+ taskName: "t",
762
+ occurredAt: NOW,
763
+ taskKind: "set",
764
+ attemptNumber: 1,
765
+ sequenceNumber: 1,
766
+ }];
767
+
768
+ await expect(emitWorkflowEvents("exec-1", events)).rejects.toThrow("server unavailable");
670
769
  });
671
770
  });
672
771
 
@@ -31,6 +31,7 @@ import {
31
31
  import { computeLlmCostMicros, ensureLoaded as ensurePricingLoaded } from "../shared/model-pricing.js";
32
32
  import { resolveToApiModelId } from "../shared/model-registry.js";
33
33
  import { buildChatModel } from "../shared/model-client.js";
34
+ import { checkDirectCredentials } from "../shared/llm-backend.js";
34
35
  import { classifyModelCallError } from "../shared/model-error.js";
35
36
 
36
37
  export interface LlmCallConfig {
@@ -169,23 +170,15 @@ export async function callLlmAction(
169
170
  `structured=${!!config.response_schema} execution=${executionId}`,
170
171
  );
171
172
 
172
- // Direct mode (no proxy) requires the provider's own API key. Validated here
173
- // rather than in buildChatModel so the shared module stays free of Temporal
174
- // failure types.
173
+ // Direct mode (no proxy) requires a usable credential path the provider's
174
+ // own API key, or a configured model backend that authenticates itself
175
+ // (vertex uses Google ADC; requiring an Anthropic key there would reject a
176
+ // correctly-configured deployment). Validated here rather than in
177
+ // buildChatModel so the shared module stays free of Temporal failure types.
175
178
  if (!proxyActive) {
176
- if (provider === "openai" && !process.env.OPENAI_API_KEY) {
177
- throw ApplicationFailure.nonRetryable(
178
- `OPENAI_API_KEY is not set and no proxy is configured. ` +
179
- `Set the API key in your environment or connect to a Stigmer Cloud deployment.`,
180
- "LLM_MISSING_API_KEY",
181
- );
182
- }
183
- if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
184
- throw ApplicationFailure.nonRetryable(
185
- `ANTHROPIC_API_KEY is not set and no proxy is configured. ` +
186
- `Set the API key in your environment or connect to a Stigmer Cloud deployment.`,
187
- "LLM_MISSING_API_KEY",
188
- );
179
+ const missing = checkDirectCredentials(provider);
180
+ if (missing !== null) {
181
+ throw ApplicationFailure.nonRetryable(missing, "LLM_MISSING_API_KEY");
189
182
  }
190
183
  }
191
184
 
@@ -23,6 +23,8 @@ import { SystemMessage, HumanMessage } from "@langchain/core/messages";
23
23
  import { activityStarted, activityFinished } from "../idle-watchdog.js";
24
24
  import { getSummarizationModel } from "../shared/model-registry.js";
25
25
  import { buildChatModel } from "../shared/model-client.js";
26
+ import { checkDirectCredentials } from "../shared/llm-backend.js";
27
+ import { tryInferProvider } from "../shared/llm-proxy.js";
26
28
  import type { Config } from "../config.js";
27
29
 
28
30
  const BATCH_SIZE = 40;
@@ -149,7 +151,8 @@ Output one classification per tool, maintaining the input order.`;
149
151
  // ─────────────────────────────────────────────────────────────────────────────
150
152
 
151
153
  export interface ClassifyToolsOptions {
152
- proxyEndpoint: string;
154
+ /** Null when the deployment has no proxy — the runner calls providers directly. */
155
+ proxyEndpoint: string | null;
153
156
  stigmerToken: string | null;
154
157
  primaryModel: string;
155
158
  }
@@ -166,6 +169,26 @@ export async function classifyTools(
166
169
 
167
170
  const model = await getSummarizationModel(options.primaryModel);
168
171
 
172
+ // Direct mode with no credential path fails closed up front, with one
173
+ // actionable message instead of a provider 401 per batch. The message
174
+ // names the economy model and its provider because classification's
175
+ // provider follows the economy-model resolution, not the operator's key:
176
+ // "OPENAI_API_KEY is missing" is baffling to an operator holding an
177
+ // Anthropic key unless the log says which model classification wanted.
178
+ if (!options.proxyEndpoint) {
179
+ const provider = tryInferProvider(model);
180
+ const missing = provider === null ? null : checkDirectCredentials(provider);
181
+ if (missing !== null) {
182
+ console.warn(
183
+ `[ClassifyToolApprovals] Cannot classify ${tools.length} tool(s) for ` +
184
+ `'${serverName}': the classifier model '${model}' (${provider}) has no ` +
185
+ `credential path — failing closed, every tool requires approval until ` +
186
+ `a reconnect re-classifies it. ${missing}`,
187
+ );
188
+ return fallbackApprovals(tools);
189
+ }
190
+ }
191
+
169
192
  const batches: ToolDescriptor[][] = [];
170
193
  for (let i = 0; i < tools.length; i += BATCH_SIZE) {
171
194
  batches.push(tools.slice(i, i + BATCH_SIZE));
@@ -236,7 +259,7 @@ interface ClassifyBatchParams {
236
259
  serverName: string;
237
260
  serverDescription: string;
238
261
  model: string;
239
- proxyEndpoint: string;
262
+ proxyEndpoint: string | null;
240
263
  stigmerToken: string | null;
241
264
  mcpServerId: string | null;
242
265
  batchIdx: number;
@@ -256,7 +279,7 @@ async function classifyBatch(params: ClassifyBatchParams): Promise<ToolApprovalR
256
279
  // primary this routes to Claude, not the hardcoded OpenAI path it used to.
257
280
  const { model: llm } = await buildChatModel({
258
281
  modelName: model,
259
- proxyEndpoint,
282
+ proxyEndpoint: proxyEndpoint ?? undefined,
260
283
  stigmerToken: stigmerToken ?? undefined,
261
284
  headerScope: { mcpServerId: mcpServerId ?? undefined },
262
285
  maxTokens,
@@ -388,8 +411,15 @@ export function createClassifyToolApprovalsActivities(config: Config) {
388
411
  `[ClassifyToolApprovals] Activity started: ${input.tools.length} tools for '${input.serverName}'`,
389
412
  );
390
413
 
414
+ // Only a real proxy is an LLM proxy. stigmerBackendEndpoint (the
415
+ // gRPC control plane) serves no /v1/proxy/llm route — coercing it
416
+ // into one here used to 404 every classification in unproxied OSS
417
+ // and silently disable the vertex backend for this activity. (The
418
+ // registry fetch legitimately falls back to the backend endpoint —
419
+ // see shared/registry-endpoint.ts — because the control plane DOES
420
+ // serve /v1/proxy/model-registry; that fallback does not port here.)
391
421
  return await classifyTools(input, {
392
- proxyEndpoint: config.proxyEndpoint ?? config.stigmerBackendEndpoint,
422
+ proxyEndpoint: config.proxyEndpoint,
393
423
  stigmerToken: config.stigmerToken,
394
424
  primaryModel: config.primaryModel,
395
425
  });