@stigmer/runner 3.10.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 (260) 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 +18 -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 +51 -57
  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 +55 -23
  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/caller-identity.d.ts +23 -2
  100. package/dist/shared/caller-identity.js +36 -5
  101. package/dist/shared/caller-identity.js.map +1 -1
  102. package/dist/shared/channel-attachment.js +1 -0
  103. package/dist/shared/channel-attachment.js.map +1 -1
  104. package/dist/shared/checkpointer/http-saver.d.ts +42 -1
  105. package/dist/shared/checkpointer/http-saver.js +96 -8
  106. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  107. package/dist/shared/conversation-attachment.js +1 -0
  108. package/dist/shared/conversation-attachment.js.map +1 -1
  109. package/dist/shared/datastore-attachment.d.ts +50 -7
  110. package/dist/shared/datastore-attachment.js +93 -11
  111. package/dist/shared/datastore-attachment.js.map +1 -1
  112. package/dist/shared/http-retry.d.ts +43 -0
  113. package/dist/shared/http-retry.js +50 -0
  114. package/dist/shared/http-retry.js.map +1 -0
  115. package/dist/shared/llm-backend.d.ts +275 -0
  116. package/dist/shared/llm-backend.js +425 -0
  117. package/dist/shared/llm-backend.js.map +1 -0
  118. package/dist/shared/llm-proxy.d.ts +8 -0
  119. package/dist/shared/llm-proxy.js +15 -0
  120. package/dist/shared/llm-proxy.js.map +1 -1
  121. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  122. package/dist/shared/mcp-enabled-tools.js +86 -0
  123. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  124. package/dist/shared/mcp-manager.d.ts +3 -1
  125. package/dist/shared/mcp-manager.js +17 -4
  126. package/dist/shared/mcp-manager.js.map +1 -1
  127. package/dist/shared/mcp-resolver.d.ts +39 -2
  128. package/dist/shared/mcp-resolver.js +38 -2
  129. package/dist/shared/mcp-resolver.js.map +1 -1
  130. package/dist/shared/model-client.d.ts +12 -5
  131. package/dist/shared/model-client.js +138 -18
  132. package/dist/shared/model-client.js.map +1 -1
  133. package/dist/shared/model-error.js +198 -5
  134. package/dist/shared/model-error.js.map +1 -1
  135. package/dist/shared/plan-mode-permissions.d.ts +26 -0
  136. package/dist/shared/plan-mode-permissions.js +28 -0
  137. package/dist/shared/plan-mode-permissions.js.map +1 -0
  138. package/dist/worker.d.ts +2 -1
  139. package/dist/worker.js +2 -4
  140. package/dist/worker.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +18 -0
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  144. package/dist/workflows/call-agent-orchestrator.js +1 -0
  145. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  146. package/dist/workflows/connect-mcp-server.js +7 -0
  147. package/dist/workflows/connect-mcp-server.js.map +1 -1
  148. package/dist/workflows/engine-core.js +23 -2
  149. package/dist/workflows/engine-core.js.map +1 -1
  150. package/dist/workflows/execute-from-execution.d.ts +1 -1
  151. package/dist/workflows/execute-from-execution.js +11 -1
  152. package/dist/workflows/execute-from-execution.js.map +1 -1
  153. package/package.json +8 -2
  154. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  155. package/src/__tests__/encryption-codec.test.ts +234 -0
  156. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  157. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  158. package/src/__tests__/preflight.test.ts +50 -2
  159. package/src/activities/__tests__/call-llm.test.ts +75 -0
  160. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  161. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  162. package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
  163. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  164. package/src/activities/call-llm.ts +9 -16
  165. package/src/activities/classify-tool-approvals.ts +34 -4
  166. package/src/activities/discover-mcp-server.ts +190 -32
  167. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  168. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  169. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
  170. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
  171. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  172. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  173. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  174. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  175. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  176. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  177. package/src/activities/execute-cursor/approval-state.ts +30 -1
  178. package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
  179. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  180. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  181. package/src/activities/execute-cursor/error-classifier.ts +91 -4
  182. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  183. package/src/activities/execute-cursor/hook-script.ts +74 -10
  184. package/src/activities/execute-cursor/index.ts +55 -71
  185. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  186. package/src/activities/execute-cursor/prompt-builder.ts +34 -9
  187. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  188. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
  189. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  190. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  191. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  192. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  193. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  194. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  195. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
  196. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  197. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  198. package/src/activities/execute-deep-agent/environment.ts +3 -1
  199. package/src/activities/execute-deep-agent/index.ts +20 -0
  200. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  201. package/src/activities/execute-deep-agent/setup.ts +76 -28
  202. package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
  203. package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
  204. package/src/activities/hydrate-workflow-execution.ts +3 -1
  205. package/src/activities/workflow-event-activities.ts +96 -69
  206. package/src/claimcheck/payload-codec.ts +33 -1
  207. package/src/client/__tests__/stigmer-client.test.ts +8 -8
  208. package/src/client/stigmer-client.ts +32 -18
  209. package/src/encryption/config.ts +91 -0
  210. package/src/encryption/index.ts +3 -0
  211. package/src/encryption/payload-codec.ts +152 -0
  212. package/src/payload-codecs.ts +56 -0
  213. package/src/preflight.ts +45 -0
  214. package/src/runner-manager.ts +6 -24
  215. package/src/runner.ts +6 -25
  216. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  217. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  218. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  219. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  220. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  221. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  222. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  223. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  224. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  225. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  226. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  227. package/src/shared/__tests__/http-retry.test.ts +67 -0
  228. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  229. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  230. package/src/shared/__tests__/mcp-manager.test.ts +84 -2
  231. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  232. package/src/shared/__tests__/model-client.test.ts +154 -0
  233. package/src/shared/__tests__/model-error.test.ts +289 -1
  234. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  235. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  236. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  237. package/src/shared/approval-policy.ts +14 -7
  238. package/src/shared/attachment-naming.ts +78 -0
  239. package/src/shared/caller-identity.ts +40 -5
  240. package/src/shared/channel-attachment.ts +1 -0
  241. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  242. package/src/shared/checkpointer/http-saver.ts +117 -9
  243. package/src/shared/conversation-attachment.ts +1 -0
  244. package/src/shared/datastore-attachment.ts +106 -11
  245. package/src/shared/http-retry.ts +50 -0
  246. package/src/shared/llm-backend.ts +544 -0
  247. package/src/shared/llm-proxy.ts +15 -0
  248. package/src/shared/mcp-enabled-tools.ts +105 -0
  249. package/src/shared/mcp-manager.ts +21 -4
  250. package/src/shared/mcp-resolver.ts +73 -2
  251. package/src/shared/model-client.ts +161 -19
  252. package/src/shared/model-error.ts +222 -4
  253. package/src/shared/plan-mode-permissions.ts +30 -0
  254. package/src/worker.ts +4 -5
  255. package/src/workflow-engine/types.ts +18 -0
  256. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  257. package/src/workflows/call-agent-orchestrator.ts +10 -0
  258. package/src/workflows/connect-mcp-server.ts +7 -0
  259. package/src/workflows/engine-core.ts +23 -2
  260. package/src/workflows/execute-from-execution.ts +12 -2
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ effectiveEnabledTools,
4
+ filterToolsByEnabledTools,
5
+ enabledToolsBySlug,
6
+ } from "../mcp-enabled-tools.js";
7
+
8
+ describe("effectiveEnabledTools", () => {
9
+ it("uses the usage's non-empty enabled_tools verbatim", () => {
10
+ expect(effectiveEnabledTools(["a", "b"], ["c"])).toEqual(["a", "b"]);
11
+ });
12
+
13
+ it("falls back to default_enabled_tools when the usage list is empty", () => {
14
+ expect(effectiveEnabledTools([], ["c", "d"])).toEqual(["c", "d"]);
15
+ });
16
+
17
+ it("falls back to default_enabled_tools when the usage list is absent", () => {
18
+ expect(effectiveEnabledTools(undefined, ["c"])).toEqual(["c"]);
19
+ });
20
+
21
+ it("returns undefined (unrestricted) when both lists are empty or absent", () => {
22
+ expect(effectiveEnabledTools([], [])).toBeUndefined();
23
+ expect(effectiveEnabledTools(undefined, undefined)).toBeUndefined();
24
+ expect(effectiveEnabledTools([], undefined)).toBeUndefined();
25
+ });
26
+
27
+ it("returns copies, never the caller's arrays", () => {
28
+ const usage = ["a"];
29
+ const result = effectiveEnabledTools(usage, undefined)!;
30
+ result.push("b");
31
+ expect(usage).toEqual(["a"]);
32
+ });
33
+ });
34
+
35
+ describe("filterToolsByEnabledTools", () => {
36
+ const tool = (name: string) => ({ name });
37
+
38
+ beforeEach(() => {
39
+ vi.restoreAllMocks();
40
+ vi.spyOn(console, "log").mockImplementation(() => {});
41
+ vi.spyOn(console, "warn").mockImplementation(() => {});
42
+ });
43
+
44
+ it("passes every tool through untouched when unrestricted (undefined)", () => {
45
+ const tools = [tool("a"), tool("b")];
46
+ expect(filterToolsByEnabledTools("srv", tools, undefined)).toEqual(tools);
47
+ });
48
+
49
+ it("keeps only the tools on the allow-list (exact bare-name match)", () => {
50
+ const tools = [tool("read"), tool("write"), tool("delete")];
51
+ expect(
52
+ filterToolsByEnabledTools("srv", tools, ["read"]).map((t) => t.name),
53
+ ).toEqual(["read"]);
54
+ });
55
+
56
+ it("warns and intersects when an enabled name is not among the discovered tools", () => {
57
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
58
+ const tools = [tool("read"), tool("write")];
59
+
60
+ const filtered = filterToolsByEnabledTools("srv", tools, ["read", "reed"]);
61
+
62
+ // The restriction still holds (intersection), the run is not failed,
63
+ // and the operator gets a loud pointer at the mismatched name.
64
+ expect(filtered.map((t) => t.name)).toEqual(["read"]);
65
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("reed"));
66
+ });
67
+
68
+ it("yields an empty toolset when no enabled name matches (restriction over availability)", () => {
69
+ const tools = [tool("read")];
70
+ expect(filterToolsByEnabledTools("srv", tools, ["ghost"])).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe("enabledToolsBySlug", () => {
75
+ it("collects only restricted servers, keyed by slug", () => {
76
+ const map = enabledToolsBySlug([
77
+ { slug: "restricted", enabledTools: ["a"] },
78
+ { slug: "open" },
79
+ ]);
80
+ expect(map).toEqual({ restricted: ["a"] });
81
+ });
82
+
83
+ it("returns an empty object when nothing is restricted (the common case)", () => {
84
+ expect(enabledToolsBySlug([{ slug: "open" }])).toEqual({});
85
+ });
86
+ });
@@ -1,14 +1,31 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
2
2
  import type { Connection } from "@langchain/mcp-adapters";
3
- import { toMcpClientConfig } from "../mcp-manager.js";
3
+ import { connectMcpServers, toMcpClientConfig } from "../mcp-manager.js";
4
4
  import type { ResolvedMcpServer } from "../mcp-resolver.js";
5
5
 
6
+ // The adapter client is mocked ONLY for the connectMcpServers tests below
7
+ // (initializeConnections returns the canned per-server tool map); the
8
+ // toMcpClientConfig suites are pure and never construct it.
9
+ const mockMcpClient = vi.hoisted(() => ({
10
+ toolMap: {} as Record<string, { name: string }[]>,
11
+ }));
12
+
13
+ vi.mock("@langchain/mcp-adapters", () => ({
14
+ MultiServerMCPClient: class {
15
+ async initializeConnections() {
16
+ return mockMcpClient.toolMap;
17
+ }
18
+ async close() {}
19
+ },
20
+ }));
21
+
6
22
  function makeServer(overrides: Partial<ResolvedMcpServer>): ResolvedMcpServer {
7
23
  return {
8
24
  slug: "test-server",
9
25
  connectionType: "stdio",
10
26
  toolApprovals: [],
11
27
  pinnedToolApprovals: [],
28
+ toolApprovalOverrides: [],
12
29
  discoveredCapabilitiesEmpty: false,
13
30
  ...overrides,
14
31
  };
@@ -167,3 +184,68 @@ describe("toMcpClientConfig — stdio env isolation (oss#256)", () => {
167
184
  expect(stdioEnv(config, "empty")).toEqual({});
168
185
  });
169
186
  });
187
+
188
+ describe("connectMcpServers — enabled_tools enforcement (issue #350)", () => {
189
+ const tool = (name: string) => ({ name }) as any;
190
+
191
+ beforeEach(() => {
192
+ vi.spyOn(console, "log").mockImplementation(() => {});
193
+ vi.spyOn(console, "warn").mockImplementation(() => {});
194
+ mockMcpClient.toolMap = {};
195
+ });
196
+
197
+ afterEach(() => {
198
+ vi.restoreAllMocks();
199
+ });
200
+
201
+ it("filters a restricted server's discovered tools down to the allow-list, in BOTH the flat list and the per-server map", async () => {
202
+ mockMcpClient.toolMap = { planton: [tool("get"), tool("apply"), tool("destroy")] };
203
+
204
+ const result = await connectMcpServers([
205
+ makeServer({ slug: "planton", command: "npx", enabledTools: ["get"] }),
206
+ ]);
207
+
208
+ // Both result shapes must narrow together: `tools` feeds the model and
209
+ // the built-in general-purpose sub-agent, `serverToolMap` feeds the
210
+ // approval gate's toolServerMap and the sub-agent McpAccess subset check.
211
+ expect(result.tools.map((t) => t.name)).toEqual(["get"]);
212
+ expect(result.serverToolMap.planton.map((t) => t.name)).toEqual(["get"]);
213
+ });
214
+
215
+ it("leaves an unrestricted server (enabledTools absent — e.g. a synthesized attachment) untouched", async () => {
216
+ mockMcpClient.toolMap = { open: [tool("a"), tool("b")] };
217
+
218
+ const result = await connectMcpServers([
219
+ makeServer({ slug: "open", command: "npx" }),
220
+ ]);
221
+
222
+ expect(result.tools.map((t) => t.name)).toEqual(["a", "b"]);
223
+ });
224
+
225
+ it("restricts per server: one restricted server never narrows its unrestricted sibling", async () => {
226
+ mockMcpClient.toolMap = {
227
+ restricted: [tool("get"), tool("apply")],
228
+ open: [tool("get"), tool("apply")],
229
+ };
230
+
231
+ const result = await connectMcpServers([
232
+ makeServer({ slug: "restricted", command: "npx", enabledTools: ["get"] }),
233
+ makeServer({ slug: "open", command: "npx" }),
234
+ ]);
235
+
236
+ expect(result.serverToolMap.restricted.map((t) => t.name)).toEqual(["get"]);
237
+ expect(result.serverToolMap.open.map((t) => t.name)).toEqual(["get", "apply"]);
238
+ });
239
+
240
+ it("warns and intersects an allow-list naming a tool the server does not expose", async () => {
241
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
242
+ mockMcpClient.toolMap = { planton: [tool("get")] };
243
+
244
+ const result = await connectMcpServers([
245
+ makeServer({ slug: "planton", command: "npx", enabledTools: ["get", "ghost"] }),
246
+ ]);
247
+
248
+ expect(result.tools.map((t) => t.name)).toEqual(["get"]);
249
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("ghost"));
250
+ });
251
+ });
@@ -1,11 +1,17 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { resolveMcpServers } from "../mcp-resolver.js";
2
+ import { mcpServerToResolved, mergeMcpServerUsages, resolveMcpServers } from "../mcp-resolver.js";
3
3
  import { McpTransportError } from "../mcp-transport-guard.js";
4
4
 
5
- function makeUsage(slug: string, org = "test-org") {
5
+ function makeUsage(
6
+ slug: string,
7
+ org = "test-org",
8
+ enabledTools: string[] = [],
9
+ toolApprovalOverrides: Array<{ toolName: string; requiresApproval: boolean }> = [],
10
+ ) {
6
11
  return {
7
12
  mcpServerRef: { slug, org, kind: 0 },
8
- toolApprovalOverrides: [],
13
+ enabledTools,
14
+ toolApprovalOverrides,
9
15
  } as any;
10
16
  }
11
17
 
@@ -106,3 +112,140 @@ describe("resolveMcpServers — transport guard integration", () => {
106
112
  expect(result.resolvedServers).toHaveLength(0);
107
113
  });
108
114
  });
115
+
116
+ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
117
+ function withDefaults(server: any, defaultEnabledTools: string[]) {
118
+ server.spec.defaultEnabledTools = defaultEnabledTools;
119
+ return server;
120
+ }
121
+
122
+ it("carries the usage's enabled_tools as the effective allow-list", async () => {
123
+ const client = clientReturning({ github: httpMcpServer("github") });
124
+
125
+ const result = await resolveMcpServers(
126
+ client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed",
127
+ );
128
+
129
+ expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
130
+ });
131
+
132
+ it("falls back to the server's default_enabled_tools for an empty usage list", async () => {
133
+ const client = clientReturning({
134
+ github: withDefaults(httpMcpServer("github"), ["search_code"]),
135
+ });
136
+
137
+ const result = await resolveMcpServers(
138
+ client, [makeUsage("github")], {}, "stdio-allowed",
139
+ );
140
+
141
+ expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
142
+ });
143
+
144
+ it("resolves unrestricted (absent field) when both lists are empty", async () => {
145
+ const client = clientReturning({ github: httpMcpServer("github") });
146
+
147
+ const result = await resolveMcpServers(
148
+ client, [makeUsage("github")], {}, "stdio-allowed",
149
+ );
150
+
151
+ expect(result.resolvedServers[0].enabledTools).toBeUndefined();
152
+ });
153
+
154
+ it("mcpServerToResolved without a usage keeps only the default fallback (the discovery path)", () => {
155
+ const server = withDefaults(httpMcpServer("github"), ["search_code"]);
156
+
157
+ const resolved = mcpServerToResolved(server, "github", {});
158
+
159
+ // No usage in hand (discovery) — a per-agent restriction cannot apply,
160
+ // only the server-declared default does.
161
+ expect(resolved?.enabledTools).toEqual(["search_code"]);
162
+ });
163
+ });
164
+
165
+ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)", () => {
166
+ it("carries the usage's overrides on its own resolved server only", async () => {
167
+ // Riding the server is the scoping mechanism: an override can no longer
168
+ // reach a same-named tool on another server, because it never exists
169
+ // anywhere but its own server's object.
170
+ const client = clientReturning({
171
+ github: httpMcpServer("github"),
172
+ slack: httpMcpServer("slack"),
173
+ });
174
+
175
+ const result = await resolveMcpServers(
176
+ client,
177
+ [
178
+ makeUsage("github", "test-org", [], [{ toolName: "delete_item", requiresApproval: false }]),
179
+ makeUsage("slack"),
180
+ ],
181
+ {},
182
+ "stdio-allowed",
183
+ );
184
+
185
+ const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
186
+ expect(bySlug.get("github")!.toolApprovalOverrides).toEqual([
187
+ { toolName: "delete_item", requiresApproval: false },
188
+ ]);
189
+ expect(bySlug.get("slack")!.toolApprovalOverrides).toEqual([]);
190
+ });
191
+
192
+ it("mcpServerToResolved without a usage carries no overrides (the discovery path)", () => {
193
+ const resolved = mcpServerToResolved(httpMcpServer("github"), "github", {});
194
+
195
+ // No usage in hand (discovery) — there is no agent context, so no
196
+ // layer-3 overrides can exist.
197
+ expect(resolved?.toolApprovalOverrides).toEqual([]);
198
+ });
199
+
200
+ it("session-wins merge feeds the SESSION usage's overrides to the resolver (native-harness parity)", async () => {
201
+ // Pins the end-to-end path that was broken before #349: the deep-agent
202
+ // harness resolved servers from the merged usages but flattened
203
+ // overrides from the AGENT usages only, so a session's overrides were
204
+ // silently ignored there (and honored by the Cursor harness).
205
+ const client = clientReturning({ github: httpMcpServer("github") });
206
+ const merged = mergeMcpServerUsages(
207
+ [makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: true }])],
208
+ [makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: false }])],
209
+ );
210
+
211
+ const result = await resolveMcpServers(client, merged, {}, "stdio-allowed");
212
+
213
+ expect(result.resolvedServers[0].toolApprovalOverrides).toEqual([
214
+ { toolName: "push", requiresApproval: false },
215
+ ]);
216
+ });
217
+ });
218
+
219
+ describe("mergeMcpServerUsages — session-wins-per-slug (shared by both harnesses)", () => {
220
+ it("session usage replaces the agent's WHOLE usage for the same slug, including enabled_tools", () => {
221
+ const merged = mergeMcpServerUsages(
222
+ [makeUsage("github", "test-org", ["create_pr"])],
223
+ [makeUsage("github", "test-org", ["search_code"])],
224
+ );
225
+
226
+ expect(merged).toHaveLength(1);
227
+ expect(merged[0].enabledTools).toEqual(["search_code"]);
228
+ });
229
+
230
+ it("a session usage with an empty list overrides to the server-default/unrestricted shape", () => {
231
+ // Session-wins is whole-usage precedence, not a list union: an empty
232
+ // session list means "back to the server's defaults", exactly as if the
233
+ // agent-level usage did not exist.
234
+ const merged = mergeMcpServerUsages(
235
+ [makeUsage("github", "test-org", ["create_pr"])],
236
+ [makeUsage("github")],
237
+ );
238
+
239
+ expect(merged).toHaveLength(1);
240
+ expect(merged[0].enabledTools).toEqual([]);
241
+ });
242
+
243
+ it("unions distinct slugs and skips usages without one", () => {
244
+ const merged = mergeMcpServerUsages(
245
+ [makeUsage("github"), { toolApprovalOverrides: [] } as any],
246
+ [makeUsage("planton")],
247
+ );
248
+
249
+ expect(merged.map((u) => u.mcpServerRef?.slug)).toEqual(["github", "planton"]);
250
+ });
251
+ });
@@ -53,6 +53,9 @@ describe("buildChatModel", () => {
53
53
  vi.clearAllMocks();
54
54
  delete process.env.ANTHROPIC_API_KEY;
55
55
  delete process.env.OPENAI_API_KEY;
56
+ delete process.env.STIGMER_ANTHROPIC_BACKEND;
57
+ delete process.env.STIGMER_OPENAI_BACKEND;
58
+ delete process.env.CLOUD_ML_REGION;
56
59
  });
57
60
 
58
61
  afterEach(() => {
@@ -159,4 +162,155 @@ describe("buildChatModel", () => {
159
162
  expect(provider).toBe("anthropic");
160
163
  expect(apiModelId).toBe("claude-haiku-4.5");
161
164
  });
165
+
166
+ // ─── Request timeout (STIGMER_LLM_REQUEST_TIMEOUT_MS) ──────────────────────
167
+ //
168
+ // The timeout lives in a different slot per wrapper: ChatOpenAI reads it
169
+ // as a constructor field; ChatAnthropic only honors clientOptions.timeout.
170
+ // Putting it in the shared constructor spread was the regression that made
171
+ // the env var inert on every Anthropic path (T02 finding 2, repaired in
172
+ // T06). The backend factories' half of the chain is pinned in the
173
+ // vertex/bedrock/foundry adapter tests.
174
+
175
+ describe("request timeout", () => {
176
+ it("places the OpenAI timeout at the constructor level with maxRetries 0", async () => {
177
+ mockRegistryResponse([
178
+ { id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
179
+ ]);
180
+
181
+ await buildChatModel({ modelName: "gpt-4.1", timeoutMs: 5000 });
182
+
183
+ const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
184
+ expect(args).toMatchObject({ timeout: 5000, maxRetries: 0 });
185
+ });
186
+
187
+ it("places the Anthropic timeout in clientOptions, never the inert constructor slot", async () => {
188
+ mockRegistryResponse([
189
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
190
+ ]);
191
+
192
+ await buildChatModel({ modelName: "claude-haiku-4.5", timeoutMs: 5000 });
193
+
194
+ const args = lastAnthropicArgs();
195
+ expect(args).toMatchObject({ clientOptions: { timeout: 5000 }, maxRetries: 0 });
196
+ expect(args).not.toHaveProperty("timeout");
197
+ });
198
+
199
+ it("merges the timeout with the proxy wiring inside one clientOptions block", async () => {
200
+ mockRegistryResponse([
201
+ { id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
202
+ ]);
203
+
204
+ await buildChatModel({
205
+ modelName: "claude-haiku-4.5",
206
+ proxyEndpoint: "https://api.stigmer.ai",
207
+ stigmerToken: "tok-123",
208
+ timeoutMs: 5000,
209
+ });
210
+
211
+ expect(lastAnthropicArgs()).toMatchObject({
212
+ clientOptions: {
213
+ timeout: 5000,
214
+ baseURL: "https://api.stigmer.ai/v1/proxy/llm/anthropic",
215
+ },
216
+ });
217
+ });
218
+ });
219
+
220
+ // ─── Provider backends (STIGMER_ANTHROPIC_BACKEND) ─────────────────────────
221
+
222
+ describe("vertex backend", () => {
223
+ beforeEach(() => {
224
+ process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
225
+ process.env.CLOUD_ML_REGION = "asia-south1";
226
+ });
227
+
228
+ it("holds the canonical-id invariant: translated id on the wire, canonical id returned", async () => {
229
+ mockRegistryResponse([
230
+ { id: "claude-sonnet-4.5", apiModelId: "claude-sonnet-4-5-20250929", provider: "anthropic" },
231
+ ]);
232
+
233
+ const { apiModelId } = await buildChatModel({ modelName: "claude-sonnet-4.5" });
234
+
235
+ // Pricing and usage metrics key on the canonical id; only the
236
+ // constructor (the wire) sees the Vertex `@date` form.
237
+ expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
238
+ const args = lastAnthropicArgs();
239
+ expect(args.model).toBe("claude-sonnet-4-5@20250929");
240
+ expect(typeof args.createClient).toBe("function");
241
+ });
242
+
243
+ it("passes dateless 4.6-generation ids to the wire untranslated", async () => {
244
+ mockRegistryResponse([
245
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
246
+ ]);
247
+
248
+ await buildChatModel({ modelName: "claude-sonnet-4.6" });
249
+
250
+ expect(lastAnthropicArgs().model).toBe("claude-sonnet-4-6");
251
+ });
252
+
253
+ it("constructs without ANTHROPIC_API_KEY (auth is Google's, not Anthropic's)", async () => {
254
+ mockRegistryResponse([
255
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
256
+ ]);
257
+
258
+ await buildChatModel({ modelName: "claude-sonnet-4.6" });
259
+
260
+ expect(lastAnthropicArgs().apiKey).toBe("");
261
+ });
262
+
263
+ it("yields to the proxy: a proxied call never consults the backend var", async () => {
264
+ mockRegistryResponse([
265
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
266
+ ]);
267
+
268
+ await buildChatModel({
269
+ modelName: "claude-sonnet-4.6",
270
+ proxyEndpoint: "https://api.stigmer.ai",
271
+ stigmerToken: "tok",
272
+ });
273
+
274
+ const args = lastAnthropicArgs();
275
+ expect(args).not.toHaveProperty("createClient");
276
+ expect(args.model).toBe("claude-sonnet-4-6");
277
+ expect(args).toMatchObject({
278
+ clientOptions: { baseURL: "https://api.stigmer.ai/v1/proxy/llm/anthropic" },
279
+ });
280
+ });
281
+
282
+ it("leaves the OpenAI path untouched by the Anthropic backend var", async () => {
283
+ mockRegistryResponse([
284
+ { id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
285
+ ]);
286
+
287
+ await buildChatModel({ modelName: "gpt-4.1" });
288
+
289
+ const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
290
+ expect(args).not.toHaveProperty("createClient");
291
+ expect(mockAnthropicCtor).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it("fails at dispatch with the region message when CLOUD_ML_REGION is missing", async () => {
295
+ // Defense in depth for paths that bypass the factories' preflight.
296
+ delete process.env.CLOUD_ML_REGION;
297
+ mockRegistryResponse([
298
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
299
+ ]);
300
+
301
+ await expect(buildChatModel({ modelName: "claude-sonnet-4.6" }))
302
+ .rejects.toThrow(/CLOUD_ML_REGION/);
303
+ expect(mockAnthropicCtor).not.toHaveBeenCalled();
304
+ });
305
+ });
306
+
307
+ it("throws the parser's message on an invalid backend value (never a silent public fallback)", async () => {
308
+ process.env.STIGMER_ANTHROPIC_BACKEND = "verteks";
309
+ mockRegistryResponse([
310
+ { id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
311
+ ]);
312
+
313
+ await expect(buildChatModel({ modelName: "claude-sonnet-4.6" }))
314
+ .rejects.toThrow(/STIGMER_ANTHROPIC_BACKEND="verteks" is not a supported backend/);
315
+ });
162
316
  });