@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
@@ -13,7 +13,10 @@ import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js"
13
13
  import { needsBackfill } from "../connect-backfill.js";
14
14
  import {
15
15
  DATASTORE_ATTACHMENT_SLUG,
16
+ EXPECTED_RECORD_TOOLS,
17
+ formatDatastoreDegradationNotice,
16
18
  formatDatastoresSection,
19
+ missingRecordTools,
17
20
  synthesizeDatastoreAttachment,
18
21
  } from "../datastore-attachment.js";
19
22
  import { injectSynthesizedAttachment } from "../synthesized-attachment.js";
@@ -94,10 +97,38 @@ describe("synthesizeDatastoreAttachment", () => {
94
97
 
95
98
  // Absence from the map means auto-approved for EVERY consumer (the
96
99
  // Cursor hook, the deep-agent gate) — DD-001 SD-3's structural bypass.
97
- const merged = mergeApprovalPolicies([attachment], [], NO_LEASES);
100
+ const merged = mergeApprovalPolicies([attachment], NO_LEASES);
98
101
  expect(merged.size).toBe(0);
99
102
  });
100
103
 
104
+ it("stays approval-free when another server's usage overrides a same-named tool (issue #349)", () => {
105
+ const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
106
+ bridgeEndpoint: "https://mcp.stigmer.ai",
107
+ credential: "tok",
108
+ backendEndpoint: "http://localhost:7234",
109
+ })!;
110
+
111
+ // Before #349 a flat override list applied inside every server's merge
112
+ // loop, so this crm override would have force-gated the attachment's
113
+ // delete_record too — and on channels (UNATTENDED mode) a gated tool
114
+ // is silently skipped. Overrides now ride their own server, so the
115
+ // attachment (which has no usage) is immune by construction.
116
+ const crm: ResolvedMcpServer = {
117
+ slug: "crm",
118
+ connectionType: "http",
119
+ url: "https://crm.example.com/mcp",
120
+ toolApprovals: [],
121
+ pinnedToolApprovals: [],
122
+ toolApprovalOverrides: [
123
+ { toolName: "delete_record", requiresApproval: true, message: "" } as any,
124
+ ],
125
+ discoveredCapabilitiesEmpty: false,
126
+ };
127
+ const merged = mergeApprovalPolicies([crm, attachment], NO_LEASES);
128
+ expect(merged.has("crm/delete_record")).toBe(true);
129
+ expect(merged.has(`${DATASTORE_ATTACHMENT_SLUG}/delete_record`)).toBe(false);
130
+ });
131
+
101
132
  it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
102
133
  const attachment = synthesizeDatastoreAttachment([usage("clinic")], {
103
134
  bridgeEndpoint: "https://mcp.stigmer.ai",
@@ -127,6 +158,7 @@ describe("injectSynthesizedAttachment (the shared injection path)", () => {
127
158
  url: "https://example.com",
128
159
  toolApprovals: [],
129
160
  pinnedToolApprovals: [],
161
+ toolApprovalOverrides: [],
130
162
  discoveredCapabilitiesEmpty: false,
131
163
  };
132
164
  const result = injectSynthesizedAttachment([other], attachment, "datastore records");
@@ -141,6 +173,7 @@ describe("injectSynthesizedAttachment (the shared injection path)", () => {
141
173
  url: "https://evil.example.com",
142
174
  toolApprovals: [],
143
175
  pinnedToolApprovals: [],
176
+ toolApprovalOverrides: [],
144
177
  discoveredCapabilitiesEmpty: false,
145
178
  };
146
179
 
@@ -153,6 +186,45 @@ describe("injectSynthesizedAttachment (the shared injection path)", () => {
153
186
  });
154
187
  });
155
188
 
189
+ describe("EXPECTED_RECORD_TOOLS", () => {
190
+ it("pins the records-roster contract: exactly the five tools the mcp-server registers", () => {
191
+ // Drift pin (issue #325): the reconciliation treats ANY of these as
192
+ // mandatory because mcp-server/src/domains/records/tools.ts registers
193
+ // all five unconditionally for the agent audience. If a record tool is
194
+ // ever added, removed, or renamed there, this pin forces the reviewer
195
+ // to update the roster contract deliberately in both places.
196
+ expect([...EXPECTED_RECORD_TOOLS]).toEqual([
197
+ "describe_datastore",
198
+ "find_records",
199
+ "insert_record",
200
+ "update_record",
201
+ "delete_record",
202
+ ]);
203
+ });
204
+ });
205
+
206
+ describe("missingRecordTools", () => {
207
+ it("returns empty when all five record tools are connected", () => {
208
+ expect(missingRecordTools([...EXPECTED_RECORD_TOOLS])).toEqual([]);
209
+ });
210
+
211
+ it("returns every expected tool when the roster is empty", () => {
212
+ expect(missingRecordTools([])).toEqual([...EXPECTED_RECORD_TOOLS]);
213
+ });
214
+
215
+ it("returns only the absent tools on a partial roster", () => {
216
+ expect(
217
+ missingRecordTools(["describe_datastore", "find_records", "insert_record"]),
218
+ ).toEqual(["update_record", "delete_record"]);
219
+ });
220
+
221
+ it("ignores extraneous tool names", () => {
222
+ expect(
223
+ missingRecordTools([...EXPECTED_RECORD_TOOLS, "some_other_tool"]),
224
+ ).toEqual([]);
225
+ });
226
+ });
227
+
156
228
  describe("formatDatastoresSection", () => {
157
229
  it("names the attached datastores and points at describe_datastore first", () => {
158
230
  const section = formatDatastoresSection([usage("clinic"), usage("inventory")]);
@@ -162,4 +234,60 @@ describe("formatDatastoresSection", () => {
162
234
  expect(section).toContain("describe_datastore");
163
235
  expect(section).toContain("</available_datastores>");
164
236
  });
237
+
238
+ it("carries the standing failure-disclosure instruction in the healthy section", () => {
239
+ // The only mechanism covering tools that connected but fail at call
240
+ // time (the WhatsApp-pilot outage shape), and the Cursor harness's
241
+ // entire coverage — that harness can never observe the live roster.
242
+ const section = formatDatastoresSection([usage("clinic")]);
243
+ expect(section).toContain("the datastore is unreachable");
244
+ expect(section).toContain("do not answer from memory");
245
+ });
246
+
247
+ it("advertises exactly the expected record tools (no hand-written drift)", () => {
248
+ const section = formatDatastoresSection([usage("clinic")]);
249
+ for (const tool of EXPECTED_RECORD_TOOLS) {
250
+ expect(section).toContain(tool);
251
+ }
252
+ });
253
+
254
+ it("renders the degraded section instead when record tools are missing", () => {
255
+ const section = formatDatastoresSection(
256
+ [usage("clinic")],
257
+ ["find_records", "insert_record"],
258
+ );
259
+ // Must NOT promise tools the agent does not have — the issue's core
260
+ // complaint (oss#325).
261
+ expect(section).not.toContain("<available_datastores>");
262
+ expect(section).toContain("<unavailable_datastores>");
263
+ expect(section).toContain("find_records, insert_record.");
264
+ expect(section).toContain("- clinic");
265
+ expect(section).toContain("cannot be reached right now");
266
+ expect(section).toContain("Never answer");
267
+ expect(section).toContain("</unavailable_datastores>");
268
+ });
269
+
270
+ it("renders healthy when the missing list is empty (negative control)", () => {
271
+ const section = formatDatastoresSection([usage("clinic")], []);
272
+ expect(section).toContain("<available_datastores>");
273
+ expect(section).not.toContain("<unavailable_datastores>");
274
+ });
275
+ });
276
+
277
+ describe("formatDatastoreDegradationNotice", () => {
278
+ it("states declared count, connected fraction, and the missing tools", () => {
279
+ const notice = formatDatastoreDegradationNotice(2, [
280
+ "update_record",
281
+ "delete_record",
282
+ ]);
283
+ expect(notice).toContain("declared 2 datastore(s)");
284
+ expect(notice).toContain("3/5 record tools connected");
285
+ expect(notice).toContain("missing: update_record, delete_record");
286
+ expect(notice).toContain("instructed to disclose");
287
+ });
288
+
289
+ it("reads 0/5 on a fully absent roster", () => {
290
+ const notice = formatDatastoreDegradationNotice(1, [...EXPECTED_RECORD_TOOLS]);
291
+ expect(notice).toContain("0/5 record tools connected");
292
+ });
165
293
  });
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Adapter test for the foundry backend branch of buildChatModel — the
3
+ * complement of foundry-seam.test.ts, mirroring vertex-adapter.test.ts and
4
+ * bedrock-adapter.test.ts.
5
+ *
6
+ * The seam test pins the REAL AnthropicFoundry client's wire behavior under
7
+ * a hand-built ChatAnthropic. This file pins OUR production wiring: a real
8
+ * `buildChatModel` and a real `ChatAnthropic` drive a mocked
9
+ * `@anthropic-ai/foundry-sdk` (and a mocked `@azure/identity`), so
10
+ * LangChain's lazy `createClient` invocation path is exercised exactly as
11
+ * production does it. Together the two files cover the whole chain without
12
+ * any test-only injection seam in production code.
13
+ *
14
+ * Pinned here:
15
+ * - the factory forwards LangChain's `maxRetries: 0` to the client,
16
+ * - the factory forwards the request timeout (clientOptions.timeout ->
17
+ * factory options -> SDK constructor; how STIGMER_LLM_REQUEST_TIMEOUT_MS
18
+ * bounds this backend),
19
+ * - construction and invocation succeed with NO ANTHROPIC_API_KEY (Foundry
20
+ * auth is Azure's, and ChatAnthropic waives the key when createClient is
21
+ * provided),
22
+ * - the deployment name (strip-date rule, map overrides applied) crosses
23
+ * the wire while the returned apiModelId stays canonical (the
24
+ * canonical-id invariant),
25
+ * - auth selection: ANTHROPIC_FOUNDRY_API_KEY present -> no token provider
26
+ * and @azure/identity never touched; absent -> the Entra chain provider
27
+ * built with the Foundry scope and handed to the client,
28
+ * - maxTokens parity: stripping the date preserves LangChain's per-model
29
+ * default, so the adapter needs NO injection (unlike bedrock's 4096
30
+ * landmine) — pinned so a LangChain bump that diverges the two forms
31
+ * fails here instead of silently capping Foundry deployments,
32
+ * - usage_metadata survives the adapter (what billing reads).
33
+ */
34
+
35
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
36
+ import { HumanMessage, AIMessage } from "@langchain/core/messages";
37
+
38
+ const { foundryCtorArgs, createRequests, identityCalls } = vi.hoisted(() => ({
39
+ foundryCtorArgs: [] as Array<{
40
+ maxRetries?: number;
41
+ timeout?: number;
42
+ azureADTokenProvider?: () => Promise<string>;
43
+ }>,
44
+ createRequests: [] as Array<Record<string, unknown>>,
45
+ identityCalls: {
46
+ credentialConstructions: 0,
47
+ tokenProviderScopes: [] as string[],
48
+ },
49
+ }));
50
+
51
+ vi.mock("@anthropic-ai/foundry-sdk", () => ({
52
+ AnthropicFoundry: class {
53
+ readonly maxRetries: number | undefined;
54
+ readonly messages = {
55
+ create: async (request: Record<string, unknown>) => {
56
+ createRequests.push(request);
57
+ return {
58
+ id: "msg_foundry_adapter_01",
59
+ type: "message",
60
+ role: "assistant",
61
+ model: request.model,
62
+ content: [{ type: "text", text: "Namaste from Foundry." }],
63
+ stop_reason: "end_turn",
64
+ stop_sequence: null,
65
+ usage: { input_tokens: 7, output_tokens: 5 },
66
+ };
67
+ },
68
+ };
69
+
70
+ constructor(opts: {
71
+ maxRetries?: number;
72
+ timeout?: number;
73
+ azureADTokenProvider?: () => Promise<string>;
74
+ }) {
75
+ foundryCtorArgs.push(opts);
76
+ this.maxRetries = opts.maxRetries;
77
+ }
78
+ },
79
+ }));
80
+
81
+ vi.mock("@azure/identity", () => ({
82
+ DefaultAzureCredential: class {
83
+ constructor() {
84
+ identityCalls.credentialConstructions += 1;
85
+ }
86
+ },
87
+ getBearerTokenProvider: (_credential: unknown, scope: string) => {
88
+ identityCalls.tokenProviderScopes.push(scope);
89
+ return async () => "entra-adapter-token";
90
+ },
91
+ }));
92
+
93
+ import { ChatAnthropic } from "@langchain/anthropic";
94
+
95
+ import { buildChatModel } from "../model-client.js";
96
+ import { _resetRegistryCache } from "../model-registry.js";
97
+
98
+ describe("buildChatModel foundry adapter", () => {
99
+ const savedEnv = { ...process.env };
100
+
101
+ beforeEach(() => {
102
+ _resetRegistryCache();
103
+ foundryCtorArgs.length = 0;
104
+ createRequests.length = 0;
105
+ identityCalls.credentialConstructions = 0;
106
+ identityCalls.tokenProviderScopes.length = 0;
107
+ delete process.env.ANTHROPIC_API_KEY;
108
+ delete process.env.ANTHROPIC_FOUNDRY_BASE_URL;
109
+ delete process.env.STIGMER_FOUNDRY_DEPLOYMENT_MAP;
110
+ process.env.STIGMER_ANTHROPIC_BACKEND = "foundry";
111
+ process.env.ANTHROPIC_FOUNDRY_RESOURCE = "adapter-test-resource";
112
+ process.env.ANTHROPIC_FOUNDRY_API_KEY = "foundry-adapter-test-key";
113
+ // Registry offline → resolveToApiModelId passes the id through, keeping
114
+ // this test free of registry fixtures (that path has its own tests).
115
+ vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry offline"));
116
+ });
117
+
118
+ afterEach(() => {
119
+ _resetRegistryCache();
120
+ vi.restoreAllMocks();
121
+ process.env = { ...savedEnv };
122
+ });
123
+
124
+ it("drives the mocked client through the real createClient path with the invariants intact", async () => {
125
+ const { model, apiModelId } = await buildChatModel({
126
+ modelName: "claude-sonnet-4-5-20250929",
127
+ maxTokens: 1024,
128
+ });
129
+
130
+ // Canonical id returned; the client is not constructed yet (lazy factory).
131
+ expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
132
+ expect(foundryCtorArgs).toHaveLength(0);
133
+
134
+ const result = await model.invoke([new HumanMessage("Weather in Chennai?")]);
135
+
136
+ // First request constructed exactly one (batch) client, with LangChain's
137
+ // maxRetries: 0 honored — LangChain owns retrying.
138
+ expect(foundryCtorArgs).toHaveLength(1);
139
+ expect(foundryCtorArgs[0].maxRetries).toBe(0);
140
+
141
+ // The wire carries the deployment name (date stripped); billing already
142
+ // got the canonical id above.
143
+ expect(createRequests).toHaveLength(1);
144
+ expect(createRequests[0].model).toBe("claude-sonnet-4-5");
145
+ expect(createRequests[0].max_tokens).toBe(1024);
146
+
147
+ // Response and usage flow back through LangChain untouched.
148
+ expect(result).toBeInstanceOf(AIMessage);
149
+ expect(result.text).toBe("Namaste from Foundry.");
150
+ expect((result as AIMessage).usage_metadata).toMatchObject({
151
+ input_tokens: 7,
152
+ output_tokens: 5,
153
+ total_tokens: 12,
154
+ });
155
+ });
156
+
157
+ it("honors a deployment-map override over the strip-date rule", async () => {
158
+ process.env.STIGMER_FOUNDRY_DEPLOYMENT_MAP =
159
+ "claude-sonnet-4-6=my-sonnet-deployment";
160
+
161
+ const { model, apiModelId } = await buildChatModel({
162
+ modelName: "claude-sonnet-4-6",
163
+ maxTokens: 256,
164
+ });
165
+ await model.invoke([new HumanMessage("hi")]);
166
+
167
+ expect(apiModelId).toBe("claude-sonnet-4-6");
168
+ expect(createRequests.at(-1)?.model).toBe("my-sonnet-deployment");
169
+ });
170
+
171
+ it("uses API-key auth when ANTHROPIC_FOUNDRY_API_KEY is set — @azure/identity untouched", async () => {
172
+ const { model } = await buildChatModel({
173
+ modelName: "claude-sonnet-4-6",
174
+ maxTokens: 256,
175
+ });
176
+ await model.invoke([new HumanMessage("hi")]);
177
+
178
+ // The SDK reads the key from its own env var; the adapter must not
179
+ // pass a token provider alongside it (the constructor treats them as
180
+ // mutually exclusive) and must not touch the Azure credential chain.
181
+ expect(foundryCtorArgs.at(-1)?.azureADTokenProvider).toBeUndefined();
182
+ expect(identityCalls.credentialConstructions).toBe(0);
183
+ expect(identityCalls.tokenProviderScopes).toHaveLength(0);
184
+ });
185
+
186
+ it("falls back to the Entra chain with the Foundry scope when no key is set", async () => {
187
+ delete process.env.ANTHROPIC_FOUNDRY_API_KEY;
188
+
189
+ const { model } = await buildChatModel({
190
+ modelName: "claude-sonnet-4-6",
191
+ maxTokens: 256,
192
+ });
193
+ await model.invoke([new HumanMessage("hi")]);
194
+
195
+ // One DefaultAzureCredential, one provider with the ai.azure.com scope,
196
+ // handed to the client so every request authenticates as Bearer.
197
+ expect(identityCalls.credentialConstructions).toBe(1);
198
+ expect(identityCalls.tokenProviderScopes).toEqual(["https://ai.azure.com/.default"]);
199
+ const provider = foundryCtorArgs.at(-1)?.azureADTokenProvider;
200
+ expect(provider).toBeTypeOf("function");
201
+ await expect(provider!()).resolves.toBe("entra-adapter-token");
202
+ });
203
+
204
+ it("treats a whitespace-only key as absent (Entra, not a blank x-api-key)", async () => {
205
+ process.env.ANTHROPIC_FOUNDRY_API_KEY = " ";
206
+
207
+ const { model } = await buildChatModel({
208
+ modelName: "claude-sonnet-4-6",
209
+ maxTokens: 256,
210
+ });
211
+ await model.invoke([new HumanMessage("hi")]);
212
+
213
+ expect(foundryCtorArgs.at(-1)?.azureADTokenProvider).toBeTypeOf("function");
214
+ });
215
+
216
+ it("needs no maxTokens injection: the deployment name inherits the canonical default", async () => {
217
+ // The vertex-style parity pin (contrast with bedrock's 4096 landmine):
218
+ // LangChain's per-model default table prefix-matches the model string,
219
+ // and a date-stripped id is a prefix of its own canonical form, so both
220
+ // resolve identically — probed 2026-08-11 across the full catalog. If a
221
+ // LangChain bump ever adds date-qualified table entries that diverge
222
+ // the two forms, this fails and the adapter needs bedrock's
223
+ // canonical-probe injection.
224
+ const canonicalDefault = new ChatAnthropic({
225
+ model: "claude-sonnet-4-5-20250929",
226
+ apiKey: "probe",
227
+ }).maxTokens;
228
+ const deploymentDefault = new ChatAnthropic({
229
+ model: "claude-sonnet-4-5",
230
+ apiKey: "probe",
231
+ }).maxTokens;
232
+ expect(deploymentDefault).toBe(canonicalDefault);
233
+
234
+ const { model } = await buildChatModel({ modelName: "claude-sonnet-4-5-20250929" });
235
+ await model.invoke([new HumanMessage("hi")]);
236
+
237
+ expect(createRequests.at(-1)?.max_tokens).toBe(canonicalDefault);
238
+ });
239
+
240
+ it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
241
+ // The timeout rides clientOptions -> factory options -> SDK constructor.
242
+ // A factory that drops it silently unbounds the operator's timeout on
243
+ // this backend (the regression T06 repaired for the public path).
244
+ const { model } = await buildChatModel({
245
+ modelName: "claude-sonnet-4-6",
246
+ maxTokens: 256,
247
+ timeoutMs: 5000,
248
+ });
249
+ await model.invoke([new HumanMessage("hi")]);
250
+
251
+ expect(foundryCtorArgs).toHaveLength(1);
252
+ expect(foundryCtorArgs[0].timeout).toBe(5000);
253
+ expect(foundryCtorArgs[0].maxRetries).toBe(0);
254
+ });
255
+
256
+ it("constructs and invokes with no ANTHROPIC_API_KEY anywhere in the environment", async () => {
257
+ expect(process.env.ANTHROPIC_API_KEY).toBeUndefined();
258
+
259
+ const { model } = await buildChatModel({
260
+ modelName: "claude-sonnet-4-6",
261
+ maxTokens: 256,
262
+ });
263
+ const result = await model.invoke([new HumanMessage("hi")]);
264
+
265
+ expect(result).toBeInstanceOf(AIMessage);
266
+ expect(createRequests.at(-1)?.model).toBe("claude-sonnet-4-6");
267
+ });
268
+
269
+ it("fails at dispatch with the catalog message when no endpoint is configured", async () => {
270
+ delete process.env.ANTHROPIC_FOUNDRY_RESOURCE;
271
+
272
+ await expect(
273
+ buildChatModel({ modelName: "claude-sonnet-4-6", maxTokens: 256 }),
274
+ ).rejects.toThrow(/ANTHROPIC_FOUNDRY_RESOURCE/);
275
+ });
276
+ });