@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
@@ -4,21 +4,37 @@
4
4
  * Wraps @langchain/mcp-adapters MultiServerMCPClient with:
5
5
  * - Conversion from ResolvedMcpServer[] to the SDK's Connection config
6
6
  * - Proper async cleanup via close()
7
- * - Tool filtering based on discovered capabilities
7
+ * - Tool filtering by each server's effective enabled_tools allow-list
8
+ * (issue #350; semantics in shared/mcp-enabled-tools.ts) — the model only
9
+ * ever sees the tools the agent's manifest enables
8
10
  *
9
11
  * Transport policy (stdio is local-runner-only) is enforced upstream at
10
12
  * resolution time by shared/mcp-transport-guard.ts — servers reaching
11
13
  * this manager have already passed it.
12
14
  *
15
+ * Stdio env contract (oss#256): a subprocess receives exactly the
16
+ * variables declared in the server's spec.env (resolved upstream by
17
+ * filterEnvToDeclaredKeys) plus the MCP SDK's minimal base environment
18
+ * (HOME, LOGNAME, PATH, SHELL, TERM, USER — getDefaultEnvironment in
19
+ * @modelcontextprotocol/sdk, merged under whatever we pass). The
20
+ * runner's own process env is never passed: it carries runner-internal
21
+ * credentials (STIGMER_TOKEN, STIGMER_RUNNER_HITL_SECRET,
22
+ * CURSOR_API_KEY, LLM provider keys) that no third-party MCP subprocess
23
+ * may see. A declared-empty env and an undeclared env are deliberately
24
+ * equivalent — both yield the SDK base environment.
25
+ *
13
26
  * The Cursor execution path does NOT use this manager — it passes MCP
14
27
  * configs directly to the Cursor SDK via toCursorMcpConfig(). This
15
- * manager is exclusively for LangGraph-based deep agent executions.
28
+ * manager serves LangGraph-based deep agent executions, and discovery
29
+ * (activities/discover-mcp-server.ts) builds its spawn config through
30
+ * toMcpClientConfig too.
16
31
  */
17
32
 
18
33
  import { MultiServerMCPClient } from "@langchain/mcp-adapters";
19
34
  import type { Connection } from "@langchain/mcp-adapters";
20
35
  import type { DynamicStructuredTool } from "@langchain/core/tools";
21
36
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
37
+ import { filterToolsByEnabledTools } from "./mcp-enabled-tools.js";
22
38
 
23
39
  /**
24
40
  * Convert harness-agnostic ResolvedMcpServer[] into the
@@ -32,11 +48,18 @@ export function toMcpClientConfig(
32
48
  for (const server of servers) {
33
49
  if (server.connectionType === "stdio") {
34
50
  if (!server.command) continue;
51
+ if (!server.env || Object.keys(server.env).length === 0) {
52
+ console.log(
53
+ `[MCP] Server '${server.slug}' declares no env — its subprocess ` +
54
+ `starts with the minimal base environment only. Declare variables ` +
55
+ `in the McpServer's spec.env to pass them.`,
56
+ );
57
+ }
35
58
  config[server.slug] = {
36
59
  transport: "stdio",
37
60
  command: server.command,
38
61
  args: server.args ?? [],
39
- env: server.env ?? processEnvAsStrings(),
62
+ env: server.env,
40
63
  cwd: server.cwd,
41
64
  };
42
65
  } else if (server.connectionType === "http" || server.connectionType === "sse") {
@@ -75,11 +98,25 @@ export async function connectMcpServers(
75
98
  }
76
99
 
77
100
  const client = new MultiServerMCPClient(connectionConfig);
78
- const serverToolMap = await client.initializeConnections();
101
+ const discoveredToolMap = await client.initializeConnections();
79
102
 
103
+ // Enforce each server's effective enabled_tools allow-list (issue #350)
104
+ // HERE, before anything downstream sees the tools: the parent tool list,
105
+ // the approval gate's toolServerMap, and the sub-agent McpAccess filter
106
+ // (whose subset-of-parent check is only real against a narrowed map) all
107
+ // derive from this result. Servers without a restriction (enabledTools
108
+ // absent — including synthesized attachments) pass through untouched.
109
+ const enabledBySlug = new Map(
110
+ servers.map((s) => [s.slug, s.enabledTools]),
111
+ );
112
+ const serverToolMap: Record<string, DynamicStructuredTool[]> = {};
80
113
  const tools: DynamicStructuredTool[] = [];
81
- for (const serverTools of Object.values(serverToolMap)) {
82
- tools.push(...serverTools);
114
+ for (const [slug, discovered] of Object.entries(discoveredToolMap)) {
115
+ const filtered = filterToolsByEnabledTools(
116
+ slug, discovered, enabledBySlug.get(slug),
117
+ );
118
+ serverToolMap[slug] = filtered;
119
+ tools.push(...filtered);
83
120
  }
84
121
 
85
122
  console.log(
@@ -91,23 +128,3 @@ export async function connectMcpServers(
91
128
 
92
129
  return { client, tools, serverToolMap };
93
130
  }
94
-
95
- /**
96
- * Snapshot of process.env as a string-only record (no undefined values).
97
- * Used as a fallback when an MCP server has no declared env — the
98
- * subprocess inherits the runner's full environment, matching standard
99
- * Unix child-process behavior.
100
- *
101
- * Without this, @modelcontextprotocol/sdk only passes a restricted
102
- * whitelist (HOME, PATH, USER, etc.) which drops platform variables
103
- * like STIGMER_SERVER_ADDRESS.
104
- */
105
- function processEnvAsStrings(): Record<string, string> {
106
- const env: Record<string, string> = {};
107
- for (const [key, value] of Object.entries(process.env)) {
108
- if (value !== undefined) {
109
- env[key] = value;
110
- }
111
- }
112
- return env;
113
- }
@@ -11,7 +11,7 @@
11
11
  * there until the two are consolidated.
12
12
  */
13
13
 
14
- import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
14
+ import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
15
15
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
16
16
  import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
17
17
  import type { StigmerClient } from "../client/stigmer-client.js";
@@ -26,6 +26,7 @@ import {
26
26
  filterEnvToDeclaredKeys,
27
27
  PlaceholderResolutionError,
28
28
  } from "./placeholder-resolver.js";
29
+ import { effectiveEnabledTools } from "./mcp-enabled-tools.js";
29
30
 
30
31
  /**
31
32
  * Harness-agnostic intermediate representation of a resolved MCP server.
@@ -45,6 +46,28 @@ export interface ResolvedMcpServer {
45
46
  toolApprovals: ToolApprovalPolicy[];
46
47
  pinnedToolApprovals: ToolApprovalPolicy[];
47
48
  discoveredCapabilitiesEmpty: boolean;
49
+ /**
50
+ * The EFFECTIVE tool allow-list for this server (issue #350): the usage's
51
+ * enabled_tools, falling back to the server's default_enabled_tools when
52
+ * the usage's list is empty (see shared/mcp-enabled-tools.ts). Absent when
53
+ * unrestricted — which keeps synthesized attachment servers (built without
54
+ * a usage) and the discovery path unfiltered by construction. Enforced at
55
+ * connect time by connectMcpServers (shared/mcp-manager.ts).
56
+ */
57
+ enabledTools?: string[];
58
+ /**
59
+ * Layer-3 per-agent approval overrides from the usage that resolved this
60
+ * server (issue #349): riding the resolved server is what SCOPES an
61
+ * override to its own server — a flat cross-server list is how an
62
+ * override once leaked onto (or silently un-gated) a same-named tool on
63
+ * another server. Deliberately REQUIRED, not optional: empty means "no
64
+ * overrides", and every construction site — including the near-duplicate
65
+ * Cursor resolver and the synthesized attachments, which have no usage
66
+ * and therefore no layer 3 — must say so explicitly, so a forgotten
67
+ * mirror cannot compile. Consumed by mergeApprovalPolicies
68
+ * (shared/approval-policy.ts).
69
+ */
70
+ toolApprovalOverrides: ToolApprovalOverride[];
48
71
  }
49
72
 
50
73
  export interface McpResolutionResult {
@@ -78,7 +101,13 @@ export async function resolveMcpServers(
78
101
  envVars,
79
102
  ref.slug,
80
103
  );
81
- const server = mcpServerToResolved(mcpServer, ref.slug, serverEnv);
104
+ const server = mcpServerToResolved(
105
+ mcpServer,
106
+ ref.slug,
107
+ serverEnv,
108
+ usage.enabledTools,
109
+ usage.toolApprovalOverrides ?? [],
110
+ );
82
111
  if (server) {
83
112
  assertTransportAllowed(server.slug, server.connectionType, transportPosture);
84
113
  resolved.push(server);
@@ -108,6 +137,8 @@ export function mcpServerToResolved(
108
137
  server: McpServer,
109
138
  slug: string,
110
139
  envVars: Record<string, string>,
140
+ usageEnabledTools?: readonly string[],
141
+ usageToolApprovalOverrides: ToolApprovalOverride[] = [],
111
142
  ): ResolvedMcpServer | null {
112
143
  const spec = server.spec;
113
144
  if (!spec) return null;
@@ -123,6 +154,13 @@ export function mcpServerToResolved(
123
154
  toolApprovals,
124
155
  pinnedToolApprovals,
125
156
  discoveredCapabilitiesEmpty,
157
+ // Callers without a usage (the discovery activity) pass nothing, so the
158
+ // default_enabled_tools fallback still applies but a per-agent
159
+ // restriction cannot: discovery must see the server's full toolset.
160
+ enabledTools: effectiveEnabledTools(usageEnabledTools, spec.defaultEnabledTools),
161
+ // Same no-usage rule for layer 3: discovery has no agent context, so no
162
+ // per-agent overrides exist — defaulting to [] keeps that structural.
163
+ toolApprovalOverrides: usageToolApprovalOverrides,
126
164
  };
127
165
 
128
166
  switch (spec.serverType.case) {
@@ -170,4 +208,37 @@ export function extractMcpServerSlugs(usages: McpServerUsage[]): string[] {
170
208
  .filter((s): s is string => !!s);
171
209
  }
172
210
 
211
+ /**
212
+ * Merge MCP server usages from agent (base) and session (overlay).
213
+ *
214
+ * Replicates session_context_merge.py::merge_mcp_server_usages():
215
+ * - Agent-level usages are the base set
216
+ * - Session-level usages extend or override by mcp_server_ref.slug
217
+ * - If both reference the same slug, session-level takes precedence —
218
+ * the WHOLE usage, including enabled_tools and approval overrides
219
+ *
220
+ * Shared by both harnesses (blueprint-resolver.ts for Cursor,
221
+ * execute-deep-agent/setup.ts for the native harness) so a duplicate slug
222
+ * resolves identically everywhere: exactly one usage per server, whose
223
+ * enabled_tools is the one the enforcement filter honors.
224
+ */
225
+ export function mergeMcpServerUsages(
226
+ agentUsages: McpServerUsage[],
227
+ sessionUsages: McpServerUsage[],
228
+ ): McpServerUsage[] {
229
+ const bySlug = new Map<string, McpServerUsage>();
230
+
231
+ for (const usage of agentUsages) {
232
+ const slug = usage.mcpServerRef?.slug;
233
+ if (slug) bySlug.set(slug, usage);
234
+ }
235
+
236
+ for (const usage of sessionUsages) {
237
+ const slug = usage.mcpServerRef?.slug;
238
+ if (slug) bySlug.set(slug, usage);
239
+ }
240
+
241
+ return [...bySlug.values()];
242
+ }
243
+
173
244
  export { PlaceholderResolutionError } from "./placeholder-resolver.js";
@@ -6,10 +6,11 @@
6
6
  * (e.g. "claude-haiku-4-5-20251001") before a client is built — so no
7
7
  * activity can bypass the translation and 404 the provider.
8
8
  *
9
- * Layering: `llm-proxy.ts` stays pure routing utilities (no LangChain
10
- * dependency); this module is the LangChain-aware layer above it that owns
11
- * the resolve -> infer-provider -> strip-prefix -> proxy-wire -> construct
12
- * sequence that used to be copy-pasted across every LLM activity.
9
+ * Layering: `llm-proxy.ts` and `llm-backend.ts` stay pure routing utilities
10
+ * (no LangChain dependency); this module is the LangChain-aware layer above
11
+ * them that owns the resolve -> infer-provider -> strip-prefix ->
12
+ * proxy-or-backend-wire -> construct sequence that used to be copy-pasted
13
+ * across every LLM activity.
13
14
  */
14
15
 
15
16
  import { ChatAnthropic } from "@langchain/anthropic";
@@ -23,6 +24,15 @@ import {
23
24
  buildProxyHeaders,
24
25
  type LlmProvider,
25
26
  } from "./llm-proxy.js";
27
+ import {
28
+ resolveAnthropicBackend,
29
+ checkVertexPrerequisites,
30
+ checkBedrockPrerequisites,
31
+ checkFoundryPrerequisites,
32
+ toVertexModelId,
33
+ toBedrockModelId,
34
+ toFoundryDeploymentName,
35
+ } from "./llm-backend.js";
26
36
  import { resolveToApiModelId } from "./model-registry.js";
27
37
 
28
38
  export interface BuildChatModelOptions {
@@ -52,7 +62,13 @@ export interface BuildChatModelOptions {
52
62
  export interface BuiltChatModel {
53
63
  readonly model: BaseChatModel;
54
64
  readonly provider: LlmProvider;
55
- /** The resolved id actually handed to the provider. */
65
+ /**
66
+ * The canonical provider API id the registry resolved to — what pricing
67
+ * and usage metrics key on. A backend adapter may translate it for the
68
+ * wire (Vertex separates the snapshot date with `@`), but the translated
69
+ * form never leaves the adapter: this field stays canonical (the
70
+ * canonical-id invariant in design decision 001-provider-backends).
71
+ */
56
72
  readonly apiModelId: string;
57
73
  }
58
74
 
@@ -67,6 +83,105 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
67
83
  const provider = inferProvider(resolved);
68
84
  const apiModelId = stripProviderPrefix(resolved);
69
85
 
86
+ // Backend precedence by construction: a proxied call never consults the
87
+ // backend var — the proxy owns provider routing (the factories' preflight
88
+ // warns when both are configured). Selection is deployment-static and read
89
+ // here the same way provider API keys are, not plumbed through Config.
90
+ const anthropicBackend =
91
+ provider === "anthropic" && !opts.proxyEndpoint
92
+ ? resolveAnthropicBackend()
93
+ : "public";
94
+
95
+ // Backend SDKs (and their auth subtrees: google-auth-library, AWS smithy)
96
+ // load lazily so deployments that never configure a backend never
97
+ // evaluate them — cheap cold starts stay cheap, and bundle-slim's CJS
98
+ // output preserves exactly this deferred evaluation (see
99
+ // scripts/bundle-slim.mjs). The factory is invoked lazily by
100
+ // ChatAnthropic once per cached client (batch + streaming), so the class
101
+ // is captured here, in async context.
102
+ //
103
+ // Each factory MUST honor options.maxRetries and options.timeout:
104
+ // LangChain owns retrying and hands the factory maxRetries: 0 — a factory
105
+ // that drops it nests the SDK's default 2 retries inside LangChain's
106
+ // retry loop, multiplying every transient failure — and `timeout` arrives
107
+ // through the same options object (ChatAnthropic spreads clientOptions
108
+ // into it), so a factory that drops it silently unbounds
109
+ // STIGMER_LLM_REQUEST_TIMEOUT_MS on that backend. Pinned by the seam
110
+ // tests. Prerequisites are re-checked here (not only in
111
+ // the factories' preflight) so paths that construct models without a
112
+ // runner factory still fail at dispatch with the catalog message instead
113
+ // of mid-request. Credentials are read natively by each SDK from its
114
+ // standard conventions (GCP: CLOUD_ML_REGION + ADC; AWS: AWS_REGION +
115
+ // the credential chain / AWS_BEARER_TOKEN_BEDROCK; Foundry:
116
+ // ANTHROPIC_FOUNDRY_API_KEY, or the Azure credential chain when no key
117
+ // is set).
118
+ let backendCreateClient:
119
+ | ((options: { maxRetries?: number; timeout?: number }) => unknown)
120
+ | undefined;
121
+ let wireModelId = apiModelId;
122
+ let maxTokens = opts.maxTokens;
123
+ if (anthropicBackend === "vertex") {
124
+ const prereq = checkVertexPrerequisites();
125
+ if (prereq !== null) throw new Error(prereq);
126
+ const { AnthropicVertex } = await import("@anthropic-ai/vertex-sdk");
127
+ backendCreateClient = (options) =>
128
+ new AnthropicVertex({ maxRetries: options.maxRetries, timeout: options.timeout });
129
+ wireModelId = toVertexModelId(apiModelId);
130
+ } else if (anthropicBackend === "bedrock") {
131
+ const prereq = checkBedrockPrerequisites();
132
+ if (prereq !== null) throw new Error(prereq);
133
+ const { AnthropicBedrock } = await import("@anthropic-ai/bedrock-sdk");
134
+ backendCreateClient = (options) =>
135
+ new AnthropicBedrock({ maxRetries: options.maxRetries, timeout: options.timeout });
136
+ wireModelId = toBedrockModelId(apiModelId);
137
+ if (maxTokens === undefined) {
138
+ // LangChain's per-model maxTokens table prefix-matches the model
139
+ // name. Vertex's translated ids still match their canonical prefix;
140
+ // Bedrock's `anthropic.…` ids match nothing and silently fall back
141
+ // to 4096 — a silent output cap. Probe the CANONICAL id with a
142
+ // throwaway construction (pure field assignment, no I/O; the key is
143
+ // never used) so bedrock inherits exactly the default the public API
144
+ // and vertex get for the same model — including models the table
145
+ // doesn't know yet, where all backends agree on the fallback.
146
+ // Pinned by the canonical-parity test in bedrock-adapter.test.ts.
147
+ maxTokens = new ChatAnthropic({ model: apiModelId, apiKey: "max-tokens-probe" }).maxTokens;
148
+ }
149
+ } else if (anthropicBackend === "foundry") {
150
+ const prereq = checkFoundryPrerequisites();
151
+ if (prereq !== null) throw new Error(prereq);
152
+ const { AnthropicFoundry } = await import("@anthropic-ai/foundry-sdk");
153
+ // Auth is an explicit either/or (the Foundry constructor throws when
154
+ // given both a key and a token provider): ANTHROPIC_FOUNDRY_API_KEY if
155
+ // present, otherwise keyless Microsoft Entra ID through the Azure
156
+ // credential chain (env service principal -> workload identity ->
157
+ // managed identity -> az CLI) — the Azure analogue of Vertex ADC and
158
+ // the AWS chain, resolved at request time. The provider is built once
159
+ // here (getBearerTokenProvider caches tokens until near-expiry) and
160
+ // shared by both cached clients; the SDK still consults it per request,
161
+ // so refreshed tokens flow without reconstruction (pinned by
162
+ // foundry-seam.test.ts). Endpoint (resource or base URL) and the key
163
+ // are read natively by the SDK from its own env vars.
164
+ let azureADTokenProvider: (() => Promise<string>) | undefined;
165
+ if (!process.env.ANTHROPIC_FOUNDRY_API_KEY?.trim()) {
166
+ const { DefaultAzureCredential, getBearerTokenProvider } = await import("@azure/identity");
167
+ azureADTokenProvider = getBearerTokenProvider(
168
+ new DefaultAzureCredential(),
169
+ "https://ai.azure.com/.default",
170
+ );
171
+ }
172
+ backendCreateClient = (options) =>
173
+ new AnthropicFoundry({
174
+ maxRetries: options.maxRetries,
175
+ timeout: options.timeout,
176
+ ...(azureADTokenProvider ? { azureADTokenProvider } : {}),
177
+ });
178
+ // Unlike the vertex/bedrock ids, the deployment name needs no maxTokens
179
+ // handling: stripping the snapshot date preserves LangChain's per-model
180
+ // default (probed 2026-08-11 across the full catalog; pinned by the
181
+ // parity test in foundry-adapter.test.ts).
182
+ wireModelId = toFoundryDeploymentName(apiModelId);
183
+ }
184
+
70
185
  const baseUrl = opts.proxyEndpoint
71
186
  ? resolveProxyBaseUrl(opts.proxyEndpoint, provider)
72
187
  : undefined;
@@ -85,9 +200,29 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
85
200
  const common = {
86
201
  temperature: opts.temperature ?? 0,
87
202
  apiKey,
88
- ...(opts.maxTokens ? { maxTokens: opts.maxTokens } : {}),
89
- ...(opts.timeoutMs ? { maxRetries: opts.maxRetries ?? 0, timeout: opts.timeoutMs } : {}),
203
+ ...(maxTokens ? { maxTokens } : {}),
204
+ ...(opts.timeoutMs ? { maxRetries: opts.maxRetries ?? 0 } : {}),
205
+ };
206
+
207
+ // The request timeout lives in a different slot per wrapper: ChatOpenAI
208
+ // reads `timeout` as a constructor field, but ChatAnthropic ignores it
209
+ // there — its slot is `clientOptions.timeout`, which the wrapper spreads
210
+ // into the createClient factory options and (on the default factory) the
211
+ // SDK constructor. This split is what makes STIGMER_LLM_REQUEST_TIMEOUT_MS
212
+ // bound every path; putting `timeout` in the shared constructor spread is
213
+ // the exact regression that made it inert for Anthropic (T02 finding 2).
214
+ // The `maxRetries` half above stays constructor-level for both wrappers:
215
+ // that is LangChain's own retry knob, distinct from the SDK-level
216
+ // maxRetries the factories receive.
217
+ const anthropicClientOptions = {
218
+ ...(opts.timeoutMs ? { timeout: opts.timeoutMs } : {}),
219
+ ...(baseUrl ? { baseURL: baseUrl } : {}),
220
+ ...(headers ? { defaultHeaders: headers } : {}),
90
221
  };
222
+ const anthropicClientOptionsField =
223
+ Object.keys(anthropicClientOptions).length > 0
224
+ ? { clientOptions: anthropicClientOptions }
225
+ : {};
91
226
 
92
227
  // The two SDKs name the transport-override block differently (OpenAI:
93
228
  // `configuration`, Anthropic: `clientOptions`) — encapsulating that here is
@@ -96,6 +231,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
96
231
  ? new ChatOpenAI({
97
232
  model: apiModelId,
98
233
  ...common,
234
+ ...(opts.timeoutMs ? { timeout: opts.timeoutMs } : {}),
99
235
  ...(baseUrl || headers
100
236
  ? {
101
237
  configuration: {
@@ -105,18 +241,24 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
105
241
  }
106
242
  : {}),
107
243
  })
108
- : new ChatAnthropic({
109
- model: apiModelId,
110
- ...common,
111
- ...(baseUrl || headers
112
- ? {
113
- clientOptions: {
114
- ...(baseUrl ? { baseURL: baseUrl } : {}),
115
- ...(headers ? { defaultHeaders: headers } : {}),
116
- },
117
- }
118
- : {}),
119
- });
244
+ : backendCreateClient
245
+ ? // Backend adapter: the translated id is wire detail only — each
246
+ // client moves it from the JSON body into its URL path. The waiver
247
+ // in ChatAnthropic (an API key is not required when createClient is
248
+ // provided) is what lets this construct with no ANTHROPIC_API_KEY.
249
+ // (Backend mode never has a proxy — see the precedence rule above —
250
+ // so the clientOptions here carry at most the timeout.)
251
+ new ChatAnthropic({
252
+ model: wireModelId,
253
+ ...common,
254
+ ...anthropicClientOptionsField,
255
+ createClient: backendCreateClient,
256
+ })
257
+ : new ChatAnthropic({
258
+ model: apiModelId,
259
+ ...common,
260
+ ...anthropicClientOptionsField,
261
+ });
120
262
 
121
263
  return { model, provider, apiModelId };
122
264
  }