@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
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
toCursorImages,
|
|
65
65
|
type NotViewableEntry,
|
|
66
66
|
} from "../../shared/attachment-vision.js";
|
|
67
|
+
import { getModelVisionCapability } from "../../shared/model-registry.js";
|
|
67
68
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
68
69
|
import { DeltaEnricher } from "./delta-enricher.js";
|
|
69
70
|
import { TodoTracker } from "./todo-tracker.js";
|
|
@@ -83,6 +84,7 @@ import {
|
|
|
83
84
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
84
85
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
85
86
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
87
|
+
import { enabledToolsBySlug } from "../../shared/mcp-enabled-tools.js";
|
|
86
88
|
import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
|
|
87
89
|
import { resolveExecutionEnv } from "./env-resolver.js";
|
|
88
90
|
import { resolveBlueprint } from "./blueprint-resolver.js";
|
|
@@ -138,7 +140,7 @@ import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/
|
|
|
138
140
|
import { activityStarted, activityFinished } from "../../idle-watchdog.js";
|
|
139
141
|
import { normalizeActivityInput, type ExecuteActivityInput } from "../../shared/activity-input.js";
|
|
140
142
|
import { getCapturedRejection, clearCapturedRejection } from "./rejection-capture.js";
|
|
141
|
-
import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "./error-classifier.js";
|
|
143
|
+
import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent, extractRunErrorSources } from "./error-classifier.js";
|
|
142
144
|
import type { ClassifiedError } from "./error-classifier.js";
|
|
143
145
|
import { createAgent, createCloudAgent } from "./session-lifecycle.js";
|
|
144
146
|
import { setMaxListeners } from "node:events";
|
|
@@ -643,9 +645,21 @@ async function executeCursorInner(
|
|
|
643
645
|
// the messaging reach; undefined lets a cloud sandbox runner's
|
|
644
646
|
// ambient session-scoped token or OSS's no-auth apply). The
|
|
645
647
|
// attachment header falls back to the ambient credential where no
|
|
646
|
-
// exchange happens.
|
|
647
|
-
|
|
648
|
-
|
|
648
|
+
// exchange happens. Unlike the env read (which hard-fails on a broken
|
|
649
|
+
// exchange — secrets are load-bearing there), this exchange is
|
|
650
|
+
// opportunistic: every consumer below degrades to an empty answer by
|
|
651
|
+
// contract, and the server refuses the ambient fallback safely, so a
|
|
652
|
+
// failed exchange must not kill the run.
|
|
653
|
+
let exchangedRunnerToken: string | undefined;
|
|
654
|
+
try {
|
|
655
|
+
exchangedRunnerToken =
|
|
656
|
+
await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
|
|
657
|
+
} catch (err) {
|
|
658
|
+
console.warn(
|
|
659
|
+
"[execute-cursor] Scoped-token exchange failed for attachment/discovery " +
|
|
660
|
+
`reads; degrading to the ambient credential: ${err instanceof Error ? err.message : err}`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
649
663
|
const attachmentCredential = exchangedRunnerToken
|
|
650
664
|
?? config.stigmerTokenRef?.current
|
|
651
665
|
?? config.stigmerToken;
|
|
@@ -733,11 +747,11 @@ async function executeCursorInner(
|
|
|
733
747
|
// them as auto-approved); the global bypass empties the map entirely.
|
|
734
748
|
const leases = deriveActiveLeases(execution);
|
|
735
749
|
const globalBypass = leases.global;
|
|
736
|
-
|
|
737
|
-
|
|
750
|
+
// Layer-3 overrides ride each resolved server from its merged usage —
|
|
751
|
+
// see ResolvedMcpServer.toolApprovalOverrides (issue #349) — so there
|
|
752
|
+
// is no separate override input to pass here.
|
|
738
753
|
const mergedPolicies = mergeApprovalPolicies(
|
|
739
754
|
mcpResolution.resolvedServers,
|
|
740
|
-
agentOverrides,
|
|
741
755
|
leases,
|
|
742
756
|
);
|
|
743
757
|
heartbeat();
|
|
@@ -770,7 +784,13 @@ async function executeCursorInner(
|
|
|
770
784
|
// artifactStorage resolved for status offload above. The vision budget
|
|
771
785
|
// rides along so image attachments are selected for inline delivery while
|
|
772
786
|
// their bytes are already in hand (attachment-vision.ts owns all policy).
|
|
773
|
-
|
|
787
|
+
// The budget also carries the requested model's registry vision
|
|
788
|
+
// capability, looked up from the raw executionConfig name — full model
|
|
789
|
+
// validation (Phase 6) isn't needed for this, and ""/"default" (the Auto
|
|
790
|
+
// pool) resolves to unknown, which the policy treats as sighted.
|
|
791
|
+
const visionBudget = new VisionBudget(CURSOR_VISION_PROFILE, {
|
|
792
|
+
modelVision: await getModelVisionCapability(spec.executionConfig?.modelName ?? ""),
|
|
793
|
+
});
|
|
774
794
|
const attachmentResults = await resolveAttachments(spec.attachments, {
|
|
775
795
|
sessionId,
|
|
776
796
|
primaryWorkspaceDir,
|
|
@@ -778,7 +798,10 @@ async function executeCursorInner(
|
|
|
778
798
|
storage: artifactStorage,
|
|
779
799
|
visionBudget,
|
|
780
800
|
});
|
|
781
|
-
const
|
|
801
|
+
const attachmentEntries = attachmentResults.map((a) => ({
|
|
802
|
+
path: a.relativePath,
|
|
803
|
+
...(a.renamedFrom !== undefined ? { renamedFrom: a.renamedFrom } : {}),
|
|
804
|
+
}));
|
|
782
805
|
// Vision facts, derived once from the single resolution result: the
|
|
783
806
|
// images the model will see inline (in attachment order) and the ones
|
|
784
807
|
// that degraded to path-only, disclosed in the prompt.
|
|
@@ -899,6 +922,11 @@ async function executeCursorInner(
|
|
|
899
922
|
captureIgnored,
|
|
900
923
|
gitWorkspace,
|
|
901
924
|
isUnattendedApprovalMode(execution),
|
|
925
|
+
// The enabled_tools capability manifest (issue #350): restricted
|
|
926
|
+
// servers' allow-lists, enforced by the hook's "disabled" arm ahead of
|
|
927
|
+
// every approval bypass. The Cursor SDK config cannot hide a server's
|
|
928
|
+
// tools, so this deny-at-call is the harness's enforcement.
|
|
929
|
+
enabledToolsBySlug(mcpResolution.resolvedServers),
|
|
902
930
|
);
|
|
903
931
|
const hitlGate = await installHitlGate({
|
|
904
932
|
workspaceRoot: primaryWorkspaceDir,
|
|
@@ -1123,7 +1151,7 @@ async function executeCursorInner(
|
|
|
1123
1151
|
subAgents: blueprint.subAgents,
|
|
1124
1152
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1125
1153
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
1126
|
-
|
|
1154
|
+
attachments: attachmentEntries,
|
|
1127
1155
|
vision: visionPromptInfo,
|
|
1128
1156
|
pendingApprovals: adjudicatedApprovals,
|
|
1129
1157
|
appliedToolCallIds,
|
|
@@ -1714,12 +1742,10 @@ async function executeCursorInner(
|
|
|
1714
1742
|
status.phase = ExecutionPhase.EXECUTION_COMPLETED;
|
|
1715
1743
|
break;
|
|
1716
1744
|
case "error": {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
?? resultAny.reason;
|
|
1722
|
-
const sdkErrorStr = sdkError ? String(sdkError) : undefined;
|
|
1745
|
+
// Shape-aware extraction, NOT String(): the result's error fields are
|
|
1746
|
+
// structured at runtime often enough that a bare coercion showed users
|
|
1747
|
+
// "[object Object]" and shadowed every fallback source below (oss#299).
|
|
1748
|
+
const runErrorSources = extractRunErrorSources(result);
|
|
1723
1749
|
|
|
1724
1750
|
// The SDK frequently resolves run.wait() to a bare { status: "error" }
|
|
1725
1751
|
// while the real reason (e.g. the original grpc-status 12 routing
|
|
@@ -1731,7 +1757,8 @@ async function executeCursorInner(
|
|
|
1731
1757
|
if (capturedRejection) clearCapturedRejection(executionId);
|
|
1732
1758
|
|
|
1733
1759
|
const classified = synthesizeError({
|
|
1734
|
-
|
|
1760
|
+
sdkError: runErrorSources.sdkError,
|
|
1761
|
+
sdkResultFields: runErrorSources.sdkResultFields,
|
|
1735
1762
|
streamErrorMessage: turnState.streamErrorMessage,
|
|
1736
1763
|
capturedRejection,
|
|
1737
1764
|
conversationErrorText,
|
|
@@ -1782,7 +1809,7 @@ async function executeCursorInner(
|
|
|
1782
1809
|
subAgents: blueprint.subAgents,
|
|
1783
1810
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1784
1811
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
1785
|
-
|
|
1812
|
+
attachments: attachmentEntries,
|
|
1786
1813
|
vision: visionPromptInfo,
|
|
1787
1814
|
pendingApprovals: adjudicatedApprovals,
|
|
1788
1815
|
interactionMode,
|
|
@@ -1849,8 +1876,14 @@ async function executeCursorInner(
|
|
|
1849
1876
|
|
|
1850
1877
|
const retryConversationErrorText = await introspectConversation(retryRun, executionId);
|
|
1851
1878
|
|
|
1879
|
+
// Same shape-aware extraction as the primary error arm — the retry
|
|
1880
|
+
// previously String()-coerced result.result alone, so a structured
|
|
1881
|
+
// retry failure both read "[object Object]" and ignored the
|
|
1882
|
+
// error/message/reason fields the primary arm consults.
|
|
1883
|
+
const retryErrorSources = extractRunErrorSources(retryResult);
|
|
1852
1884
|
const retryClassified = synthesizeError({
|
|
1853
|
-
|
|
1885
|
+
sdkError: retryErrorSources.sdkError,
|
|
1886
|
+
sdkResultFields: retryErrorSources.sdkResultFields,
|
|
1854
1887
|
streamErrorMessage: turnState.streamErrorMessage,
|
|
1855
1888
|
capturedRejection: retryRejection,
|
|
1856
1889
|
conversationErrorText: retryConversationErrorText,
|
|
@@ -1967,6 +2000,7 @@ async function executeCursorInner(
|
|
|
1967
2000
|
`finalTextLength=${finalText.length}`,
|
|
1968
2001
|
);
|
|
1969
2002
|
try {
|
|
2003
|
+
const { extractStructuredOutput } = await import("./extract-structured-output.js");
|
|
1970
2004
|
structuredOutput = await extractStructuredOutput(
|
|
1971
2005
|
finalText, structuredOutputSchema, config, requestedModel,
|
|
1972
2006
|
);
|
|
@@ -2291,53 +2325,6 @@ function seedCursorTranscriptFromExecution(
|
|
|
2291
2325
|
return persisted.subAgentExecutions.map((sub) => clone(SubAgentExecutionSchema, sub));
|
|
2292
2326
|
}
|
|
2293
2327
|
|
|
2294
|
-
// ---------------------------------------------------------------------------
|
|
2295
|
-
// Structured Output Extraction (Cursor Harness Tier 2)
|
|
2296
|
-
// ---------------------------------------------------------------------------
|
|
2297
|
-
|
|
2298
|
-
/**
|
|
2299
|
-
* Extract structured data from an agent's free-text response using an
|
|
2300
|
-
* economy-tier LLM with withStructuredOutput (function-calling).
|
|
2301
|
-
* Guarantees schema-conformant JSON output via the API's tool-use mechanism.
|
|
2302
|
-
*
|
|
2303
|
-
* Construction (registry-id resolution, provider inference, proxy wiring) is
|
|
2304
|
-
* delegated to the shared buildChatModel so the economy model's registry id is
|
|
2305
|
-
* always resolved to a provider API id before the call.
|
|
2306
|
-
*/
|
|
2307
|
-
async function extractStructuredOutput(
|
|
2308
|
-
agentResponse: string,
|
|
2309
|
-
schema: Record<string, unknown>,
|
|
2310
|
-
config: Config,
|
|
2311
|
-
primaryModel: string,
|
|
2312
|
-
): Promise<unknown | null> {
|
|
2313
|
-
const { getEconomyModel } = await import("../../shared/model-registry.js");
|
|
2314
|
-
const { buildChatModel } = await import("../../shared/model-client.js");
|
|
2315
|
-
|
|
2316
|
-
const extractionModel = await getEconomyModel(primaryModel);
|
|
2317
|
-
const proxyEndpoint = config.proxyEndpoint ?? config.stigmerBackendEndpoint;
|
|
2318
|
-
|
|
2319
|
-
const { model: llm } = await buildChatModel({
|
|
2320
|
-
modelName: extractionModel,
|
|
2321
|
-
proxyEndpoint,
|
|
2322
|
-
stigmerToken: config.stigmerToken ?? undefined,
|
|
2323
|
-
maxTokens: 4096,
|
|
2324
|
-
});
|
|
2325
|
-
|
|
2326
|
-
const zodSchema = jsonSchemaToZod(schema);
|
|
2327
|
-
const structured = llm.withStructuredOutput(zodSchema);
|
|
2328
|
-
|
|
2329
|
-
const result = await structured.invoke([
|
|
2330
|
-
{ role: "system", content: "Extract the structured data from the agent's response. Return only the data that matches the schema." },
|
|
2331
|
-
{ role: "user", content: agentResponse },
|
|
2332
|
-
]);
|
|
2333
|
-
|
|
2334
|
-
return result ?? null;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
// Re-export for use within this module; shared implementation eliminates
|
|
2338
|
-
// the three duplicate converters that previously drifted independently.
|
|
2339
|
-
import { jsonSchemaToZod } from "../../shared/json-schema-to-zod.js";
|
|
2340
|
-
|
|
2341
2328
|
// ---------------------------------------------------------------------------
|
|
2342
2329
|
// Prompt selection
|
|
2343
2330
|
// ---------------------------------------------------------------------------
|
|
@@ -2358,7 +2345,11 @@ export interface BuildPromptInput {
|
|
|
2358
2345
|
subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
|
|
2359
2346
|
workspaceDirs: string[];
|
|
2360
2347
|
workspaceFileRefs: string[];
|
|
2361
|
-
|
|
2348
|
+
/**
|
|
2349
|
+
* This turn's resolved attachments for the `<input_files>` section —
|
|
2350
|
+
* final paths plus duplicate-rename disclosure (attachment-resolver.ts).
|
|
2351
|
+
*/
|
|
2352
|
+
attachments: import("./prompt-builder.js").AttachmentPromptEntry[];
|
|
2362
2353
|
/**
|
|
2363
2354
|
* Vision facts for the input-files section (T04): which attachments the
|
|
2364
2355
|
* model sees inline and which degraded to path-only. PER-TURN like the
|
|
@@ -2451,7 +2442,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2451
2442
|
subAgents,
|
|
2452
2443
|
workspaceDirs,
|
|
2453
2444
|
workspaceFileRefs,
|
|
2454
|
-
|
|
2445
|
+
attachments,
|
|
2455
2446
|
interactionMode,
|
|
2456
2447
|
buildFromPlan,
|
|
2457
2448
|
conversationCatchup,
|
|
@@ -2485,9 +2476,9 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2485
2476
|
if (resolution.reason === "resumed_successfully") {
|
|
2486
2477
|
const prefixes = [
|
|
2487
2478
|
formatInteractionModePrefix(interactionMode),
|
|
2488
|
-
formatImplementPlanSection(buildFromPlan,
|
|
2489
|
-
|
|
2490
|
-
? formatInputFiles(
|
|
2479
|
+
formatImplementPlanSection(buildFromPlan, attachments),
|
|
2480
|
+
attachments.length > 0
|
|
2481
|
+
? formatInputFiles(attachments, input.vision)
|
|
2491
2482
|
: undefined,
|
|
2492
2483
|
conversationCatchup !== undefined
|
|
2493
2484
|
? formatConversationCatchupSection(conversationCatchup)
|
|
@@ -2510,7 +2501,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2510
2501
|
subAgents,
|
|
2511
2502
|
workspaceDirs,
|
|
2512
2503
|
workspaceFileRefs,
|
|
2513
|
-
|
|
2504
|
+
attachments,
|
|
2514
2505
|
vision: input.vision,
|
|
2515
2506
|
interactionMode,
|
|
2516
2507
|
buildFromPlan,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* same pipeline as the Python agent-runner's config_transformer.py.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
17
|
+
import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
18
18
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
19
19
|
import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
20
20
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
filterEnvToDeclaredKeys,
|
|
30
30
|
PlaceholderResolutionError,
|
|
31
31
|
} from "./placeholder-resolver.js";
|
|
32
|
+
import { effectiveEnabledTools } from "../../shared/mcp-enabled-tools.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Cursor SDK MCP server config shape (matches @cursor/sdk McpServerConfig).
|
|
@@ -70,6 +71,29 @@ export interface ResolvedMcpServer {
|
|
|
70
71
|
pinnedToolApprovals: ToolApprovalPolicy[];
|
|
71
72
|
/** True when the server has never been connected (no tool discovery yet). */
|
|
72
73
|
discoveredCapabilitiesEmpty: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The EFFECTIVE tool allow-list for this server (issue #350): the usage's
|
|
76
|
+
* enabled_tools, falling back to the server's default_enabled_tools when
|
|
77
|
+
* the usage's list is empty (see shared/mcp-enabled-tools.ts). Absent when
|
|
78
|
+
* unrestricted. The Cursor SDK config cannot hide tools, so this harness
|
|
79
|
+
* enforces it in the HITL hook: the restricted map is written into the
|
|
80
|
+
* approval state (mcpServerEnabledTools) and non-enabled calls are denied
|
|
81
|
+
* with the non-pausing "disabled" kind (see hook-script.ts).
|
|
82
|
+
*/
|
|
83
|
+
enabledTools?: string[];
|
|
84
|
+
/**
|
|
85
|
+
* Layer-3 per-agent approval overrides from the usage that resolved this
|
|
86
|
+
* server (issue #349): riding the resolved server is what SCOPES an
|
|
87
|
+
* override to its own server — a flat cross-server list is how an
|
|
88
|
+
* override once leaked onto (or silently un-gated) a same-named tool on
|
|
89
|
+
* another server. Deliberately REQUIRED, not optional: empty means "no
|
|
90
|
+
* overrides", and every construction site — including the shared
|
|
91
|
+
* resolver this one mirrors and the synthesized attachments, which have
|
|
92
|
+
* no usage and therefore no layer 3 — must say so explicitly, so a
|
|
93
|
+
* forgotten mirror cannot compile. Consumed by mergeApprovalPolicies
|
|
94
|
+
* (shared/approval-policy.ts).
|
|
95
|
+
*/
|
|
96
|
+
toolApprovalOverrides: ToolApprovalOverride[];
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
/**
|
|
@@ -115,7 +139,13 @@ export async function resolveMcpServers(
|
|
|
115
139
|
envVars,
|
|
116
140
|
ref.slug,
|
|
117
141
|
);
|
|
118
|
-
const server = mcpServerToResolved(
|
|
142
|
+
const server = mcpServerToResolved(
|
|
143
|
+
mcpServer,
|
|
144
|
+
ref.slug,
|
|
145
|
+
serverEnv,
|
|
146
|
+
usage.enabledTools,
|
|
147
|
+
usage.toolApprovalOverrides ?? [],
|
|
148
|
+
);
|
|
119
149
|
if (server) {
|
|
120
150
|
assertTransportAllowed(server.slug, server.connectionType, transportPosture);
|
|
121
151
|
resolved.push(server);
|
|
@@ -152,6 +182,8 @@ function mcpServerToResolved(
|
|
|
152
182
|
server: McpServer,
|
|
153
183
|
slug: string,
|
|
154
184
|
envVars: Record<string, string>,
|
|
185
|
+
usageEnabledTools?: readonly string[],
|
|
186
|
+
usageToolApprovalOverrides: ToolApprovalOverride[] = [],
|
|
155
187
|
): ResolvedMcpServer | null {
|
|
156
188
|
const spec = server.spec;
|
|
157
189
|
if (!spec) return null;
|
|
@@ -167,6 +199,8 @@ function mcpServerToResolved(
|
|
|
167
199
|
toolApprovals,
|
|
168
200
|
pinnedToolApprovals,
|
|
169
201
|
discoveredCapabilitiesEmpty,
|
|
202
|
+
enabledTools: effectiveEnabledTools(usageEnabledTools, spec.defaultEnabledTools),
|
|
203
|
+
toolApprovalOverrides: usageToolApprovalOverrides,
|
|
170
204
|
};
|
|
171
205
|
|
|
172
206
|
switch (spec.serverType.case) {
|
|
@@ -71,6 +71,18 @@ export interface VisionPromptInfo {
|
|
|
71
71
|
notViewable: NotViewableEntry[];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* One resolved attachment as the `<input_files>` section renders it —
|
|
76
|
+
* the final workspace-relative path plus the duplicate-rename disclosure
|
|
77
|
+
* (attachment-resolver.ts / shared/attachment-naming.ts).
|
|
78
|
+
*/
|
|
79
|
+
export interface AttachmentPromptEntry {
|
|
80
|
+
/** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
|
|
81
|
+
path: string;
|
|
82
|
+
/** Original filename, present only when a duplicate name was renamed. */
|
|
83
|
+
renamedFrom?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
export interface EnhancedPromptOptions {
|
|
75
87
|
instructions: string;
|
|
76
88
|
userMessage: string;
|
|
@@ -78,7 +90,10 @@ export interface EnhancedPromptOptions {
|
|
|
78
90
|
/**
|
|
79
91
|
* Datastores attached via `datastore_usages` — rendered as the
|
|
80
92
|
* `<available_datastores>` section (DD-005 SD-5, skills precedent)
|
|
81
|
-
* pointing the model at the synthesized record tools.
|
|
93
|
+
* pointing the model at the synthesized record tools. Always the healthy
|
|
94
|
+
* rendering here: the Cursor SDK connects MCP itself, so this harness can
|
|
95
|
+
* never observe the live roster — the section's standing
|
|
96
|
+
* failure-disclosure instruction is its whole coverage (issue #325).
|
|
82
97
|
*/
|
|
83
98
|
datastoreUsages?: DatastoreUsage[];
|
|
84
99
|
/**
|
|
@@ -90,7 +105,7 @@ export interface EnhancedPromptOptions {
|
|
|
90
105
|
subAgents: SubAgent[];
|
|
91
106
|
workspaceDirs: string[];
|
|
92
107
|
workspaceFileRefs: string[];
|
|
93
|
-
|
|
108
|
+
attachments: AttachmentPromptEntry[];
|
|
94
109
|
/** Inline/degraded image facts for the input-files section (T04 vision). */
|
|
95
110
|
vision?: VisionPromptInfo;
|
|
96
111
|
interactionMode?: InteractionMode;
|
|
@@ -153,7 +168,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
153
168
|
|
|
154
169
|
const implementPlan = formatImplementPlanSection(
|
|
155
170
|
options.buildFromPlan,
|
|
156
|
-
options.
|
|
171
|
+
options.attachments,
|
|
157
172
|
);
|
|
158
173
|
if (implementPlan) {
|
|
159
174
|
sections.push(implementPlan);
|
|
@@ -195,8 +210,8 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
195
210
|
sections.push(formatWorkspaceContext(safeDirs));
|
|
196
211
|
}
|
|
197
212
|
|
|
198
|
-
if (options.
|
|
199
|
-
sections.push(formatInputFiles(options.
|
|
213
|
+
if (options.attachments.length > 0) {
|
|
214
|
+
sections.push(formatInputFiles(options.attachments, options.vision));
|
|
200
215
|
}
|
|
201
216
|
|
|
202
217
|
if (options.workspaceFileRefs.length > 0) {
|
|
@@ -479,8 +494,18 @@ export function formatWorkspaceContext(dirs: string[]): string {
|
|
|
479
494
|
].join("\n");
|
|
480
495
|
}
|
|
481
496
|
|
|
482
|
-
export function formatInputFiles(
|
|
483
|
-
|
|
497
|
+
export function formatInputFiles(
|
|
498
|
+
attachments: readonly AttachmentPromptEntry[],
|
|
499
|
+
vision?: VisionPromptInfo,
|
|
500
|
+
): string {
|
|
501
|
+
// A duplicate-renamed file (attachment-naming.ts) discloses its original
|
|
502
|
+
// name so the agent can connect "the two report.pdfs" in the user's
|
|
503
|
+
// message to distinct files on disk.
|
|
504
|
+
const entries = attachments.map((a) =>
|
|
505
|
+
a.renamedFrom !== undefined
|
|
506
|
+
? `- \`${a.path}\` (renamed from duplicate '${a.renamedFrom}')`
|
|
507
|
+
: `- \`${a.path}\``,
|
|
508
|
+
);
|
|
484
509
|
// The vision lines (shared wording, attachment-vision.ts) tell the model
|
|
485
510
|
// which of these files it can already SEE inline versus which degraded to
|
|
486
511
|
// path-only — without them an agent silently ignores a photo the user
|
|
@@ -541,14 +566,14 @@ export function formatInteractionModePrefix(
|
|
|
541
566
|
*/
|
|
542
567
|
export function formatImplementPlanSection(
|
|
543
568
|
buildFromPlan: boolean | undefined,
|
|
544
|
-
|
|
569
|
+
attachments: readonly AttachmentPromptEntry[],
|
|
545
570
|
): string | undefined {
|
|
546
571
|
if (!buildFromPlan) {
|
|
547
572
|
return undefined;
|
|
548
573
|
}
|
|
549
574
|
|
|
550
575
|
const directive = buildImplementPlanDirective(
|
|
551
|
-
findApprovedPlanPath(
|
|
576
|
+
findApprovedPlanPath(attachments.map((a) => a.path)),
|
|
552
577
|
);
|
|
553
578
|
return ["<implement_plan>", directive, "</implement_plan>"].join("\n");
|
|
554
579
|
}
|
|
@@ -387,8 +387,11 @@ export async function consumeCursorTurnStream(
|
|
|
387
387
|
console.log(
|
|
388
388
|
`ExecuteCursor stream status: execution=${executionId}, status=${JSON.stringify(event)}`,
|
|
389
389
|
);
|
|
390
|
-
const statusEvent = event as { status?: string; message?:
|
|
391
|
-
|
|
390
|
+
const statusEvent = event as { status?: string; message?: unknown };
|
|
391
|
+
// The cast is a claim, not a guarantee: message is untyped at runtime
|
|
392
|
+
// (the oss#299 class of bug). A structured value here would crash
|
|
393
|
+
// classification downstream, so only capture actual text.
|
|
394
|
+
if (statusEvent.status === "ERROR" && typeof statusEvent.message === "string" && statusEvent.message.length > 0) {
|
|
392
395
|
state.streamErrorMessage = statusEvent.message;
|
|
393
396
|
}
|
|
394
397
|
}
|
|
@@ -600,24 +600,97 @@ describe("injectAttachments", () => {
|
|
|
600
600
|
expect(Buffer.compare(writtenBuffer, binaryContent)).toBe(0);
|
|
601
601
|
});
|
|
602
602
|
|
|
603
|
-
it("
|
|
603
|
+
it("uniquifies duplicate default-derived filenames instead of failing (issue #364)", async () => {
|
|
604
|
+
const contentA = Buffer.from("first");
|
|
605
|
+
const contentB = Buffer.from("second");
|
|
606
|
+
const storage = makeMockStorage();
|
|
607
|
+
await storage.upload("attachments/a/data.csv", contentA);
|
|
608
|
+
await storage.upload("attachments/b/data.csv", contentB);
|
|
609
|
+
|
|
610
|
+
const backend = mockWorkspaceBackend();
|
|
611
|
+
|
|
612
|
+
const result = await injectAttachments({
|
|
613
|
+
backend,
|
|
614
|
+
attachments: [
|
|
615
|
+
makeAttachment({ filename: "data.csv", storageKey: "attachments/a/data.csv" }),
|
|
616
|
+
makeAttachment({ filename: "data.csv", storageKey: "attachments/b/data.csv" }),
|
|
617
|
+
],
|
|
618
|
+
storage,
|
|
619
|
+
isLocalMode: false,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Both files land, both downloads run — the execution is never burned
|
|
623
|
+
// over a mechanically resolvable name.
|
|
624
|
+
expect(result).toHaveLength(2);
|
|
625
|
+
expect(result[0]).toMatchObject({
|
|
626
|
+
filename: "data.csv",
|
|
627
|
+
path: ".stigmer/inputs/data.csv",
|
|
628
|
+
});
|
|
629
|
+
expect(result[0].renamedFrom).toBeUndefined();
|
|
630
|
+
// The rename follows the platform's stem-2.ext semantics and carries the
|
|
631
|
+
// disclosure payload for the prompt.
|
|
632
|
+
expect(result[1]).toMatchObject({
|
|
633
|
+
filename: "data-2.csv",
|
|
634
|
+
path: ".stigmer/inputs/data-2.csv",
|
|
635
|
+
renamedFrom: "data.csv",
|
|
636
|
+
});
|
|
637
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(
|
|
638
|
+
".stigmer/inputs/data.csv", contentA,
|
|
639
|
+
);
|
|
640
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(
|
|
641
|
+
".stigmer/inputs/data-2.csv", contentB,
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("rejects two attachments explicitly pinning the same mountPath (a user contradiction)", async () => {
|
|
604
646
|
const backend = mockWorkspaceBackend();
|
|
605
647
|
const storage = makeMockStorage();
|
|
606
648
|
|
|
607
649
|
await expect(injectAttachments({
|
|
608
650
|
backend,
|
|
609
651
|
attachments: [
|
|
610
|
-
makeAttachment({ filename: "
|
|
611
|
-
makeAttachment({ filename: "
|
|
652
|
+
makeAttachment({ filename: "a.csv", storageKey: "key1", mountPath: "inputs/data.csv" }),
|
|
653
|
+
makeAttachment({ filename: "b.csv", storageKey: "key2", mountPath: "inputs/data.csv" }),
|
|
612
654
|
],
|
|
613
655
|
storage,
|
|
614
656
|
isLocalMode: false,
|
|
615
657
|
})).rejects.toThrow(/collides with/);
|
|
616
658
|
|
|
617
|
-
//
|
|
659
|
+
// The contradiction is detected before any downloads
|
|
618
660
|
expect(storage.download).not.toHaveBeenCalled();
|
|
619
661
|
});
|
|
620
662
|
|
|
663
|
+
it("default-derived names dodge an explicit mountPath already inside the inputs prefix", async () => {
|
|
664
|
+
const storage = makeMockStorage();
|
|
665
|
+
await storage.upload("attachments/a/pinned", Buffer.from("pinned"));
|
|
666
|
+
await storage.upload("attachments/b/report.pdf", Buffer.from("derived"));
|
|
667
|
+
|
|
668
|
+
const backend = mockWorkspaceBackend();
|
|
669
|
+
|
|
670
|
+
const result = await injectAttachments({
|
|
671
|
+
backend,
|
|
672
|
+
attachments: [
|
|
673
|
+
// Listed AFTER the default-derived attachment: explicit paths claim
|
|
674
|
+
// first regardless of order, so the default still dodges.
|
|
675
|
+
makeAttachment({ filename: "report.pdf", storageKey: "attachments/b/report.pdf" }),
|
|
676
|
+
makeAttachment({
|
|
677
|
+
filename: "report.pdf",
|
|
678
|
+
storageKey: "attachments/a/pinned",
|
|
679
|
+
mountPath: ".stigmer/inputs/report.pdf",
|
|
680
|
+
}),
|
|
681
|
+
],
|
|
682
|
+
storage,
|
|
683
|
+
isLocalMode: false,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const byPath = new Map(result.map((f) => [f.path, f]));
|
|
687
|
+
expect(byPath.get(".stigmer/inputs/report.pdf")?.renamedFrom).toBeUndefined();
|
|
688
|
+
expect(byPath.get(".stigmer/inputs/report-2.pdf")).toMatchObject({
|
|
689
|
+
filename: "report-2.pdf",
|
|
690
|
+
renamedFrom: "report.pdf",
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
621
694
|
it("derives filename from storageKey when filename is empty", async () => {
|
|
622
695
|
const content = Buffer.from("fallback");
|
|
623
696
|
const localFile = join(tempDir, "fallback.txt");
|
|
@@ -662,7 +735,7 @@ describe("injectAttachments", () => {
|
|
|
662
735
|
expect(result[0].path).toBe("workspace/input.csv");
|
|
663
736
|
});
|
|
664
737
|
|
|
665
|
-
it("error
|
|
738
|
+
it("explicit-collision error names both attachments and stays actionable", async () => {
|
|
666
739
|
const backend = mockWorkspaceBackend();
|
|
667
740
|
const storage = makeMockStorage();
|
|
668
741
|
|
|
@@ -670,8 +743,8 @@ describe("injectAttachments", () => {
|
|
|
670
743
|
await injectAttachments({
|
|
671
744
|
backend,
|
|
672
745
|
attachments: [
|
|
673
|
-
makeAttachment({ filename: "first.csv" }),
|
|
674
|
-
makeAttachment({ filename: "
|
|
746
|
+
makeAttachment({ filename: "first.csv", mountPath: "inputs/shared.csv" }),
|
|
747
|
+
makeAttachment({ filename: "second.csv", storageKey: "key2", mountPath: "inputs/shared.csv" }),
|
|
675
748
|
],
|
|
676
749
|
storage,
|
|
677
750
|
isLocalMode: false,
|
|
@@ -680,7 +753,7 @@ describe("injectAttachments", () => {
|
|
|
680
753
|
} catch (err) {
|
|
681
754
|
expect(err).toBeInstanceOf(AttachmentInjectionError);
|
|
682
755
|
const injErr = err as AttachmentInjectionError;
|
|
683
|
-
expect(injErr.attachmentFilename).toBe("
|
|
756
|
+
expect(injErr.attachmentFilename).toBe("second.csv");
|
|
684
757
|
expect(injErr.message).toContain("first.csv");
|
|
685
758
|
expect(injErr.message).toContain("mountPath");
|
|
686
759
|
}
|
|
@@ -776,6 +849,28 @@ describe("injectAttachments — vision selection", () => {
|
|
|
776
849
|
expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/photo.png", PNG_BYTES);
|
|
777
850
|
});
|
|
778
851
|
|
|
852
|
+
it("degrades an image with model_no_vision on a blind model, file written intact", async () => {
|
|
853
|
+
const backend = mockWorkspaceBackend();
|
|
854
|
+
const storage = makeMockStorage();
|
|
855
|
+
await storage.upload("attachments/abc/photo.png", PNG_BYTES);
|
|
856
|
+
|
|
857
|
+
const [injected] = await injectAttachments({
|
|
858
|
+
backend,
|
|
859
|
+
attachments: [makeAttachment({
|
|
860
|
+
filename: "photo.png",
|
|
861
|
+
storageKey: "attachments/abc/photo.png",
|
|
862
|
+
contentType: "image/png",
|
|
863
|
+
})],
|
|
864
|
+
storage,
|
|
865
|
+
isLocalMode: false,
|
|
866
|
+
visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE, { modelVision: false }),
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
expect(injected.vision).toBeUndefined();
|
|
870
|
+
expect(injected.visionDegraded).toBe("model_no_vision");
|
|
871
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/photo.png", PNG_BYTES);
|
|
872
|
+
});
|
|
873
|
+
|
|
779
874
|
it("accepts WebP on the deep-agent profile (unlike the Cursor harness)", async () => {
|
|
780
875
|
const backend = mockWorkspaceBackend();
|
|
781
876
|
const storage = makeMockStorage();
|
|
@@ -838,6 +933,35 @@ describe("injectAttachments — vision selection", () => {
|
|
|
838
933
|
expect(injected.visionDegraded).toBe("type_mismatch");
|
|
839
934
|
});
|
|
840
935
|
|
|
936
|
+
it("labels a duplicate-renamed image's vision payload with the on-disk name", async () => {
|
|
937
|
+
// Coherence pin: the prompt lists the renamed path and the vision
|
|
938
|
+
// disclosure lists image filenames — if the offer used the original
|
|
939
|
+
// attachment.filename, the agent would see two images with one
|
|
940
|
+
// indistinguishable name.
|
|
941
|
+
const backend = mockWorkspaceBackend();
|
|
942
|
+
const storage = makeMockStorage();
|
|
943
|
+
await storage.upload("attachments/a/photo.png", PNG_BYTES);
|
|
944
|
+
await storage.upload("attachments/b/photo.png", PNG_BYTES);
|
|
945
|
+
|
|
946
|
+
const injected = await injectAttachments({
|
|
947
|
+
backend,
|
|
948
|
+
attachments: [
|
|
949
|
+
makeAttachment({ filename: "photo.png", storageKey: "attachments/a/photo.png", contentType: "image/png" }),
|
|
950
|
+
makeAttachment({ filename: "photo.png", storageKey: "attachments/b/photo.png", contentType: "image/png" }),
|
|
951
|
+
],
|
|
952
|
+
storage,
|
|
953
|
+
isLocalMode: false,
|
|
954
|
+
visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
expect(injected[0].vision?.filename).toBe("photo.png");
|
|
958
|
+
expect(injected[1]).toMatchObject({
|
|
959
|
+
filename: "photo-2.png",
|
|
960
|
+
renamedFrom: "photo.png",
|
|
961
|
+
});
|
|
962
|
+
expect(injected[1].vision?.filename).toBe("photo-2.png");
|
|
963
|
+
});
|
|
964
|
+
|
|
841
965
|
it("never offers an extract archive to the budget — extracted files carry no vision fields", async () => {
|
|
842
966
|
// A PNG inside a ZIP has no attachment-level bytes; the archive rides the
|
|
843
967
|
// normal extraction story with no vision involvement or disclosure.
|