@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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History-encryption tripwire (stigmer-cloud#227).
|
|
3
|
+
*
|
|
4
|
+
* Runs a secret-bearing workflow through a real Temporal server with the
|
|
5
|
+
* encryption codec installed, then scans the RAW workflow history and
|
|
6
|
+
* asserts the secret bytes appear in no payload. This is the test that
|
|
7
|
+
* proves the issue's claim is closed: without the codec, the secret
|
|
8
|
+
* appears in the hydrate activity result, in every per-task activity
|
|
9
|
+
* input (the engine passes the full env map), and in local-activity
|
|
10
|
+
* markers from expression evaluation.
|
|
11
|
+
*
|
|
12
|
+
* Also pins the cross-language completion contract: the
|
|
13
|
+
* execute-from-execution workflow must complete with a data-less result
|
|
14
|
+
* (void), because the Java parent awaits it as Void and Temporal Java's
|
|
15
|
+
* converter has no Void special-case — a data-bearing encrypted result
|
|
16
|
+
* would fail its converter lookup.
|
|
17
|
+
*
|
|
18
|
+
* Follows the golden-e2e pattern: tests skip gracefully when the
|
|
19
|
+
* Temporal test server cannot start.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
23
|
+
import { randomBytes } from "node:crypto";
|
|
24
|
+
import { join, dirname } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { loadWorkflowFromYaml } from "../workflow-engine/loader.js";
|
|
27
|
+
import { evaluateExpressionBatch } from "../workflow-engine/expression.js";
|
|
28
|
+
import { EncryptionPayloadCodec } from "../encryption/payload-codec.js";
|
|
29
|
+
import type { ExecuteServerlessWorkflowInput } from "../workflows/execute-serverless-workflow.js";
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const WORKFLOWS_PATH = join(__dirname, "../workflows/index.ts");
|
|
33
|
+
const TASK_QUEUE = "history-encryption-e2e";
|
|
34
|
+
|
|
35
|
+
const SECRET = "sup3r-s3cret-t0ken-do-not-persist";
|
|
36
|
+
|
|
37
|
+
const SECRET_BEARING_YAML = `
|
|
38
|
+
document:
|
|
39
|
+
dsl: '1.0.0'
|
|
40
|
+
namespace: tripwire
|
|
41
|
+
name: secret-bearing
|
|
42
|
+
version: '1.0.0'
|
|
43
|
+
description: Exercises every env-to-history crossing with a secret value
|
|
44
|
+
do:
|
|
45
|
+
# Expression evaluation: the secret flows through an EvaluateExpressions
|
|
46
|
+
# local activity whose result is recorded as a history marker.
|
|
47
|
+
- stampToken:
|
|
48
|
+
set:
|
|
49
|
+
authHeader: \${ "Bearer " + $env.API_TOKEN }
|
|
50
|
+
# Per-task activity: CallHttp receives the interpolated config AND the
|
|
51
|
+
# full runtime env map as activity input.
|
|
52
|
+
- callApi:
|
|
53
|
+
call: http
|
|
54
|
+
with:
|
|
55
|
+
method: GET
|
|
56
|
+
endpoint:
|
|
57
|
+
uri: https://example.com/data
|
|
58
|
+
headers:
|
|
59
|
+
Authorization: \${ $context.authHeader }
|
|
60
|
+
- done:
|
|
61
|
+
set:
|
|
62
|
+
finished: true
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
type TestWorkflowEnvironment = import("@temporalio/testing").TestWorkflowEnvironment;
|
|
66
|
+
type Worker = import("@temporalio/worker").Worker;
|
|
67
|
+
|
|
68
|
+
let env: TestWorkflowEnvironment | null = null;
|
|
69
|
+
let worker: Worker | null = null;
|
|
70
|
+
let workerRunPromise: Promise<void> | null = null;
|
|
71
|
+
let envReady = false;
|
|
72
|
+
|
|
73
|
+
function createMockActivities() {
|
|
74
|
+
return {
|
|
75
|
+
HydrateWorkflowExecution: async (): Promise<ExecuteServerlessWorkflowInput> => ({
|
|
76
|
+
model: loadWorkflowFromYaml(SECRET_BEARING_YAML),
|
|
77
|
+
workflow_input: null,
|
|
78
|
+
env: { API_TOKEN: SECRET },
|
|
79
|
+
metadata: { execution_id: "tripwire-exec", org_id: "tripwire-org" },
|
|
80
|
+
}),
|
|
81
|
+
EvaluateExpressions: async (
|
|
82
|
+
expressions: Record<string, string>,
|
|
83
|
+
input: unknown,
|
|
84
|
+
stateVars: Record<string, unknown>,
|
|
85
|
+
): Promise<Record<string, unknown>> => {
|
|
86
|
+
return evaluateExpressionBatch(expressions, input, stateVars);
|
|
87
|
+
},
|
|
88
|
+
CallHttp: async (): Promise<unknown> => ({ ok: true }),
|
|
89
|
+
ResetEventSequence: async (): Promise<number> => 0,
|
|
90
|
+
EmitWorkflowEvents: async (): Promise<void> => {},
|
|
91
|
+
LoadRecoveryContext: async (): Promise<unknown[]> => [],
|
|
92
|
+
PromoteTaskOutput: async (): Promise<void> => {},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Recursively collects every byte field in the raw history proto. */
|
|
97
|
+
function collectByteFields(value: unknown, out: Uint8Array[]): void {
|
|
98
|
+
if (value instanceof Uint8Array) {
|
|
99
|
+
out.push(value);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
for (const item of value) collectByteFields(item, out);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (value && typeof value === "object") {
|
|
107
|
+
for (const item of Object.values(value)) collectByteFields(item, out);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe("History encryption tripwire — Temporal TestWorkflowEnvironment", () => {
|
|
112
|
+
beforeAll(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const { TestWorkflowEnvironment: TWE } = await import("@temporalio/testing");
|
|
115
|
+
const { Worker: W } = await import("@temporalio/worker");
|
|
116
|
+
|
|
117
|
+
env = await TWE.createLocal();
|
|
118
|
+
worker = await W.create({
|
|
119
|
+
connection: env.nativeConnection,
|
|
120
|
+
taskQueue: TASK_QUEUE,
|
|
121
|
+
workflowsPath: WORKFLOWS_PATH,
|
|
122
|
+
activities: createMockActivities(),
|
|
123
|
+
dataConverter: {
|
|
124
|
+
payloadCodecs: [
|
|
125
|
+
new EncryptionPayloadCodec({
|
|
126
|
+
primary: { keyId: "tripwire-key", key: randomBytes(32) },
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
workerRunPromise = worker.run();
|
|
132
|
+
envReady = true;
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
console.warn(`Temporal test server unavailable (tests will be skipped): ${msg}`);
|
|
136
|
+
envReady = false;
|
|
137
|
+
}
|
|
138
|
+
}, 60_000);
|
|
139
|
+
|
|
140
|
+
afterAll(async () => {
|
|
141
|
+
if (worker) {
|
|
142
|
+
worker.shutdown();
|
|
143
|
+
await workerRunPromise?.catch(() => {});
|
|
144
|
+
}
|
|
145
|
+
if (env) await env.teardown();
|
|
146
|
+
}, 30_000);
|
|
147
|
+
|
|
148
|
+
it("records no plaintext secret bytes anywhere in workflow history", async () => {
|
|
149
|
+
if (!envReady || !env) return;
|
|
150
|
+
|
|
151
|
+
const workflowId = `tripwire-${Date.now()}`;
|
|
152
|
+
// The starting client has NO codec, mirroring the Java/Go
|
|
153
|
+
// orchestrators: the slim input is plaintext and the void result must
|
|
154
|
+
// be readable without a key.
|
|
155
|
+
const result = await env.client.workflow.execute(
|
|
156
|
+
"stigmer/workflow/execute-from-execution",
|
|
157
|
+
{
|
|
158
|
+
taskQueue: TASK_QUEUE,
|
|
159
|
+
workflowId,
|
|
160
|
+
args: [
|
|
161
|
+
{
|
|
162
|
+
execution_id: "tripwire-exec",
|
|
163
|
+
workflow_instance_id: "",
|
|
164
|
+
workflow_id: "tripwire-wf",
|
|
165
|
+
org_id: "tripwire-org",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
workflowExecutionTimeout: "30s",
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
expect(result).toBeUndefined();
|
|
172
|
+
|
|
173
|
+
// Fetch the RAW history (no codec on this read — we want the bytes
|
|
174
|
+
// exactly as Temporal persisted them).
|
|
175
|
+
const secretBytes = Buffer.from(SECRET);
|
|
176
|
+
const byteFields: Uint8Array[] = [];
|
|
177
|
+
let encryptedPayloadCount = 0;
|
|
178
|
+
let completedResultHasData = false;
|
|
179
|
+
|
|
180
|
+
let nextPageToken: Uint8Array | undefined;
|
|
181
|
+
do {
|
|
182
|
+
const response = await env.client.workflowService.getWorkflowExecutionHistory({
|
|
183
|
+
namespace: "default",
|
|
184
|
+
execution: { workflowId },
|
|
185
|
+
nextPageToken,
|
|
186
|
+
});
|
|
187
|
+
for (const event of response.history?.events ?? []) {
|
|
188
|
+
collectByteFields(event, byteFields);
|
|
189
|
+
|
|
190
|
+
const completed = event.workflowExecutionCompletedEventAttributes;
|
|
191
|
+
if (completed?.result?.payloads?.some((p) => p.data && p.data.length > 0)) {
|
|
192
|
+
completedResultHasData = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
encryptedPayloadCount += countEncryptedEncodings(response.history?.events ?? []);
|
|
196
|
+
nextPageToken =
|
|
197
|
+
response.nextPageToken && response.nextPageToken.length > 0
|
|
198
|
+
? response.nextPageToken
|
|
199
|
+
: undefined;
|
|
200
|
+
} while (nextPageToken);
|
|
201
|
+
|
|
202
|
+
// The tripwire itself: the secret must not appear in any byte field
|
|
203
|
+
// of any history event — payload data, marker details, anywhere.
|
|
204
|
+
expect(byteFields.length).toBeGreaterThan(0);
|
|
205
|
+
for (const bytes of byteFields) {
|
|
206
|
+
expect(Buffer.from(bytes).includes(secretBytes)).toBe(false);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sanity: encryption was actually active (otherwise this test would
|
|
210
|
+
// pass vacuously against a broken codec setup).
|
|
211
|
+
expect(encryptedPayloadCount).toBeGreaterThan(0);
|
|
212
|
+
|
|
213
|
+
// Cross-language completion contract: void result, no data payload.
|
|
214
|
+
expect(completedResultHasData).toBe(false);
|
|
215
|
+
}, 60_000);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
function countEncryptedEncodings(events: unknown[]): number {
|
|
219
|
+
let count = 0;
|
|
220
|
+
const encodings: Uint8Array[] = [];
|
|
221
|
+
for (const event of events) {
|
|
222
|
+
collectEncodingMetadata(event, encodings);
|
|
223
|
+
}
|
|
224
|
+
for (const encoding of encodings) {
|
|
225
|
+
if (Buffer.from(encoding).toString("utf-8") === "binary/encrypted") count++;
|
|
226
|
+
}
|
|
227
|
+
return count;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectEncodingMetadata(value: unknown, out: Uint8Array[]): void {
|
|
231
|
+
if (!value || typeof value !== "object") return;
|
|
232
|
+
if (Array.isArray(value)) {
|
|
233
|
+
for (const item of value) collectEncodingMetadata(item, out);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const record = value as Record<string, unknown>;
|
|
237
|
+
const metadata = record["metadata"];
|
|
238
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
239
|
+
const encoding = (metadata as Record<string, unknown>)["encoding"];
|
|
240
|
+
if (encoding instanceof Uint8Array) out.push(encoding);
|
|
241
|
+
}
|
|
242
|
+
for (const item of Object.values(record)) collectEncodingMetadata(item, out);
|
|
243
|
+
}
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* the desktop Run dialog via the IPC handshake.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
import {
|
|
14
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
isNodeSqliteAvailable,
|
|
17
|
+
preflightNodeRuntime,
|
|
18
|
+
assertLlmBackendsPreflight,
|
|
19
|
+
} from "../preflight.js";
|
|
16
20
|
|
|
17
21
|
describe("preflightNodeRuntime", () => {
|
|
18
22
|
it("passes (returns null) when node:sqlite is available", () => {
|
|
@@ -39,3 +43,47 @@ describe("preflightNodeRuntime", () => {
|
|
|
39
43
|
expect(preflightNodeRuntime() === null).toBe(isNodeSqliteAvailable());
|
|
40
44
|
});
|
|
41
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("assertLlmBackendsPreflight", () => {
|
|
48
|
+
// This is the gate both runner factories run at construction — the throw
|
|
49
|
+
// is what a misconfigured deployment sees instead of accepting work. The
|
|
50
|
+
// per-case parse/prereq tables live in shared/__tests__/llm-backend.test.ts;
|
|
51
|
+
// here we pin the wrapper's contract: throw on fatal, warn on precedence,
|
|
52
|
+
// silent when clean, and options-proxy treated like the env proxy.
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.unstubAllEnvs();
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Pin the vars this gate reads so ambient shell env cannot leak in. */
|
|
59
|
+
function stubBackendEnv(vars: Record<string, string>) {
|
|
60
|
+
vi.stubEnv("STIGMER_PROXY_ENDPOINT", "");
|
|
61
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
62
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
63
|
+
vi.stubEnv("CLOUD_ML_REGION", "");
|
|
64
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
65
|
+
vi.stubEnv(key, value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("passes silently for an unconfigured deployment", () => {
|
|
70
|
+
stubBackendEnv({});
|
|
71
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
72
|
+
expect(() => assertLlmBackendsPreflight(null)).not.toThrow();
|
|
73
|
+
expect(warn).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("throws the actionable message for a misconfigured backend", () => {
|
|
77
|
+
stubBackendEnv({ STIGMER_ANTHROPIC_BACKEND: "vertex" });
|
|
78
|
+
expect(() => assertLlmBackendsPreflight(null)).toThrow(/CLOUD_ML_REGION/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("warns instead of throwing when an options-supplied proxy makes the var inert", () => {
|
|
82
|
+
stubBackendEnv({ STIGMER_ANTHROPIC_BACKEND: "vertex" });
|
|
83
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
84
|
+
expect(() => assertLlmBackendsPreflight("https://api.stigmer.ai")).not.toThrow();
|
|
85
|
+
expect(warn).toHaveBeenCalledWith(
|
|
86
|
+
expect.stringContaining("STIGMER_ANTHROPIC_BACKEND=vertex is ignored"),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -5,6 +5,45 @@ import { _resetRegistryCache } from "../../shared/model-registry.js";
|
|
|
5
5
|
const originalFetch = globalThis.fetch;
|
|
6
6
|
const originalEnv = { ...process.env };
|
|
7
7
|
|
|
8
|
+
const { vertexCreateRequests } = vi.hoisted(() => ({
|
|
9
|
+
vertexCreateRequests: [] as Array<Record<string, unknown>>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mocked at the SDK class (the vertex-adapter.test.ts pattern) so no real
|
|
13
|
+
// google-auth credential resolution ever runs inside a unit test — locally
|
|
14
|
+
// it fails fast, but in CI it can probe the GCE metadata server. ChatAnthropic
|
|
15
|
+
// consumes the client's parsed event stream, so the streaming arm returns a
|
|
16
|
+
// plain async generator of Anthropic stream events.
|
|
17
|
+
vi.mock("@anthropic-ai/vertex-sdk", () => ({
|
|
18
|
+
AnthropicVertex: class {
|
|
19
|
+
readonly messages = {
|
|
20
|
+
create: async (request: Record<string, unknown>) => {
|
|
21
|
+
vertexCreateRequests.push(request);
|
|
22
|
+
if (request.stream === true) {
|
|
23
|
+
return (async function* () {
|
|
24
|
+
yield { type: "message_start", message: { id: "msg_vertex_call_llm", type: "message", role: "assistant", model: request.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 11, output_tokens: 1 } } };
|
|
25
|
+
yield { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } };
|
|
26
|
+
yield { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Vertex direct." } };
|
|
27
|
+
yield { type: "content_block_stop", index: 0 };
|
|
28
|
+
yield { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 4 } };
|
|
29
|
+
yield { type: "message_stop" };
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
id: "msg_vertex_call_llm",
|
|
34
|
+
type: "message",
|
|
35
|
+
role: "assistant",
|
|
36
|
+
model: request.model,
|
|
37
|
+
content: [{ type: "text", text: "Vertex direct." }],
|
|
38
|
+
stop_reason: "end_turn",
|
|
39
|
+
stop_sequence: null,
|
|
40
|
+
usage: { input_tokens: 11, output_tokens: 4 },
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
8
47
|
describe("callLlmAction", () => {
|
|
9
48
|
let mockFetch: ReturnType<typeof vi.fn>;
|
|
10
49
|
|
|
@@ -16,6 +55,12 @@ describe("callLlmAction", () => {
|
|
|
16
55
|
delete process.env.STIGMER_TOKEN;
|
|
17
56
|
delete process.env.OPENAI_API_KEY;
|
|
18
57
|
delete process.env.ANTHROPIC_API_KEY;
|
|
58
|
+
// Deterministic regardless of the developer's shell: a leaked
|
|
59
|
+
// STIGMER_ANTHROPIC_BACKEND=vertex would reroute every direct-mode test.
|
|
60
|
+
delete process.env.STIGMER_ANTHROPIC_BACKEND;
|
|
61
|
+
delete process.env.STIGMER_OPENAI_BACKEND;
|
|
62
|
+
delete process.env.CLOUD_ML_REGION;
|
|
63
|
+
vertexCreateRequests.length = 0;
|
|
19
64
|
});
|
|
20
65
|
|
|
21
66
|
afterEach(() => {
|
|
@@ -234,6 +279,36 @@ describe("callLlmAction", () => {
|
|
|
234
279
|
});
|
|
235
280
|
});
|
|
236
281
|
|
|
282
|
+
describe("direct mode — Anthropic on the vertex backend", () => {
|
|
283
|
+
it("serves the call with no ANTHROPIC_API_KEY anywhere (ADC is the credential path)", async () => {
|
|
284
|
+
// The regression pin for the backend-blind key check: a correctly
|
|
285
|
+
// configured Vertex deployment holds no Anthropic key, and llm_call
|
|
286
|
+
// tasks must not fail with LLM_MISSING_API_KEY before construction.
|
|
287
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
|
|
288
|
+
process.env.CLOUD_ML_REGION = "global";
|
|
289
|
+
mockFetchWithRegistry(() => {
|
|
290
|
+
throw new Error("unexpected fetch: vertex traffic must go through the SDK client");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const result = await callLlmAction(baseAnthropicConfig, {}, "exec-1");
|
|
294
|
+
|
|
295
|
+
expect(result.result).toBe("Vertex direct.");
|
|
296
|
+
expect(result.provider).toBe("anthropic");
|
|
297
|
+
expect(result.input_tokens).toBe(11);
|
|
298
|
+
expect(result.output_tokens).toBe(4);
|
|
299
|
+
expect(vertexCreateRequests.length).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("keeps the LLM_MISSING_API_KEY contract on the public backend, now naming the backend remedy", async () => {
|
|
303
|
+
const err = await callLlmAction(baseAnthropicConfig, {}, "exec-1").catch((e: unknown) => e as Error & { type?: string });
|
|
304
|
+
|
|
305
|
+
expect(err).toBeInstanceOf(Error);
|
|
306
|
+
expect((err as { type?: string }).type).toBe("LLM_MISSING_API_KEY");
|
|
307
|
+
expect((err as Error).message).toContain("ANTHROPIC_API_KEY");
|
|
308
|
+
expect((err as Error).message).toContain("STIGMER_ANTHROPIC_BACKEND=vertex");
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
237
312
|
describe("proxy mode", () => {
|
|
238
313
|
it("routes through proxy when endpoint is set", async () => {
|
|
239
314
|
process.env.STIGMER_PROXY_ENDPOINT = "https://proxy.stigmer.ai";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import type { ClassifyToolApprovalsInput, ToolDescriptor } from "../classify-tool-approvals.js";
|
|
3
3
|
|
|
4
4
|
vi.mock("../../idle-watchdog.js", () => ({
|
|
@@ -597,6 +597,122 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
597
597
|
expect(headers).not.toHaveProperty("X-Stigmer-Mcp-Server-Id");
|
|
598
598
|
});
|
|
599
599
|
});
|
|
600
|
+
|
|
601
|
+
describe("direct mode (no proxy)", () => {
|
|
602
|
+
// The regression suite for the stigmerBackendEndpoint-as-LLM-proxy bug:
|
|
603
|
+
// an unproxied runner must call the provider directly with the real key,
|
|
604
|
+
// and must fail closed with one clear message when it has none.
|
|
605
|
+
const directOptions = { proxyEndpoint: null, stigmerToken: null, primaryModel: "gpt-4.1" };
|
|
606
|
+
|
|
607
|
+
beforeEach(() => {
|
|
608
|
+
// Deterministic regardless of the developer's shell: blank reads as
|
|
609
|
+
// missing, and backend/proxy vars must not leak in from outside.
|
|
610
|
+
vi.stubEnv("OPENAI_API_KEY", "");
|
|
611
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "");
|
|
612
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
613
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
614
|
+
vi.stubEnv("STIGMER_PROXY_ENDPOINT", "");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
afterEach(() => {
|
|
618
|
+
vi.unstubAllEnvs();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("fails closed without any credential path: every tool gated, no client constructed", async () => {
|
|
622
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
623
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
624
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
625
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
626
|
+
|
|
627
|
+
const tools: ToolDescriptor[] = [
|
|
628
|
+
{ name: "search_code", description: "Search code" },
|
|
629
|
+
{ name: "delete_file", description: "Delete a file" },
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const result = await classifyTools(
|
|
633
|
+
{ tools, serverName: "github", serverDescription: "GitHub", mcpServerId: null },
|
|
634
|
+
directOptions,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
expect(result).toEqual([
|
|
638
|
+
{ tool_name: "search_code", requires_approval: true, message: "Execute search_code" },
|
|
639
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Execute delete_file" },
|
|
640
|
+
]);
|
|
641
|
+
expect(ChatOpenAI).not.toHaveBeenCalled();
|
|
642
|
+
expect(ChatAnthropic).not.toHaveBeenCalled();
|
|
643
|
+
expect(mockInvoke).not.toHaveBeenCalled();
|
|
644
|
+
// The message must name the model and provider (classification's
|
|
645
|
+
// provider follows the economy model, not the operator's key) and
|
|
646
|
+
// carry the predicate's remedy text.
|
|
647
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("'gpt-4o-mini' (openai)"));
|
|
648
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("OPENAI_API_KEY"));
|
|
649
|
+
warn.mockRestore();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("uses the provider key directly — no proxy baseURL, no Authorization header", async () => {
|
|
653
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct-test");
|
|
654
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
655
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
656
|
+
|
|
657
|
+
mockInvoke.mockResolvedValueOnce({
|
|
658
|
+
approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const result = await classifyTools(
|
|
662
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
663
|
+
directOptions,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
expect(result).toEqual([]);
|
|
667
|
+
const args = vi.mocked(ChatOpenAI).mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
668
|
+
expect(args.apiKey).toBe("sk-direct-test");
|
|
669
|
+
// The bug this suite pins: the gRPC control-plane endpoint must never
|
|
670
|
+
// reappear as a baseURL/Authorization transport override.
|
|
671
|
+
expect(args).not.toHaveProperty("configuration");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("routes an Anthropic economy model directly through ChatAnthropic with the real key", async () => {
|
|
675
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "sk-ant-direct");
|
|
676
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
677
|
+
const { getSummarizationModel } = await import("../../shared/model-registry.js");
|
|
678
|
+
vi.mocked(getSummarizationModel).mockResolvedValueOnce("claude-haiku-4.5");
|
|
679
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
680
|
+
|
|
681
|
+
mockInvoke.mockResolvedValueOnce({
|
|
682
|
+
approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await classifyTools(
|
|
686
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
687
|
+
directOptions,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const args = vi.mocked(ChatAnthropic).mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
691
|
+
expect(args.apiKey).toBe("sk-ant-direct");
|
|
692
|
+
expect(args).not.toHaveProperty("clientOptions");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("defers an un-inferable economy model to the per-batch fail-closed path", async () => {
|
|
696
|
+
// Real scenario: the registry-empty fallback returns the primary model
|
|
697
|
+
// verbatim (model-registry.ts), whatever its name. The pre-check must
|
|
698
|
+
// not guess a provider — the batch path owns the precise error.
|
|
699
|
+
const { getSummarizationModel } = await import("../../shared/model-registry.js");
|
|
700
|
+
vi.mocked(getSummarizationModel).mockResolvedValueOnce("mystery-model");
|
|
701
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
702
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
703
|
+
|
|
704
|
+
const result = await classifyTools(
|
|
705
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
706
|
+
directOptions,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result).toEqual([
|
|
710
|
+
{ tool_name: "t", requires_approval: true, message: "Execute t" },
|
|
711
|
+
]);
|
|
712
|
+
expect(error).toHaveBeenCalled();
|
|
713
|
+
error.mockRestore();
|
|
714
|
+
});
|
|
715
|
+
});
|
|
600
716
|
});
|
|
601
717
|
|
|
602
718
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression pin for issue #239's hang mechanism, against the REAL MCP client
|
|
3
|
+
* stack (no @langchain/mcp-adapters mock — that's why this lives in its own
|
|
4
|
+
* file, apart from discover-mcp-server.test.ts's mocked suite).
|
|
5
|
+
*
|
|
6
|
+
* The monday.com failure shape: the endpoint answers the streamable-HTTP
|
|
7
|
+
* initialize POST with a 4xx, mcp-adapters automatically falls back to SSE at
|
|
8
|
+
* the same URL, the endpoint accepts the stream and never sends the legacy
|
|
9
|
+
* `endpoint` event — and the MCP SDK's SSEClientTransport has no timer of its
|
|
10
|
+
* own, so initialization hangs forever. Before the transport-aware init bound,
|
|
11
|
+
* that hang rode until Temporal killed the activity opaquely; this test pins
|
|
12
|
+
* that discovery now fails fast (in fake time) with an error that names the
|
|
13
|
+
* endpoint.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
16
|
+
import { createServer, type Server } from "node:http";
|
|
17
|
+
|
|
18
|
+
vi.mock("../../idle-watchdog.js", () => ({
|
|
19
|
+
activityStarted: vi.fn(),
|
|
20
|
+
activityFinished: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// classifyHttpOAuthFailure re-probes the endpoint on the failure path; keep it
|
|
24
|
+
// deterministic here (its own behavior is covered by mcp-oauth-detect tests).
|
|
25
|
+
vi.mock("../../shared/mcp-oauth-detect.js", () => ({
|
|
26
|
+
detectOAuthChallenge: vi.fn().mockResolvedValue(null),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe("discovery against a 4xx-then-silent-SSE endpoint (issue #239)", () => {
|
|
30
|
+
let server: Server;
|
|
31
|
+
let url: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
server = createServer((req, res) => {
|
|
35
|
+
if (req.method === "POST") {
|
|
36
|
+
// monday-shaped: reject the streamable-HTTP initialize outright.
|
|
37
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
38
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// The SSE fallback: accept the stream, never send an `endpoint` event.
|
|
42
|
+
res.writeHead(200, {
|
|
43
|
+
"Content-Type": "text/event-stream",
|
|
44
|
+
"Cache-Control": "no-cache",
|
|
45
|
+
Connection: "keep-alive",
|
|
46
|
+
});
|
|
47
|
+
res.write(": silent stream\n\n");
|
|
48
|
+
});
|
|
49
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
50
|
+
const address = server.address();
|
|
51
|
+
if (typeof address === "string" || address === null) throw new Error("no port");
|
|
52
|
+
url = `http://127.0.0.1:${address.port}/mcp`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
// closeAllConnections: the hung SSE stream is a live socket that would
|
|
57
|
+
// otherwise keep server.close() (and the vitest worker) waiting.
|
|
58
|
+
server.closeAllConnections();
|
|
59
|
+
server.close();
|
|
60
|
+
vi.useRealTimers();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fails within the HTTP init bound with an endpoint-naming error", async () => {
|
|
64
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
65
|
+
|
|
66
|
+
const stigmerClient = {
|
|
67
|
+
getMcpServer: vi.fn().mockResolvedValue({
|
|
68
|
+
metadata: { slug: "monday", id: "mcp-monday" },
|
|
69
|
+
spec: {
|
|
70
|
+
serverType: {
|
|
71
|
+
case: "http",
|
|
72
|
+
value: { url, headers: {}, queryParams: {}, timeoutSeconds: 0 },
|
|
73
|
+
},
|
|
74
|
+
env: {},
|
|
75
|
+
pinnedToolApprovals: [],
|
|
76
|
+
},
|
|
77
|
+
status: undefined,
|
|
78
|
+
}),
|
|
79
|
+
getExecutionContextByExecutionId: vi.fn(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// shouldAdvanceTime keeps real I/O flowing (the POST → 405 → SSE fallback
|
|
83
|
+
// happens over real sockets) while letting the test jump the 60s bound.
|
|
84
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
85
|
+
|
|
86
|
+
const promise = discoverMcpServer(
|
|
87
|
+
{ mcpServerId: "mcp-monday" },
|
|
88
|
+
{ stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden" },
|
|
89
|
+
);
|
|
90
|
+
// Attach the rejection expectation BEFORE advancing so the rejection is
|
|
91
|
+
// never momentarily unhandled.
|
|
92
|
+
const expectation = expect(promise).rejects.toThrow(
|
|
93
|
+
new RegExp(`at ${url.replaceAll(".", "\\.")}.*did not complete MCP initialization`, "s"),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Give the real client a beat to reach the hang (POST + fallback), then
|
|
97
|
+
// jump past the 30s HTTP bound in fake time.
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
99
|
+
await vi.advanceTimersByTimeAsync(31_000);
|
|
100
|
+
|
|
101
|
+
await expectation;
|
|
102
|
+
}, 15_000);
|
|
103
|
+
});
|