@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,213 @@
1
+ /**
2
+ * Adapter test for the bedrock backend branch of buildChatModel — the
3
+ * complement of bedrock-seam.test.ts, mirroring vertex-adapter.test.ts.
4
+ *
5
+ * The seam test pins the REAL AnthropicBedrock client's wire behavior under
6
+ * a hand-built ChatAnthropic. This file pins OUR production wiring: a real
7
+ * `buildChatModel` and a real `ChatAnthropic` drive a mocked
8
+ * `@anthropic-ai/bedrock-sdk`, so LangChain's lazy `createClient` invocation
9
+ * path (client constructed on first request, once per cached client) is
10
+ * exercised exactly as production does it. Together the two files cover the
11
+ * whole chain without any test-only injection seam in production code.
12
+ *
13
+ * Pinned here:
14
+ * - the factory forwards LangChain's `maxRetries: 0` to the client (a
15
+ * factory that drops it nests SDK retries inside LangChain's own loop),
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 (Bedrock
20
+ * auth is AWS's, and ChatAnthropic waives the key when createClient is
21
+ * provided),
22
+ * - the translated `anthropic.…-v1:0` id (with the deployment's inference
23
+ * prefix and map overrides applied) crosses the wire while the returned
24
+ * apiModelId stays canonical (the canonical-id invariant),
25
+ * - the maxTokens landmine: Bedrock ids miss LangChain's per-model default
26
+ * table, so the adapter must inject the CANONICAL id's default,
27
+ * - usage_metadata survives the adapter (what billing reads).
28
+ */
29
+
30
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
31
+ import { HumanMessage, AIMessage } from "@langchain/core/messages";
32
+
33
+ const { bedrockCtorArgs, createRequests } = vi.hoisted(() => ({
34
+ bedrockCtorArgs: [] as Array<{ maxRetries?: number; timeout?: number }>,
35
+ createRequests: [] as Array<Record<string, unknown>>,
36
+ }));
37
+
38
+ vi.mock("@anthropic-ai/bedrock-sdk", () => ({
39
+ AnthropicBedrock: class {
40
+ readonly maxRetries: number | undefined;
41
+ readonly messages = {
42
+ create: async (request: Record<string, unknown>) => {
43
+ createRequests.push(request);
44
+ return {
45
+ id: "msg_bedrock_adapter_01",
46
+ type: "message",
47
+ role: "assistant",
48
+ model: request.model,
49
+ content: [{ type: "text", text: "Namaste from Bedrock." }],
50
+ stop_reason: "end_turn",
51
+ stop_sequence: null,
52
+ usage: { input_tokens: 7, output_tokens: 5 },
53
+ };
54
+ },
55
+ };
56
+
57
+ constructor(opts: { maxRetries?: number; timeout?: number }) {
58
+ bedrockCtorArgs.push(opts);
59
+ this.maxRetries = opts.maxRetries;
60
+ }
61
+ },
62
+ }));
63
+
64
+ import { ChatAnthropic } from "@langchain/anthropic";
65
+
66
+ import { buildChatModel } from "../model-client.js";
67
+ import { _resetRegistryCache } from "../model-registry.js";
68
+
69
+ describe("buildChatModel bedrock adapter", () => {
70
+ const savedEnv = { ...process.env };
71
+
72
+ beforeEach(() => {
73
+ _resetRegistryCache();
74
+ bedrockCtorArgs.length = 0;
75
+ createRequests.length = 0;
76
+ delete process.env.ANTHROPIC_API_KEY;
77
+ delete process.env.STIGMER_BEDROCK_MODEL_MAP;
78
+ delete process.env.STIGMER_BEDROCK_INFERENCE_PREFIX;
79
+ process.env.STIGMER_ANTHROPIC_BACKEND = "bedrock";
80
+ process.env.AWS_REGION = "ap-south-1";
81
+ // Registry offline → resolveToApiModelId passes the id through, keeping
82
+ // this test free of registry fixtures (that path has its own tests).
83
+ vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry offline"));
84
+ });
85
+
86
+ afterEach(() => {
87
+ _resetRegistryCache();
88
+ vi.restoreAllMocks();
89
+ process.env = { ...savedEnv };
90
+ });
91
+
92
+ it("drives the mocked client through the real createClient path with the invariants intact", async () => {
93
+ const { model, apiModelId } = await buildChatModel({
94
+ modelName: "claude-sonnet-4-5-20250929",
95
+ maxTokens: 1024,
96
+ });
97
+
98
+ // Canonical id returned; the client is not constructed yet (lazy factory).
99
+ expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
100
+ expect(bedrockCtorArgs).toHaveLength(0);
101
+
102
+ const result = await model.invoke([new HumanMessage("Weather in Chennai?")]);
103
+
104
+ // First request constructed exactly one (batch) client, with LangChain's
105
+ // maxRetries: 0 honored — LangChain owns retrying.
106
+ expect(bedrockCtorArgs).toHaveLength(1);
107
+ expect(bedrockCtorArgs[0].maxRetries).toBe(0);
108
+
109
+ // The wire carries the Bedrock-translated id; billing already got the
110
+ // canonical one above.
111
+ expect(createRequests).toHaveLength(1);
112
+ expect(createRequests[0].model).toBe("anthropic.claude-sonnet-4-5-20250929-v1:0");
113
+ expect(createRequests[0].max_tokens).toBe(1024);
114
+
115
+ // Response and usage flow back through LangChain untouched.
116
+ expect(result).toBeInstanceOf(AIMessage);
117
+ expect(result.text).toBe("Namaste from Bedrock.");
118
+ expect((result as AIMessage).usage_metadata).toMatchObject({
119
+ input_tokens: 7,
120
+ output_tokens: 5,
121
+ total_tokens: 12,
122
+ });
123
+ });
124
+
125
+ it("applies the deployment's inference prefix to the wire id, canonical id untouched", async () => {
126
+ process.env.STIGMER_BEDROCK_INFERENCE_PREFIX = "us";
127
+
128
+ const { model, apiModelId } = await buildChatModel({
129
+ modelName: "claude-sonnet-4-6",
130
+ maxTokens: 256,
131
+ });
132
+ await model.invoke([new HumanMessage("hi")]);
133
+
134
+ expect(apiModelId).toBe("claude-sonnet-4-6");
135
+ expect(createRequests.at(-1)?.model).toBe("us.anthropic.claude-sonnet-4-6-v1:0");
136
+ });
137
+
138
+ it("honors a model-map override over prefix and rule", async () => {
139
+ process.env.STIGMER_BEDROCK_MODEL_MAP =
140
+ "claude-sonnet-4-6=eu.anthropic.claude-sonnet-4-6-custom-v2:0";
141
+ process.env.STIGMER_BEDROCK_INFERENCE_PREFIX = "us";
142
+
143
+ const { model } = await buildChatModel({
144
+ modelName: "claude-sonnet-4-6",
145
+ maxTokens: 256,
146
+ });
147
+ await model.invoke([new HumanMessage("hi")]);
148
+
149
+ expect(createRequests.at(-1)?.model).toBe("eu.anthropic.claude-sonnet-4-6-custom-v2:0");
150
+ });
151
+
152
+ it("injects the CANONICAL id's default maxTokens when the caller omits it (the 4096 landmine)", async () => {
153
+ // LangChain's per-model default table prefix-matches the model string.
154
+ // `anthropic.…` ids match nothing and silently fall back to 4096 —
155
+ // setup.ts omits maxTokens, so every deep-agent call would be capped.
156
+ // The adapter must inject what the canonical id would have received on
157
+ // the public API or vertex, whatever that is at the installed LangChain
158
+ // version (16384 for 4.5/4.6-generation ids at 1.4.0).
159
+ const canonicalDefault = new ChatAnthropic({
160
+ model: "claude-sonnet-4-5-20250929",
161
+ apiKey: "probe",
162
+ }).maxTokens;
163
+ const bedrockFallback = new ChatAnthropic({
164
+ model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
165
+ apiKey: "probe",
166
+ }).maxTokens;
167
+ // The landmine this guards against: if these ever agree, the injection
168
+ // is redundant (but still correct) — the pin below is what matters.
169
+ expect(bedrockFallback).not.toBe(canonicalDefault);
170
+
171
+ const { model } = await buildChatModel({ modelName: "claude-sonnet-4-5-20250929" });
172
+ await model.invoke([new HumanMessage("hi")]);
173
+
174
+ expect(createRequests.at(-1)?.max_tokens).toBe(canonicalDefault);
175
+ });
176
+
177
+ it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
178
+ // The timeout rides clientOptions -> factory options -> SDK constructor.
179
+ // A factory that drops it silently unbounds the operator's timeout on
180
+ // this backend (the regression T06 repaired for the public path).
181
+ const { model } = await buildChatModel({
182
+ modelName: "claude-sonnet-4-6",
183
+ maxTokens: 256,
184
+ timeoutMs: 5000,
185
+ });
186
+ await model.invoke([new HumanMessage("hi")]);
187
+
188
+ expect(bedrockCtorArgs).toHaveLength(1);
189
+ expect(bedrockCtorArgs[0].timeout).toBe(5000);
190
+ expect(bedrockCtorArgs[0].maxRetries).toBe(0);
191
+ });
192
+
193
+ it("constructs and invokes with no ANTHROPIC_API_KEY anywhere in the environment", async () => {
194
+ expect(process.env.ANTHROPIC_API_KEY).toBeUndefined();
195
+
196
+ const { model } = await buildChatModel({
197
+ modelName: "claude-sonnet-4-6",
198
+ maxTokens: 256,
199
+ });
200
+ const result = await model.invoke([new HumanMessage("hi")]);
201
+
202
+ expect(result).toBeInstanceOf(AIMessage);
203
+ expect(createRequests.at(-1)?.model).toBe("anthropic.claude-sonnet-4-6-v1:0");
204
+ });
205
+
206
+ it("fails at dispatch with the catalog message when AWS_REGION is missing", async () => {
207
+ delete process.env.AWS_REGION;
208
+
209
+ await expect(
210
+ buildChatModel({ modelName: "claude-sonnet-4-6", maxTokens: 256 }),
211
+ ).rejects.toThrow(/AWS_REGION/);
212
+ });
213
+ });
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Characterization test for the ChatAnthropic `createClient` ->
3
+ * AnthropicBedrock seam — the integration the T04 bedrock backend adapter
4
+ * is built on. Sibling of vertex-seam.test.ts; same rules: this pins REAL
5
+ * cross-package behavior (`@langchain/anthropic` bundling @anthropic-ai/sdk
6
+ * 0.95.x driving `@anthropic-ai/bedrock-sdk` bundling its own nested
7
+ * >=0.115 copy), so a future bump of either side fails here in CI instead
8
+ * of in production. See scripts/check-langchain-deps.sh for why the two
9
+ * @anthropic-ai/sdk versions coexist and when they collapse to one.
10
+ *
11
+ * Determinism: zero live credentials, zero network. SigV4 signing runs for
12
+ * real — it is pure HMAC over static test keys passed through the SDK's
13
+ * own `awsAccessKey`/`awsSecretKey` options (the AWS credential provider
14
+ * chain is bypassed entirely, so no IMDS/config-file probing) — and
15
+ * transport is a recording `fetch` injected through the SDK's `fetch`
16
+ * option. Streaming fixtures are binary AWS EventStream frames built with
17
+ * the SDK's OWN exported marshaller (getMinimalSerdeContext), so the
18
+ * EventStream -> SSE normalization path that every real Bedrock stream
19
+ * takes is exercised, not bypassed.
20
+ */
21
+
22
+ import { describe, it, expect } from "vitest";
23
+ import { ChatAnthropic } from "@langchain/anthropic";
24
+ import { AnthropicBedrock } from "@anthropic-ai/bedrock-sdk";
25
+ import { getMinimalSerdeContext } from "@anthropic-ai/bedrock-sdk/core/streaming";
26
+ import { HumanMessage, AIMessage, AIMessageChunk } from "@langchain/core/messages";
27
+
28
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Bedrock's id form for a dated pre-4.6 Claude: `anthropic.` vendor prefix
32
+ * and `-v1:0` version suffix (see llm-backend.ts translation, T04).
33
+ */
34
+ const BEDROCK_MODEL_ID = "anthropic.claude-sonnet-4-5-20250929-v1:0";
35
+ const REGION = "asia-south1";
36
+ const ACCESS_KEY = "AKIA-SEAM-TEST";
37
+ const SECRET_KEY = "seam-test-secret";
38
+
39
+ const WEATHER_TOOL = {
40
+ name: "get_weather",
41
+ description: "Get the current weather for a city.",
42
+ input_schema: {
43
+ type: "object" as const,
44
+ properties: { city: { type: "string" } },
45
+ required: ["city"],
46
+ },
47
+ };
48
+
49
+ /** Non-streaming (`/invoke`) response: text + tool_use + usage. */
50
+ const MESSAGE_RESPONSE = {
51
+ id: "msg_bedrock_test_01",
52
+ type: "message",
53
+ role: "assistant",
54
+ model: BEDROCK_MODEL_ID,
55
+ content: [
56
+ { type: "text", text: "I'll check the weather." },
57
+ { type: "tool_use", id: "toolu_test_01", name: "get_weather", input: { city: "Chennai" } },
58
+ ],
59
+ stop_reason: "tool_use",
60
+ stop_sequence: null,
61
+ usage: { input_tokens: 25, output_tokens: 17 },
62
+ };
63
+
64
+ /**
65
+ * Streaming (`/invoke-with-response-stream`) events in Anthropic's grammar.
66
+ * On the Bedrock wire each rides base64-encoded inside a binary EventStream
67
+ * `chunk` frame; the SDK's eventStreamToSSEResponse turns them back into
68
+ * the SSE the nested @anthropic-ai/sdk parses.
69
+ */
70
+ const STREAM_EVENTS: ReadonlyArray<Record<string, unknown>> = [
71
+ {
72
+ type: "message_start",
73
+ message: {
74
+ id: "msg_bedrock_test_02", type: "message", role: "assistant",
75
+ model: BEDROCK_MODEL_ID, content: [], stop_reason: null, stop_sequence: null,
76
+ usage: { input_tokens: 25, output_tokens: 1 },
77
+ },
78
+ },
79
+ { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
80
+ { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "I'll check the weather." } },
81
+ { type: "content_block_stop", index: 0 },
82
+ { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "toolu_test_02", name: "get_weather", input: {} } },
83
+ { type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: '{"city":"Chennai"}' } },
84
+ { type: "content_block_stop", index: 1 },
85
+ { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 17 } },
86
+ { type: "message_stop" },
87
+ ];
88
+
89
+ /**
90
+ * Serialize Anthropic events into Bedrock's binary EventStream framing
91
+ * using the SDK's own marshaller — the same codec its deserializer runs —
92
+ * so the fixture is wire-faithful by construction, not by hand-rolling.
93
+ */
94
+ async function binaryEventStreamBody(
95
+ events: ReadonlyArray<Record<string, unknown>>,
96
+ ): Promise<Buffer> {
97
+ const { eventStreamMarshaller } = getMinimalSerdeContext();
98
+ async function* input(): AsyncGenerator<Record<string, unknown>> {
99
+ yield* events;
100
+ }
101
+ const serialized = eventStreamMarshaller.serialize(input(), (event) => ({
102
+ headers: {
103
+ ":event-type": { type: "string", value: "chunk" },
104
+ ":message-type": { type: "string", value: "event" },
105
+ ":content-type": { type: "string", value: "application/json" },
106
+ },
107
+ body: new TextEncoder().encode(
108
+ JSON.stringify({ bytes: Buffer.from(JSON.stringify(event)).toString("base64") }),
109
+ ),
110
+ }));
111
+ const chunks: Buffer[] = [];
112
+ for await (const chunk of serialized) {
113
+ chunks.push(Buffer.from(chunk as Uint8Array));
114
+ }
115
+ return Buffer.concat(chunks);
116
+ }
117
+
118
+ // ─── Harness ─────────────────────────────────────────────────────────────────
119
+
120
+ interface RecordedRequest {
121
+ url: string;
122
+ method: string;
123
+ headers: Record<string, string>;
124
+ body: Record<string, unknown>;
125
+ }
126
+
127
+ interface SeamHarness {
128
+ model: ChatAnthropic;
129
+ requests: RecordedRequest[];
130
+ /** Options ChatAnthropic passed to the createClient factory. */
131
+ factoryOptions: Array<{ maxRetries?: number }>;
132
+ /** The AnthropicBedrock instances the factory constructed. */
133
+ clients: AnthropicBedrock[];
134
+ }
135
+
136
+ /**
137
+ * Build a real ChatAnthropic wired to a real AnthropicBedrock through the
138
+ * `createClient` seam, with transport replaced by a recording fetch.
139
+ *
140
+ * The factory honors `maxRetries` from the incoming options: LangChain owns
141
+ * retrying (its AsyncCaller wraps every request) and passes `maxRetries: 0`
142
+ * so the underlying SDK must not retry underneath it — same contract the
143
+ * vertex seam pins, preserved by the T04 adapter.
144
+ */
145
+ function buildSeamHarness(streamBody?: Buffer): SeamHarness {
146
+ const requests: RecordedRequest[] = [];
147
+ const factoryOptions: SeamHarness["factoryOptions"] = [];
148
+ const clients: AnthropicBedrock[] = [];
149
+
150
+ const recordingFetch: typeof fetch = async (input, init) => {
151
+ const headers: Record<string, string> = {};
152
+ new Headers(init?.headers).forEach((value, key) => {
153
+ headers[key] = value;
154
+ });
155
+ requests.push({
156
+ url: String(input),
157
+ method: init?.method ?? "GET",
158
+ headers,
159
+ body: JSON.parse(String(init?.body)) as Record<string, unknown>,
160
+ });
161
+ const streaming = String(input).includes("invoke-with-response-stream");
162
+ return streaming
163
+ ? new Response(streamBody, {
164
+ status: 200,
165
+ // The real Bedrock wire content type — adaptResponse matches it
166
+ // positively and transcodes the binary frames back to SSE.
167
+ headers: { "Content-Type": "application/vnd.amazon.eventstream" },
168
+ })
169
+ : new Response(JSON.stringify(MESSAGE_RESPONSE), {
170
+ status: 200,
171
+ headers: { "Content-Type": "application/json" },
172
+ });
173
+ };
174
+
175
+ const model = new ChatAnthropic({
176
+ model: BEDROCK_MODEL_ID,
177
+ temperature: 0,
178
+ maxTokens: 1024,
179
+ createClient: (options) => {
180
+ factoryOptions.push({ maxRetries: options.maxRetries });
181
+ const client = new AnthropicBedrock({
182
+ awsRegion: REGION,
183
+ awsAccessKey: ACCESS_KEY,
184
+ awsSecretKey: SECRET_KEY,
185
+ fetch: recordingFetch,
186
+ maxRetries: options.maxRetries,
187
+ });
188
+ clients.push(client);
189
+ return client;
190
+ },
191
+ });
192
+
193
+ return { model, requests, factoryOptions, clients };
194
+ }
195
+
196
+ /**
197
+ * Bedrock moves the model id into the URL path. Pinned: the installed
198
+ * 0.32.x interpolates it UNENCODED — the `-v1:0` colon stays literal in
199
+ * the path (newer SDK source URI-encodes via its `path` template helper;
200
+ * if an upgrade flips this to `%3A`, AWS accepts both, but this pin makes
201
+ * the change visible instead of silent).
202
+ */
203
+ const expectedUrl = (specifier: "invoke" | "invoke-with-response-stream") =>
204
+ `https://bedrock-runtime.${REGION}.amazonaws.com/model/` +
205
+ `${BEDROCK_MODEL_ID}/${specifier}`;
206
+
207
+ // ─── Tests ───────────────────────────────────────────────────────────────────
208
+
209
+ describe("ChatAnthropic createClient -> AnthropicBedrock seam", () => {
210
+ it("constructs without an Anthropic API key when createClient is provided", () => {
211
+ // chat_models.js waives the "Anthropic API key not found" check for
212
+ // factory-constructed clients — the waiver the bedrock backend depends on.
213
+ expect(() => buildSeamHarness()).not.toThrow();
214
+ });
215
+
216
+ it("defaults awsRegion to us-east-1 when AWS_REGION is unset (why preflight requires it)", () => {
217
+ // Pinned deliberately: the SDK will happily route traffic to us-east-1
218
+ // on a missing region. For a deployment-controlled data-residency
219
+ // feature that silent cross-region default is unacceptable, so
220
+ // checkBedrockPrerequisites (llm-backend.ts) refuses to start without
221
+ // an explicit AWS_REGION. If the SDK ever drops the default, this test
222
+ // tells us the preflight rationale needs rewording.
223
+ const saved = process.env.AWS_REGION;
224
+ delete process.env.AWS_REGION;
225
+ try {
226
+ const client = new AnthropicBedrock({
227
+ awsAccessKey: ACCESS_KEY,
228
+ awsSecretKey: SECRET_KEY,
229
+ });
230
+ expect(client.awsRegion).toBe("us-east-1");
231
+ } finally {
232
+ if (saved !== undefined) process.env.AWS_REGION = saved;
233
+ }
234
+ });
235
+
236
+ it("shapes a non-streaming request into Bedrock wire form (/invoke) and SigV4-signs it", async () => {
237
+ const h = buildSeamHarness();
238
+
239
+ const result = await h.model.invoke([new HumanMessage("Weather in Chennai?")]);
240
+
241
+ expect(h.requests).toHaveLength(1);
242
+ const req = h.requests[0];
243
+
244
+ // Model id rides in the URL path (URI-encoded), never in the body;
245
+ // stream flag is likewise URL routing, not body content.
246
+ expect(req.url).toBe(expectedUrl("invoke"));
247
+ expect(req.method).toBe("POST");
248
+ expect(req.body).not.toHaveProperty("model");
249
+ expect(req.body).not.toHaveProperty("stream");
250
+ expect(req.body.anthropic_version).toBe("bedrock-2023-05-31");
251
+ expect(req.body.max_tokens).toBe(1024);
252
+
253
+ // SigV4 signature over the adapted request reached the wire, scoped to
254
+ // the region and the bedrock service — the signature must cover the
255
+ // FINAL (middleware-adapted) request or AWS rejects it.
256
+ expect(req.headers.authorization).toMatch(/^AWS4-HMAC-SHA256 /);
257
+ expect(req.headers.authorization).toContain(`/${REGION}/bedrock/aws4_request`);
258
+ expect(req.headers["x-amz-date"]).toMatch(/^\d{8}T\d{6}Z$/);
259
+
260
+ expect(result).toBeInstanceOf(AIMessage);
261
+ });
262
+
263
+ it("round-trips tool definitions and tool_use blocks into tool_calls", async () => {
264
+ const h = buildSeamHarness();
265
+ const withTools = h.model.bindTools([WEATHER_TOOL]);
266
+
267
+ const result = (await withTools.invoke([
268
+ new HumanMessage("Weather in Chennai?"),
269
+ ])) as AIMessage;
270
+
271
+ // Tool definition survived request shaping through the Bedrock adapter.
272
+ const tools = h.requests[0].body.tools as Array<{ name: string }>;
273
+ expect(tools).toHaveLength(1);
274
+ expect(tools[0].name).toBe("get_weather");
275
+
276
+ // The tool_use response block became a LangChain tool_call with its id —
277
+ // the identity the HITL approval flow keys on.
278
+ expect(result.tool_calls).toHaveLength(1);
279
+ expect(result.tool_calls?.[0]).toMatchObject({
280
+ id: "toolu_test_01",
281
+ name: "get_weather",
282
+ args: { city: "Chennai" },
283
+ });
284
+ });
285
+
286
+ it("reports usage_metadata on non-streaming responses (what billing reads)", async () => {
287
+ const h = buildSeamHarness();
288
+
289
+ const result = (await h.model.invoke([
290
+ new HumanMessage("Weather in Chennai?"),
291
+ ])) as AIMessage;
292
+
293
+ expect(result.usage_metadata).toMatchObject({
294
+ input_tokens: 25,
295
+ output_tokens: 17,
296
+ total_tokens: 42,
297
+ });
298
+ });
299
+
300
+ it("streams via /invoke-with-response-stream, transcoding EventStream frames to tool_calls and usage", async () => {
301
+ const h = buildSeamHarness(await binaryEventStreamBody(STREAM_EVENTS));
302
+
303
+ let final: AIMessageChunk | undefined;
304
+ for await (const chunk of await h.model.stream([
305
+ new HumanMessage("Weather in Chennai?"),
306
+ ])) {
307
+ final = final === undefined ? chunk : final.concat(chunk);
308
+ }
309
+
310
+ expect(h.requests).toHaveLength(1);
311
+ expect(h.requests[0].url).toBe(expectedUrl("invoke-with-response-stream"));
312
+ expect(h.requests[0].body).not.toHaveProperty("stream");
313
+ expect(h.requests[0].body).not.toHaveProperty("model");
314
+
315
+ expect(final).toBeDefined();
316
+ expect(final?.text).toBe("I'll check the weather.");
317
+ expect(final?.tool_calls).toHaveLength(1);
318
+ expect(final?.tool_calls?.[0]).toMatchObject({
319
+ id: "toolu_test_02",
320
+ name: "get_weather",
321
+ args: { city: "Chennai" },
322
+ });
323
+
324
+ // input from message_start; output accumulated across message events.
325
+ // Pinned to the observed accumulation (same math the vertex seam pins)
326
+ // so a LangChain bump that changes usage accounting fails here first.
327
+ expect(final?.usage_metadata).toMatchObject({
328
+ input_tokens: 25,
329
+ output_tokens: 18,
330
+ });
331
+ });
332
+
333
+ it("passes maxRetries: 0 to the factory and the honored value reaches the client", async () => {
334
+ const h = buildSeamHarness(await binaryEventStreamBody(STREAM_EVENTS));
335
+
336
+ await h.model.invoke([new HumanMessage("hi")]);
337
+ for await (const chunk of await h.model.stream([new HumanMessage("hi")])) {
338
+ void chunk;
339
+ }
340
+
341
+ // LangChain owns retrying: it must hand the factory maxRetries: 0, once
342
+ // per cached client (batch + streaming are constructed independently).
343
+ expect(h.factoryOptions).toHaveLength(2);
344
+ expect(h.factoryOptions.every((o) => o.maxRetries === 0)).toBe(true);
345
+ expect(h.clients).toHaveLength(2);
346
+ expect(h.clients.every((c) => c.maxRetries === 0)).toBe(true);
347
+ });
348
+
349
+ it("cannot serve countTokens: the request is never adapted to a Bedrock route", async () => {
350
+ // Bedrock supports neither token counting nor prompt caching — a
351
+ // documented limitation of the bedrock backend (operator doc,
352
+ // docs/guides/runners/model-backends.mdx). The installed 0.32.x still
353
+ // EXPOSES messages.countTokens (newer SDK source deletes the resource
354
+ // at construction), but its request bypasses the /model/… rewrite —
355
+ // MODEL_ENDPOINTS covers only /v1/complete and /v1/messages — so it
356
+ // targets a path that does not exist on the Bedrock runtime and can
357
+ // never succeed against AWS. Pinned so an SDK upgrade that either
358
+ // deletes the method or starts adapting it surfaces here, prompting a
359
+ // docs review instead of silent drift.
360
+ const requests: string[] = [];
361
+ const notFoundFetch: typeof fetch = async (input) => {
362
+ requests.push(String(input));
363
+ return new Response("{}", { status: 404 });
364
+ };
365
+ const client = new AnthropicBedrock({
366
+ awsRegion: REGION,
367
+ awsAccessKey: ACCESS_KEY,
368
+ awsSecretKey: SECRET_KEY,
369
+ fetch: notFoundFetch,
370
+ maxRetries: 0,
371
+ });
372
+
373
+ // 0.32.x quirk: the TYPES already omit countTokens while the RUNTIME
374
+ // still exposes it — the cast reaches the runtime truth this test pins.
375
+ const messages = client.messages as unknown as {
376
+ countTokens: (req: Record<string, unknown>) => Promise<unknown>;
377
+ };
378
+ await expect(
379
+ messages.countTokens({
380
+ model: BEDROCK_MODEL_ID,
381
+ messages: [{ role: "user", content: "hi" }],
382
+ }),
383
+ ).rejects.toThrow();
384
+ expect(requests).toHaveLength(1);
385
+ // Anthropic-shaped path on the Bedrock host — not a /model/… route.
386
+ expect(requests[0]).toBe(
387
+ `https://bedrock-runtime.${REGION}.amazonaws.com/v1/messages/count_tokens`,
388
+ );
389
+ });
390
+ });
@@ -11,6 +11,7 @@ import {
11
11
  CALLER_IDENTITY_VALUE_ENV_KEY,
12
12
  SESSION_ID_ENV_KEY,
13
13
  STIGMER_USER_KIND,
14
+ SYSTEM_CREATOR_SENTINEL,
14
15
  anonymousCallerIdentity,
15
16
  injectAnonymousCallerIdentityForDiscovery,
16
17
  injectCallerIdentityEnv,
@@ -70,6 +71,30 @@ describe("resolveCallerIdentity precedence", () => {
70
71
  });
71
72
  });
72
73
 
74
+ it('the "system" audit placeholder is never a caller — falls to anonymous (contract guard)', () => {
75
+ // The OSS server stamps created_by.id = "system" on every create (no
76
+ // local auth) and the cloud's AuditActorBuilder falls back to the
77
+ // same literal — one string shared by ALL such traffic. Presenting it
78
+ // would let a single MCP binding for "system" grant every one of
79
+ // those sessions, so it must resolve to the anonymous sentinel.
80
+ expect(resolveCallerIdentity(undefined, { id: SYSTEM_CREATOR_SENTINEL })).toEqual({
81
+ kind: ANONYMOUS_KIND,
82
+ value: "",
83
+ });
84
+ expect(resolveCallerIdentity({}, { id: " system ", email: "" })).toEqual({
85
+ kind: ANONYMOUS_KIND,
86
+ value: "",
87
+ });
88
+ });
89
+
90
+ it('a real email always wins — an account is never demoted for its id alone', () => {
91
+ // Email-first ordering: the sentinel check only ever sees creators
92
+ // that have no email, so a resolvable principal cannot be demoted.
93
+ expect(
94
+ resolveCallerIdentity(undefined, { id: SYSTEM_CREATOR_SENTINEL, email: "ops@example.com" }),
95
+ ).toEqual({ kind: STIGMER_USER_KIND, value: "ops@example.com" });
96
+ });
97
+
73
98
  it("a half-present channel identity (value without kind) is NOT a channel sender", () => {
74
99
  // readSenderIdentity requires value AND kind; a half-stamped session
75
100
  // falls through to the creator rather than fabricating a kind.
@@ -169,7 +169,7 @@ describe("synthesizeChannelAttachment", () => {
169
169
  // UNATTENDED mode, where a gated tool resolves as skip-and-adapt —
170
170
  // a gated send tool means reminders never send.
171
171
  const merged = mergeApprovalPolicies(
172
- [attachment as ResolvedMcpServer], [], noLeases,
172
+ [attachment as ResolvedMcpServer], noLeases,
173
173
  );
174
174
  expect(merged.size).toBe(0);
175
175
  });
@@ -14,6 +14,7 @@ function makeServer(overrides: Partial<ResolvedMcpServer> = {}): ResolvedMcpServ
14
14
  args: ["-y", "@mcp/test-server"],
15
15
  toolApprovals: [],
16
16
  pinnedToolApprovals: [],
17
+ toolApprovalOverrides: [],
17
18
  discoveredCapabilitiesEmpty: false,
18
19
  ...overrides,
19
20
  };
@@ -126,7 +126,7 @@ describe("synthesizeConversationAttachment", () => {
126
126
  // Channel surfaces run APPROVAL_MODE_UNATTENDED, where a gated tool
127
127
  // resolves as skip-and-adapt — a gated escalation would never fire
128
128
  // (DD-008's approval-free ruling, the DD-001 SD-3 structural bypass).
129
- const merged = mergeApprovalPolicies([attachment], [], noLeases);
129
+ const merged = mergeApprovalPolicies([attachment], noLeases);
130
130
  expect(merged.size).toBe(0);
131
131
  });
132
132