@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.
- package/README.md +12 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +32 -0
- package/dist/activities/discover-mcp-server.js +162 -27
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +25 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
- package/dist/activities/execute-cursor/error-classifier.js +81 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -1
- package/dist/activities/execute-cursor/index.js +59 -58
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
- package/dist/activities/execute-cursor/prompt-builder.js +12 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -26
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +3 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -4
- package/dist/client/stigmer-client.js +28 -15
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +32 -0
- package/dist/encryption/config.js +68 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/payload-codecs.d.ts +16 -0
- package/dist/payload-codecs.js +38 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +5 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +5 -16
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/attachment-vision.d.ts +43 -2
- package/dist/shared/attachment-vision.js +72 -6
- package/dist/shared/attachment-vision.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +42 -1
- package/dist/shared/checkpointer/http-saver.js +96 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/http-retry.d.ts +43 -0
- package/dist/shared/http-retry.js +50 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +17 -2
- package/dist/shared/mcp-manager.js +37 -25
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/model-client.d.ts +12 -5
- package/dist/shared/model-client.js +138 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-registry.d.ts +20 -2
- package/dist/shared/model-registry.js +37 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -0
- package/dist/shared/plan-mode-permissions.js +28 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +7 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +234 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +50 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +190 -32
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +91 -4
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +63 -72
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/prompt-builder.ts +34 -9
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +86 -31
- package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
- package/src/activities/hydrate-workflow-execution.ts +3 -1
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +8 -8
- package/src/client/stigmer-client.ts +32 -18
- package/src/encryption/config.ts +91 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/payload-codecs.ts +56 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +6 -24
- package/src/runner.ts +6 -25
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/attachment-vision.test.ts +97 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +67 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +170 -2
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/model-client.test.ts +154 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-registry.test.ts +71 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/attachment-vision.ts +92 -9
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +117 -9
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/http-retry.ts +50 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +43 -26
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/model-client.ts +161 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-registry.ts +50 -2
- package/src/shared/plan-mode-permissions.ts +30 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +7 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
|
@@ -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],
|
|
172
|
+
[attachment as ResolvedMcpServer], noLeases,
|
|
173
173
|
);
|
|
174
174
|
expect(merged.size).toBe(0);
|
|
175
175
|
});
|
|
@@ -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],
|
|
129
|
+
const merged = mergeApprovalPolicies([attachment], noLeases);
|
|
130
130
|
expect(merged.size).toBe(0);
|
|
131
131
|
});
|
|
132
132
|
|
|
@@ -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],
|
|
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
|
});
|