@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
|
@@ -22,7 +22,7 @@ import { createHash } from "node:crypto";
|
|
|
22
22
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
23
23
|
import { activityStarted, activityFinished } from "../idle-watchdog.js";
|
|
24
24
|
import { StigmerClient } from "../client/stigmer-client.js";
|
|
25
|
-
import { mcpServerToResolved } from "../shared/mcp-resolver.js";
|
|
25
|
+
import { mcpServerToResolved, type ResolvedMcpServer } from "../shared/mcp-resolver.js";
|
|
26
26
|
import { toMcpClientConfig } from "../shared/mcp-manager.js";
|
|
27
27
|
import {
|
|
28
28
|
assertTransportAllowed,
|
|
@@ -31,11 +31,44 @@ import {
|
|
|
31
31
|
} from "../shared/mcp-transport-guard.js";
|
|
32
32
|
import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
|
|
33
33
|
import { injectAnonymousCallerIdentityForDiscovery } from "../shared/caller-identity.js";
|
|
34
|
+
import { startHeartbeat } from "../shared/heartbeat.js";
|
|
34
35
|
import { withTimeout } from "../shared/with-timeout.js";
|
|
35
36
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
37
|
+
import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
36
38
|
import type { Config } from "../config.js";
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* MCP session-init bounds, per transport (issue #239).
|
|
42
|
+
*
|
|
43
|
+
* A remote endpoint that accepts a connection but never completes the MCP
|
|
44
|
+
* handshake would otherwise hang initialization forever: the MCP SDK's SSE
|
|
45
|
+
* transport resolves only on the server's `endpoint` event, with no timer of
|
|
46
|
+
* its own, and mcp-adapters silently falls back to SSE whenever the
|
|
47
|
+
* streamable-HTTP POST is answered with a 4xx (monday.com's endpoint did
|
|
48
|
+
* exactly this — 4xx on POST, then a silently-open SSE stream).
|
|
49
|
+
*
|
|
50
|
+
* HTTP endpoints get a short bound: there is nothing to install or compile,
|
|
51
|
+
* so a healthy endpoint completes the handshake in seconds — a short bound is
|
|
52
|
+
* what converts the silent hang into a fast, actionable failure (with room for
|
|
53
|
+
* the 10s OAuth re-probe on the failure path). stdio servers keep the generous
|
|
54
|
+
* bound because their first run may compile or install packages (`go run`,
|
|
55
|
+
* `npx`) — the cold-start case (issue #243). The OSS server's connect-workflow
|
|
56
|
+
* run timeout (connectTimeout, controller/connect.go) is derived from the
|
|
57
|
+
* stdio bound + the classification floor, so both errors stay reachable.
|
|
58
|
+
*/
|
|
59
|
+
const HTTP_INIT_TIMEOUT_MS = 30_000;
|
|
60
|
+
const STDIO_INIT_TIMEOUT_MS = 270_000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The server-side secret-redaction sentinel. Byte-for-byte the value both
|
|
64
|
+
* editions substitute for secret values a caller may not decrypt
|
|
65
|
+
* (`SecretEncryptionService.REDACTED_MARKER` in stigmer-cloud,
|
|
66
|
+
* `RedactedMarker` in the OSS Go server). Discovery dialing an
|
|
67
|
+
* endpoint with a redacted credential is guaranteed to fail confusingly
|
|
68
|
+
* (e.g. `Authorization: Bearer ***REDACTED***` → 401 → SSE-fallback limbo),
|
|
69
|
+
* so it is refused up front with an actionable error instead.
|
|
70
|
+
*/
|
|
71
|
+
const REDACTED_MARKER = "***REDACTED***";
|
|
39
72
|
|
|
40
73
|
const PLATFORM_INJECTABLE_MAP: Record<string, string> = {
|
|
41
74
|
STIGMER_SERVER_ADDRESS: "STIGMER_MCP_PUBLIC_ENDPOINT",
|
|
@@ -95,6 +128,28 @@ export interface ToolApprovalDict {
|
|
|
95
128
|
message: string;
|
|
96
129
|
}
|
|
97
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Raised when the credentials a server declares could not be delivered to
|
|
133
|
+
* discovery — the ExecutionContext read failed, came back empty, or returned
|
|
134
|
+
* redacted values (issue #239).
|
|
135
|
+
*
|
|
136
|
+
* Exists because the alternative is strictly worse: proceeding without the
|
|
137
|
+
* declared credentials either dies later as an opaque
|
|
138
|
+
* PlaceholderResolutionError (when a header/arg templates the missing var) or
|
|
139
|
+
* dials the endpoint with a garbage credential and strands the connect in the
|
|
140
|
+
* 4xx → SSE-fallback limbo this file's init bounds exist to contain. Failing
|
|
141
|
+
* here names the ROOT cause — credential delivery — instead of its downstream
|
|
142
|
+
* symptom. The message is user-facing and self-contained: it survives the
|
|
143
|
+
* Temporal boundary and the Go/Java connect wrappers include it in the
|
|
144
|
+
* user-facing failure text.
|
|
145
|
+
*/
|
|
146
|
+
export class CredentialResolutionError extends Error {
|
|
147
|
+
constructor(message: string) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.name = "CredentialResolutionError";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
98
153
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
154
|
// Tools Fingerprint (pure, deterministic, safe in workflow code)
|
|
100
155
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -242,14 +297,15 @@ export async function discoverMcpServer(
|
|
|
242
297
|
const slug = mcpServer.metadata?.slug || mcpServerId;
|
|
243
298
|
const previousState = extractPreviousState(mcpServer);
|
|
244
299
|
|
|
300
|
+
const declaredEnv = mcpServer.spec.env ?? {};
|
|
245
301
|
const envVars = await resolveEnvVarsForDiscovery(
|
|
246
302
|
stigmerClient,
|
|
247
303
|
executionContextId ?? null,
|
|
304
|
+
slug,
|
|
305
|
+
declaredEnv,
|
|
248
306
|
);
|
|
249
307
|
|
|
250
|
-
const declaredEnvKeys =
|
|
251
|
-
? new Set(Object.keys(mcpServer.spec.env))
|
|
252
|
-
: new Set<string>();
|
|
308
|
+
const declaredEnvKeys = new Set(Object.keys(declaredEnv));
|
|
253
309
|
const platformEnv = injectPlatformEnv(declaredEnvKeys, envVars);
|
|
254
310
|
|
|
255
311
|
// Discovery runs with no session, so every declared caller-identity key
|
|
@@ -277,6 +333,7 @@ export async function discoverMcpServer(
|
|
|
277
333
|
const { tools, resourceTemplates } = await connectAndDiscover(
|
|
278
334
|
slug,
|
|
279
335
|
connectionConfig,
|
|
336
|
+
resolved,
|
|
280
337
|
);
|
|
281
338
|
|
|
282
339
|
const newFp = toolsFingerprint(tools);
|
|
@@ -300,51 +357,144 @@ export async function discoverMcpServer(
|
|
|
300
357
|
// Env Var Resolution
|
|
301
358
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Resolve the discovery env from the connect flow's ExecutionContext,
|
|
362
|
+
* failing CLOSED when a server's declared credentials cannot be delivered.
|
|
363
|
+
*
|
|
364
|
+
* The strictness is keyed on whether the server declares any NON-OPTIONAL
|
|
365
|
+
* env var ("credentials expected"):
|
|
366
|
+
*
|
|
367
|
+
* - Credentials expected + the EC read errors, or the EC is missing/empty →
|
|
368
|
+
* {@link CredentialResolutionError}. The backend only creates a connect EC
|
|
369
|
+
* when it resolved credentials to deliver, so an unreadable or empty EC is
|
|
370
|
+
* a delivery failure (auth/scope refusal, transient backend error), never a
|
|
371
|
+
* normal state. Limping ahead was issue #239's failure mode: discovery died
|
|
372
|
+
* later as an opaque PlaceholderResolutionError or a doomed dial.
|
|
373
|
+
* - Any delivered value equal to the redaction sentinel →
|
|
374
|
+
* {@link CredentialResolutionError}, regardless of optionality. A redacted
|
|
375
|
+
* value means the server-side decrypt gate refused THIS runner's credential
|
|
376
|
+
* class/scope and fell closed to redaction — dialing with the literal
|
|
377
|
+
* sentinel can only produce a misleading 401.
|
|
378
|
+
* - No non-optional declarations → the old lenient path (warn and continue):
|
|
379
|
+
* servers declaring nothing (or only optional/injected keys like the
|
|
380
|
+
* caller-identity family) legitimately discover without an EC.
|
|
381
|
+
*/
|
|
303
382
|
async function resolveEnvVarsForDiscovery(
|
|
304
383
|
client: StigmerClient,
|
|
305
384
|
executionContextId: string | null,
|
|
385
|
+
slug: string,
|
|
386
|
+
declaredEnv: Record<string, EnvVarDeclaration>,
|
|
306
387
|
): Promise<Record<string, string>> {
|
|
388
|
+
const requiredKeys = Object.entries(declaredEnv)
|
|
389
|
+
.filter(([, decl]) => !decl.optional)
|
|
390
|
+
.map(([key]) => key);
|
|
391
|
+
const credentialsExpected = requiredKeys.length > 0;
|
|
392
|
+
|
|
307
393
|
if (!executionContextId) return {};
|
|
308
394
|
|
|
395
|
+
let execCtx: Awaited<ReturnType<typeof client.getExecutionContextByExecutionId>>;
|
|
309
396
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`
|
|
317
|
-
`
|
|
397
|
+
execCtx = await client.getExecutionContextByExecutionId(executionContextId);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
400
|
+
if (credentialsExpected) {
|
|
401
|
+
throw new CredentialResolutionError(
|
|
402
|
+
`Could not resolve the credentials MCP server '${slug}' requires ` +
|
|
403
|
+
`(${requiredKeys.join(", ")}): the connect credential store was ` +
|
|
404
|
+
`unreadable (${cause}). This is a platform-side delivery failure, ` +
|
|
405
|
+
`not a problem with your credentials — retry the connect, and if it ` +
|
|
406
|
+
`persists, re-run the OAuth sign-in or re-enter the credentials.`,
|
|
318
407
|
);
|
|
319
|
-
return {};
|
|
320
408
|
}
|
|
409
|
+
console.warn(
|
|
410
|
+
`[DiscoverMcpServer] Failed to resolve ExecutionContext '${executionContextId}': ` +
|
|
411
|
+
`${cause}`,
|
|
412
|
+
);
|
|
413
|
+
return {};
|
|
414
|
+
}
|
|
321
415
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
416
|
+
const data = execCtx?.spec?.data ?? {};
|
|
417
|
+
if (Object.keys(data).length === 0) {
|
|
418
|
+
if (credentialsExpected) {
|
|
419
|
+
throw new CredentialResolutionError(
|
|
420
|
+
`MCP server '${slug}' requires ${requiredKeys.join(", ")}, but the ` +
|
|
421
|
+
`connect flow delivered no credentials. Re-run the OAuth sign-in ` +
|
|
422
|
+
`(or re-enter the credentials) and connect again.`,
|
|
423
|
+
);
|
|
325
424
|
}
|
|
326
|
-
|
|
327
|
-
console.log(
|
|
328
|
-
`[DiscoverMcpServer] Resolved ${Object.keys(envVars).length} env var(s) ` +
|
|
329
|
-
`from ExecutionContext '${executionContextId}'`,
|
|
330
|
-
);
|
|
331
|
-
return envVars;
|
|
332
|
-
} catch (err) {
|
|
333
425
|
console.warn(
|
|
334
|
-
`[DiscoverMcpServer]
|
|
335
|
-
|
|
426
|
+
`[DiscoverMcpServer] ExecutionContext '${executionContextId}' not found ` +
|
|
427
|
+
`or empty — MCP server may not require environment variables`,
|
|
336
428
|
);
|
|
337
429
|
return {};
|
|
338
430
|
}
|
|
431
|
+
|
|
432
|
+
const redactedKeys = Object.entries(data)
|
|
433
|
+
.filter(([, execValue]) => execValue.value === REDACTED_MARKER)
|
|
434
|
+
.map(([key]) => key);
|
|
435
|
+
if (redactedKeys.length > 0) {
|
|
436
|
+
throw new CredentialResolutionError(
|
|
437
|
+
`The credentials for MCP server '${slug}' were delivered redacted ` +
|
|
438
|
+
`(${redactedKeys.join(", ")}): the platform refused to decrypt them ` +
|
|
439
|
+
`for this runner. This is a platform-side authorization failure — ` +
|
|
440
|
+
`retry the connect, and report it if it persists.`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const envVars: Record<string, string> = {};
|
|
445
|
+
for (const [key, execValue] of Object.entries(data)) {
|
|
446
|
+
envVars[key] = execValue.value;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log(
|
|
450
|
+
`[DiscoverMcpServer] Resolved ${Object.keys(envVars).length} env var(s) ` +
|
|
451
|
+
`from ExecutionContext '${executionContextId}'`,
|
|
452
|
+
);
|
|
453
|
+
return envVars;
|
|
339
454
|
}
|
|
340
455
|
|
|
341
456
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
342
457
|
// MCP Connection + Enumeration
|
|
343
458
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
344
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Pick the session-init bound for a resolved server's transport.
|
|
462
|
+
*
|
|
463
|
+
* Exported for tests; pure so the timeout choice (the load-bearing half of
|
|
464
|
+
* the issue-#239 fix) can be pinned without wiring a slow clock.
|
|
465
|
+
*/
|
|
466
|
+
export function initTimeoutMsFor(connectionType: ResolvedMcpServer["connectionType"]): number {
|
|
467
|
+
return connectionType === "stdio" ? STDIO_INIT_TIMEOUT_MS : HTTP_INIT_TIMEOUT_MS;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The user-facing message for a session-init timeout. Names the endpoint (or
|
|
472
|
+
* command) so the failure is diagnosable from the message alone, and explains
|
|
473
|
+
* the known silent-hang shape for HTTP endpoints — issue #239's mechanism.
|
|
474
|
+
*/
|
|
475
|
+
export function initTimeoutMessageFor(slug: string, resolved: ResolvedMcpServer): string {
|
|
476
|
+
const seconds = Math.round(initTimeoutMsFor(resolved.connectionType) / 1000);
|
|
477
|
+
if (resolved.connectionType === "stdio") {
|
|
478
|
+
return (
|
|
479
|
+
`MCP server '${slug}' (command: ${resolved.command}) did not complete ` +
|
|
480
|
+
`MCP initialization within ${seconds}s. If this server requires ` +
|
|
481
|
+
`compilation or package installation on first run (e.g. go run, npx), ` +
|
|
482
|
+
`the cold start may have exceeded the discovery timeout.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return (
|
|
486
|
+
`MCP server '${slug}' at ${resolved.url} did not complete MCP ` +
|
|
487
|
+
`initialization within ${seconds}s. The endpoint accepted the connection ` +
|
|
488
|
+
`but never finished the handshake — commonly an endpoint that rejects ` +
|
|
489
|
+
`streamable HTTP while leaving its SSE fallback stream silently open. ` +
|
|
490
|
+
`Verify the URL points at a live streamable-HTTP MCP endpoint.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
345
494
|
async function connectAndDiscover(
|
|
346
495
|
slug: string,
|
|
347
496
|
connectionConfig: ReturnType<typeof toMcpClientConfig>,
|
|
497
|
+
resolved: ResolvedMcpServer,
|
|
348
498
|
): Promise<{
|
|
349
499
|
tools: DiscoveredToolResult[];
|
|
350
500
|
resourceTemplates: DiscoveredResourceTemplateResult[];
|
|
@@ -354,12 +504,10 @@ async function connectAndDiscover(
|
|
|
354
504
|
const resourceTemplates: DiscoveredResourceTemplateResult[] = [];
|
|
355
505
|
|
|
356
506
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
`start may have exceeded the discovery timeout.`;
|
|
362
|
-
await withTimeout(SESSION_INIT_TIMEOUT_MS, timeoutMessage, async () => {
|
|
507
|
+
await withTimeout(
|
|
508
|
+
initTimeoutMsFor(resolved.connectionType),
|
|
509
|
+
() => initTimeoutMessageFor(slug, resolved),
|
|
510
|
+
async () => {
|
|
363
511
|
await client.initializeConnections();
|
|
364
512
|
|
|
365
513
|
const mcpClient = await client.getClient(slug);
|
|
@@ -461,12 +609,22 @@ export function createDiscoverMcpServerActivities(config: Config) {
|
|
|
461
609
|
input: DiscoverMcpServerInput,
|
|
462
610
|
): Promise<DiscoverMcpServerOutput> => {
|
|
463
611
|
activityStarted();
|
|
612
|
+
// The connect workflow proxies this activity with a 60s heartbeatTimeout,
|
|
613
|
+
// so it MUST heartbeat: before this loop existed, any discovery slower
|
|
614
|
+
// than 60s (stdio cold start — issue #243) or wedged on a silent remote
|
|
615
|
+
// (issue #239) was killed by Temporal with an opaque heartbeat timeout
|
|
616
|
+
// instead of reaching this file's actionable init-timeout errors.
|
|
617
|
+
const hb = startHeartbeat(15_000, () => ({
|
|
618
|
+
phase: "discovering_mcp_server",
|
|
619
|
+
mcpServerId: input.mcpServerId,
|
|
620
|
+
}));
|
|
464
621
|
try {
|
|
465
622
|
return await discoverMcpServer(input, {
|
|
466
623
|
stigmerClient,
|
|
467
624
|
transportPosture: resolveMcpTransportPosture(config.mode),
|
|
468
625
|
});
|
|
469
626
|
} finally {
|
|
627
|
+
hb.stop();
|
|
470
628
|
activityFinished();
|
|
471
629
|
}
|
|
472
630
|
},
|
|
@@ -108,6 +108,14 @@ export interface CursorHookHarnessOptions {
|
|
|
108
108
|
* non-pausing "unattended" kind and the adapt-and-explain agent message.
|
|
109
109
|
*/
|
|
110
110
|
unattendedSkip?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Per-server enabled_tools allow-lists (issue #350) — restricted servers
|
|
113
|
+
* only. The hook's manifest arm denies a beforeMCPExecution call whose
|
|
114
|
+
* mcp_server_name is listed here with a tool name outside its list (kind
|
|
115
|
+
* "disabled", ahead of every approval bypass). hookMcp payloads carry
|
|
116
|
+
* mcp_server_name "srv".
|
|
117
|
+
*/
|
|
118
|
+
mcpServerEnabledTools?: Record<string, string[]>;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/**
|
|
@@ -170,6 +178,7 @@ export function setupCursorHookHarness(opts: CursorHookHarnessOptions = {}): Cur
|
|
|
170
178
|
opts.captureIgnored ?? false,
|
|
171
179
|
opts.gitWorkspace ?? true,
|
|
172
180
|
opts.unattendedSkip ?? false,
|
|
181
|
+
opts.mcpServerEnabledTools ?? {},
|
|
173
182
|
);
|
|
174
183
|
writeFileSync(statePath, JSON.stringify(state), "utf-8");
|
|
175
184
|
}
|
|
@@ -219,6 +219,20 @@ describe("buildApprovalState", () => {
|
|
|
219
219
|
expect((state as unknown as Record<string, unknown>).builtInGatedList).toBeUndefined();
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it("carries the enabled_tools manifest (issue #350) and defaults it to empty", () => {
|
|
223
|
+
// Default: no restriction — the hook's disabled arm is inert.
|
|
224
|
+
const bare = buildApprovalState(mcpPolicies, false, new Set());
|
|
225
|
+
expect(bare.mcpServerEnabledTools).toEqual({});
|
|
226
|
+
|
|
227
|
+
// Restricted servers ride through verbatim for the hook's server-scoped
|
|
228
|
+
// membership check.
|
|
229
|
+
const restricted = buildApprovalState(
|
|
230
|
+
mcpPolicies, false, new Set(), undefined, false, false, true, false,
|
|
231
|
+
{ planton: ["get_cloud_resource"] },
|
|
232
|
+
);
|
|
233
|
+
expect(restricted.mcpServerEnabledTools).toEqual({ planton: ["get_cloud_resource"] });
|
|
234
|
+
});
|
|
235
|
+
|
|
222
236
|
it("emits the CONTENT token when a grant carries a content digest", () => {
|
|
223
237
|
const grants = [{ toolName: "edit", mcpServerSlug: "", key: "write", salient: "/x/gated.txt", contentDigest: "deadbeef", sourceToolCallId: "consent-1" }];
|
|
224
238
|
const state = buildApprovalState(mcpPolicies, false, new Set(), grants);
|
|
@@ -114,6 +114,59 @@ describe("resolveAttachments", () => {
|
|
|
114
114
|
expect(readFileSync(join(platformDir, "inputs", "data.csv"), "utf-8")).toBe("a,b,c");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("uniquifies duplicate filenames on the storage branch — neither file's bytes are lost (issue #364)", async () => {
|
|
118
|
+
// Before the fix this branch had no collision check and the second write
|
|
119
|
+
// silently overwrote the first.
|
|
120
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
121
|
+
await storage.upload("attachments/01AAA/report.pdf", Buffer.from("first bytes"), "application/pdf");
|
|
122
|
+
await storage.upload("attachments/01BBB/report.pdf", Buffer.from("second bytes"), "application/pdf");
|
|
123
|
+
|
|
124
|
+
const result = await resolveAttachments(
|
|
125
|
+
[
|
|
126
|
+
makeAttachment({ filename: "report.pdf", storageKey: "attachments/01AAA/report.pdf" }),
|
|
127
|
+
makeAttachment({ filename: "report.pdf", storageKey: "attachments/01BBB/report.pdf" }),
|
|
128
|
+
],
|
|
129
|
+
options({ storage }),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(result).toEqual([
|
|
133
|
+
{ filename: "report.pdf", relativePath: ".stigmer/inputs/report.pdf" },
|
|
134
|
+
{
|
|
135
|
+
filename: "report-2.pdf",
|
|
136
|
+
relativePath: ".stigmer/inputs/report-2.pdf",
|
|
137
|
+
renamedFrom: "report.pdf",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
expect(readFileSync(join(platformDir, "inputs", "report.pdf"), "utf-8")).toBe("first bytes");
|
|
141
|
+
expect(readFileSync(join(platformDir, "inputs", "report-2.pdf"), "utf-8")).toBe("second bytes");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("uniquifies duplicate filenames across the local and storage branches (one shared taken-set)", async () => {
|
|
145
|
+
const srcPath = join(workspaceDir, "notes.md");
|
|
146
|
+
writeFileSync(srcPath, "local copy");
|
|
147
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
148
|
+
await storage.upload("attachments/01ABC/notes.md", Buffer.from("uploaded copy"), "text/markdown");
|
|
149
|
+
|
|
150
|
+
const result = await resolveAttachments(
|
|
151
|
+
[
|
|
152
|
+
makeAttachment({ filename: "notes.md", storageKey: "", localPath: srcPath }),
|
|
153
|
+
makeAttachment({ filename: "notes.md", storageKey: "attachments/01ABC/notes.md" }),
|
|
154
|
+
],
|
|
155
|
+
options({ storage }),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual([
|
|
159
|
+
{ filename: "notes.md", relativePath: ".stigmer/inputs/notes.md" },
|
|
160
|
+
{
|
|
161
|
+
filename: "notes-2.md",
|
|
162
|
+
relativePath: ".stigmer/inputs/notes-2.md",
|
|
163
|
+
renamedFrom: "notes.md",
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
expect(readFileSync(join(platformDir, "inputs", "notes.md"), "utf-8")).toBe("local copy");
|
|
167
|
+
expect(readFileSync(join(platformDir, "inputs", "notes-2.md"), "utf-8")).toBe("uploaded copy");
|
|
168
|
+
});
|
|
169
|
+
|
|
117
170
|
it("ignores localPath in cloud mode and downloads by storage key", async () => {
|
|
118
171
|
const { storage } = makeInMemoryArtifactStorage();
|
|
119
172
|
await storage.upload("attachments/01ABC/plan.md", Buffer.from("from storage"), "text/markdown");
|
|
@@ -313,6 +366,45 @@ describe("resolveAttachments", () => {
|
|
|
313
366
|
expect(readFileSync(join(platformDir, "inputs", "notes.txt"), "utf-8")).toBe("plain text");
|
|
314
367
|
});
|
|
315
368
|
|
|
369
|
+
it("degrades a storage-key image with model_no_vision on a blind model, file intact", async () => {
|
|
370
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
371
|
+
await storage.upload("attachments/01ABC/photo.png", PNG_BYTES, "image/png");
|
|
372
|
+
|
|
373
|
+
const [resolved] = await resolveAttachments(
|
|
374
|
+
[makeAttachment({ filename: "photo.png", storageKey: "attachments/01ABC/photo.png", contentType: "image/png" })],
|
|
375
|
+
options({
|
|
376
|
+
storage,
|
|
377
|
+
visionBudget: new VisionBudget(CURSOR_VISION_PROFILE, { modelVision: false }),
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(resolved.vision).toBeUndefined();
|
|
382
|
+
expect(resolved.visionDegraded).toBe("model_no_vision");
|
|
383
|
+
expect(readFileSync(join(platformDir, "inputs", "photo.png")).equals(PNG_BYTES)).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("reports model_no_vision (never too_large) for an oversized localPath image on a blind model", async () => {
|
|
387
|
+
// The blind check must pre-empt the stat-based oversize fast path:
|
|
388
|
+
// too_large's "resend smaller" advice would be wrong for a model that
|
|
389
|
+
// cannot see any image.
|
|
390
|
+
const srcPath = join(workspaceDir, "big.png");
|
|
391
|
+
writeFileSync(srcPath, PNG_BYTES);
|
|
392
|
+
|
|
393
|
+
const [resolved] = await resolveAttachments(
|
|
394
|
+
[makeAttachment({ filename: "big.png", storageKey: "", localPath: srcPath, contentType: "image/png" })],
|
|
395
|
+
options({
|
|
396
|
+
visionBudget: new VisionBudget(CURSOR_VISION_PROFILE, {
|
|
397
|
+
maxImageBytes: PNG_BYTES.length - 1,
|
|
398
|
+
modelVision: false,
|
|
399
|
+
}),
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(resolved.vision).toBeUndefined();
|
|
404
|
+
expect(resolved.visionDegraded).toBe("model_no_vision");
|
|
405
|
+
expect(readFileSync(join(platformDir, "inputs", "big.png")).equals(PNG_BYTES)).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
316
408
|
it("selects greedily in attachment order when the total budget cuts off", async () => {
|
|
317
409
|
const { storage } = makeInMemoryArtifactStorage();
|
|
318
410
|
await storage.upload("attachments/01A/a.png", PNG_BYTES, "image/png");
|
|
@@ -47,7 +47,7 @@ function input(overrides: Partial<BuildPromptInput>): BuildPromptInput {
|
|
|
47
47
|
subAgents: [],
|
|
48
48
|
workspaceDirs: ["/tmp/ws"],
|
|
49
49
|
workspaceFileRefs: [],
|
|
50
|
-
|
|
50
|
+
attachments: [],
|
|
51
51
|
pendingApprovals: [],
|
|
52
52
|
...overrides,
|
|
53
53
|
};
|
|
@@ -103,6 +103,10 @@ describe("buildPrompt", () => {
|
|
|
103
103
|
expect(prompt).toContain("<available_datastores>");
|
|
104
104
|
expect(prompt).toContain("- clinic");
|
|
105
105
|
expect(prompt).toContain("describe_datastore");
|
|
106
|
+
// The standing failure-disclosure instruction (issue #325) is this
|
|
107
|
+
// harness's ONLY outage coverage: the Cursor SDK connects MCP itself,
|
|
108
|
+
// so the runner can never reconcile the live roster here.
|
|
109
|
+
expect(prompt).toContain("do not answer from memory");
|
|
106
110
|
});
|
|
107
111
|
|
|
108
112
|
it("omits the datastores section when the agent uses no datastores", () => {
|
|
@@ -269,7 +273,7 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
269
273
|
|
|
270
274
|
it("announces this turn's attachments to a resumed agent (per-execution value, never inherited)", () => {
|
|
271
275
|
const prompt = buildPrompt(
|
|
272
|
-
input({ ...RESUMED,
|
|
276
|
+
input({ ...RESUMED, attachments: [{ path: ".stigmer/inputs/lease.pdf" }] }),
|
|
273
277
|
);
|
|
274
278
|
expect(prompt).toContain("<input_files>");
|
|
275
279
|
expect(prompt).toContain("`.stigmer/inputs/lease.pdf`");
|
|
@@ -277,6 +281,23 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
277
281
|
expect(prompt.endsWith(USER_MESSAGE)).toBe(true);
|
|
278
282
|
});
|
|
279
283
|
|
|
284
|
+
it("discloses a duplicate-renamed attachment's original name (issue #364)", () => {
|
|
285
|
+
const prompt = buildPrompt(
|
|
286
|
+
input({
|
|
287
|
+
...RESUMED,
|
|
288
|
+
attachments: [
|
|
289
|
+
{ path: ".stigmer/inputs/report.pdf" },
|
|
290
|
+
{ path: ".stigmer/inputs/report-2.pdf", renamedFrom: "report.pdf" },
|
|
291
|
+
],
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
expect(prompt).toContain(
|
|
295
|
+
"- `.stigmer/inputs/report-2.pdf` (renamed from duplicate 'report.pdf')",
|
|
296
|
+
);
|
|
297
|
+
// The first file keeps a clean entry — no disclosure noise.
|
|
298
|
+
expect(prompt).toContain("- `.stigmer/inputs/report.pdf`\n");
|
|
299
|
+
});
|
|
300
|
+
|
|
280
301
|
it("keeps a resumed turn WITHOUT attachments byte-identical to the raw message (regression guard)", () => {
|
|
281
302
|
const prompt = buildPrompt(input({ ...RESUMED }));
|
|
282
303
|
expect(prompt).toBe(USER_MESSAGE);
|
|
@@ -286,7 +307,7 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
286
307
|
const prompt = buildPrompt(
|
|
287
308
|
input({
|
|
288
309
|
...RESUMED,
|
|
289
|
-
|
|
310
|
+
attachments: [{ path: ".stigmer/inputs/photo.jpg" }],
|
|
290
311
|
conversationCatchup: "User also said hello on the channel.",
|
|
291
312
|
}),
|
|
292
313
|
);
|
|
@@ -300,7 +321,7 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
300
321
|
const prompt = buildPrompt(
|
|
301
322
|
input({
|
|
302
323
|
...RESUMED,
|
|
303
|
-
|
|
324
|
+
attachments: [{ path: ".stigmer/inputs/a.jpg" }, { path: ".stigmer/inputs/big.png" }],
|
|
304
325
|
vision: {
|
|
305
326
|
inlineFilenames: ["a.jpg"],
|
|
306
327
|
notViewable: [{ path: ".stigmer/inputs/big.png", reason: "too_large" }],
|
|
@@ -316,7 +337,7 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
316
337
|
const prompt = buildPrompt(
|
|
317
338
|
input({
|
|
318
339
|
resolution: resolution("local", "created_first_execution"),
|
|
319
|
-
|
|
340
|
+
attachments: [{ path: ".stigmer/inputs/a.jpg" }],
|
|
320
341
|
vision: { inlineFilenames: ["a.jpg"], notViewable: [] },
|
|
321
342
|
}),
|
|
322
343
|
);
|
|
@@ -336,7 +357,7 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
|
|
|
336
357
|
message: "Write file: gated.txt",
|
|
337
358
|
}),
|
|
338
359
|
],
|
|
339
|
-
|
|
360
|
+
attachments: [{ path: ".stigmer/inputs/photo.jpg" }],
|
|
340
361
|
vision: { inlineFilenames: ["photo.jpg"], notViewable: [] },
|
|
341
362
|
}),
|
|
342
363
|
);
|
|
@@ -543,7 +564,10 @@ describe("formatImplementPlanSection", () => {
|
|
|
543
564
|
const PLAN_PATH = ".stigmer/inputs/plan.md";
|
|
544
565
|
|
|
545
566
|
it("wraps the attached-plan directive when the plan is among the attachments", () => {
|
|
546
|
-
const section = formatImplementPlanSection(true, [
|
|
567
|
+
const section = formatImplementPlanSection(true, [
|
|
568
|
+
{ path: PLAN_PATH },
|
|
569
|
+
{ path: ".stigmer/inputs/data.csv" },
|
|
570
|
+
]);
|
|
547
571
|
|
|
548
572
|
expect(section).toBeDefined();
|
|
549
573
|
expect(section!.startsWith("<implement_plan>")).toBe(true);
|
|
@@ -553,7 +577,9 @@ describe("formatImplementPlanSection", () => {
|
|
|
553
577
|
});
|
|
554
578
|
|
|
555
579
|
it("falls back to the conversation-plan directive when no plan attachment resolved", () => {
|
|
556
|
-
const section = formatImplementPlanSection(true, [
|
|
580
|
+
const section = formatImplementPlanSection(true, [
|
|
581
|
+
{ path: ".stigmer/inputs/data.csv" },
|
|
582
|
+
]);
|
|
557
583
|
|
|
558
584
|
expect(section).toBeDefined();
|
|
559
585
|
expect(section).not.toContain("plan.md");
|
|
@@ -561,12 +587,12 @@ describe("formatImplementPlanSection", () => {
|
|
|
561
587
|
});
|
|
562
588
|
|
|
563
589
|
it("returns undefined for an ordinary (non-build) execution", () => {
|
|
564
|
-
expect(formatImplementPlanSection(false, [PLAN_PATH])).toBeUndefined();
|
|
565
|
-
expect(formatImplementPlanSection(undefined, [PLAN_PATH])).toBeUndefined();
|
|
590
|
+
expect(formatImplementPlanSection(false, [{ path: PLAN_PATH }])).toBeUndefined();
|
|
591
|
+
expect(formatImplementPlanSection(undefined, [{ path: PLAN_PATH }])).toBeUndefined();
|
|
566
592
|
});
|
|
567
593
|
|
|
568
594
|
it("carries the plan-derived progress-tracking instruction (Tier 3)", () => {
|
|
569
|
-
const section = formatImplementPlanSection(true, [PLAN_PATH]);
|
|
595
|
+
const section = formatImplementPlanSection(true, [{ path: PLAN_PATH }]);
|
|
570
596
|
|
|
571
597
|
expect(section).toContain("to-do list");
|
|
572
598
|
expect(section).toContain("break the plan into");
|
|
@@ -577,7 +603,7 @@ describe("formatImplementPlanSection", () => {
|
|
|
577
603
|
input({
|
|
578
604
|
resolution: resolution("local", "created_first_execution"),
|
|
579
605
|
buildFromPlan: true,
|
|
580
|
-
|
|
606
|
+
attachments: [{ path: PLAN_PATH }],
|
|
581
607
|
}),
|
|
582
608
|
);
|
|
583
609
|
|
|
@@ -593,7 +619,7 @@ describe("formatImplementPlanSection", () => {
|
|
|
593
619
|
input({
|
|
594
620
|
resolution: resolution("local", "resumed_successfully"),
|
|
595
621
|
buildFromPlan: true,
|
|
596
|
-
|
|
622
|
+
attachments: [{ path: PLAN_PATH }],
|
|
597
623
|
}),
|
|
598
624
|
);
|
|
599
625
|
|
|
@@ -611,7 +637,7 @@ describe("formatImplementPlanSection", () => {
|
|
|
611
637
|
input({
|
|
612
638
|
resolution: resolution("local", "resumed_successfully"),
|
|
613
639
|
buildFromPlan: false,
|
|
614
|
-
|
|
640
|
+
attachments: [{ path: PLAN_PATH }],
|
|
615
641
|
}),
|
|
616
642
|
);
|
|
617
643
|
|