@stigmer/runner 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +18 -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 +51 -57
- 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 +55 -23
- 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/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 +3 -1
- package/dist/shared/mcp-manager.js +17 -4
- 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/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 +53 -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 +31 -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 +55 -71
- 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 +110 -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 +76 -28
- 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__/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 +84 -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__/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/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 +21 -4
- 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/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
|
@@ -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");
|
|
@@ -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
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shape-aware run.wait() error extraction (oss#299).
|
|
3
|
+
*
|
|
4
|
+
* A bare String() on a structured error value yields "[object Object]",
|
|
5
|
+
* which end users saw verbatim AND which shadowed every lower-priority
|
|
6
|
+
* classifier source (stream, rejection, conversation introspection) because
|
|
7
|
+
* classification stops at the first non-empty source. These tests pin:
|
|
8
|
+
*
|
|
9
|
+
* - the extractRunErrorSources shape matrix (strings, Errors, field objects,
|
|
10
|
+
* hopeless values)
|
|
11
|
+
* - first-USABLE-candidate chain order (a hopeless object no longer hides a
|
|
12
|
+
* usable string one field later; an empty string no longer short-circuits)
|
|
13
|
+
* - end-to-end: structured errors classify and re-enable fresh-agent retry;
|
|
14
|
+
* hopeless extraction yields to the introspection sources
|
|
15
|
+
* - the "[object Object]" defense-in-depth guard in classifyFromSources
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
extractRunErrorSources,
|
|
21
|
+
synthesizeError,
|
|
22
|
+
shouldRetryWithFreshAgent,
|
|
23
|
+
} from "../error-classifier.js";
|
|
24
|
+
|
|
25
|
+
const FALLBACK = { model: "default", mode: "cloud", agentId: "agent-1" };
|
|
26
|
+
|
|
27
|
+
function base() {
|
|
28
|
+
return {
|
|
29
|
+
sdkResultFields: undefined,
|
|
30
|
+
streamErrorMessage: undefined,
|
|
31
|
+
capturedRejection: undefined,
|
|
32
|
+
isResumedHandle: false,
|
|
33
|
+
fallbackContext: FALLBACK,
|
|
34
|
+
} as const;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A run.wait()-shaped error result carrying the given error-detail fields. */
|
|
38
|
+
function errorResult(fields: Record<string, unknown>): unknown {
|
|
39
|
+
return { id: "run-1", status: "error", ...fields };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NOTHING = { sdkError: undefined, sdkResultFields: undefined };
|
|
43
|
+
|
|
44
|
+
describe("extractRunErrorSources shape matrix", () => {
|
|
45
|
+
it("routes a plain string to sdkResultFields", () => {
|
|
46
|
+
expect(extractRunErrorSources(errorResult({ result: "rate limit exceeded" })))
|
|
47
|
+
.toEqual({ sdkError: undefined, sdkResultFields: "rate limit exceeded" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("lifts an Error instance into the structured channel", () => {
|
|
51
|
+
expect(extractRunErrorSources(errorResult({ result: new Error("connection lost") })))
|
|
52
|
+
.toEqual({ sdkError: { message: "connection lost" }, sdkResultFields: undefined });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("lifts an Error carrying a code (Node/SDK error shape)", () => {
|
|
56
|
+
const err = Object.assign(new Error("stream torn down"), { code: "unavailable" });
|
|
57
|
+
expect(extractRunErrorSources(errorResult({ result: err })))
|
|
58
|
+
.toEqual({ sdkError: { code: "unavailable", message: "stream torn down" }, sdkResultFields: undefined });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("lifts { code, status, message } from a plain object", () => {
|
|
62
|
+
expect(extractRunErrorSources(errorResult({ error: { code: "unauthenticated", status: 401, message: "bad token" } })))
|
|
63
|
+
.toEqual({
|
|
64
|
+
sdkError: { code: "unauthenticated", status: 401, message: "bad token" },
|
|
65
|
+
sdkResultFields: undefined,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("lifts a message-only object", () => {
|
|
70
|
+
expect(extractRunErrorSources(errorResult({ error: { message: "boom" } })))
|
|
71
|
+
.toEqual({ sdkError: { message: "boom" }, sdkResultFields: undefined });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("lifts a code-only object", () => {
|
|
75
|
+
expect(extractRunErrorSources(errorResult({ error: { code: "resource_exhausted" } })))
|
|
76
|
+
.toEqual({ sdkError: { code: "resource_exhausted" }, sdkResultFields: undefined });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("yields nothing for an object with no recognizable fields (no JSON.stringify junk)", () => {
|
|
80
|
+
expect(extractRunErrorSources(errorResult({ result: { weird: "shape" } }))).toEqual(NOTHING);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("yields nothing for a circular object", () => {
|
|
84
|
+
const circular: Record<string, unknown> = {};
|
|
85
|
+
circular.self = circular;
|
|
86
|
+
expect(extractRunErrorSources(errorResult({ result: circular }))).toEqual(NOTHING);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("refuses the '[object Object]' junk string itself", () => {
|
|
90
|
+
expect(extractRunErrorSources(errorResult({ result: "[object Object]" }))).toEqual(NOTHING);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stringifies non-string primitives losslessly", () => {
|
|
94
|
+
expect(extractRunErrorSources(errorResult({ result: 503 })))
|
|
95
|
+
.toEqual({ sdkError: undefined, sdkResultFields: "503" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("yields nothing when no candidate field is present", () => {
|
|
99
|
+
expect(extractRunErrorSources(errorResult({}))).toEqual(NOTHING);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("yields nothing for non-object results", () => {
|
|
103
|
+
expect(extractRunErrorSources(undefined)).toEqual(NOTHING);
|
|
104
|
+
expect(extractRunErrorSources(null)).toEqual(NOTHING);
|
|
105
|
+
expect(extractRunErrorSources("not-a-result-object")).toEqual(NOTHING);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("extractRunErrorSources chain order (first USABLE candidate wins)", () => {
|
|
110
|
+
it("a hopeless object in result no longer hides a usable string in message", () => {
|
|
111
|
+
const extracted = extractRunErrorSources(
|
|
112
|
+
errorResult({ result: { weird: "shape" }, message: "the real reason" }),
|
|
113
|
+
);
|
|
114
|
+
expect(extracted).toEqual({ sdkError: undefined, sdkResultFields: "the real reason" });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("an empty string in result no longer short-circuits the chain", () => {
|
|
118
|
+
const extracted = extractRunErrorSources(
|
|
119
|
+
errorResult({ result: "", reason: "torn down mid-stream" }),
|
|
120
|
+
);
|
|
121
|
+
expect(extracted).toEqual({ sdkError: undefined, sdkResultFields: "torn down mid-stream" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("respects the documented field order: result before error before message before reason", () => {
|
|
125
|
+
const extracted = extractRunErrorSources(
|
|
126
|
+
errorResult({ result: "from-result", error: "from-error", message: "from-message" }),
|
|
127
|
+
);
|
|
128
|
+
expect(extracted.sdkResultFields).toBe("from-result");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("yields nothing when every candidate is hopeless", () => {
|
|
132
|
+
const extracted = extractRunErrorSources(
|
|
133
|
+
errorResult({ result: {}, error: "", message: "[object Object]" }),
|
|
134
|
+
);
|
|
135
|
+
expect(extracted).toEqual(NOTHING);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("end-to-end through synthesizeError", () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
142
|
+
});
|
|
143
|
+
afterEach(() => {
|
|
144
|
+
vi.restoreAllMocks();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("a structured retryable error classifies and re-enables fresh-agent recovery", () => {
|
|
148
|
+
// The regression at the heart of oss#299: String() turned this into
|
|
149
|
+
// "[object Object]" -> category=unknown, retryable=false -> the
|
|
150
|
+
// poisoned-handle retry could never fire for a plain network flake.
|
|
151
|
+
const extracted = extractRunErrorSources(
|
|
152
|
+
errorResult({ error: { code: "unavailable", message: "upstream connect error" } }),
|
|
153
|
+
);
|
|
154
|
+
const classified = synthesizeError({ ...base(), ...extracted });
|
|
155
|
+
|
|
156
|
+
expect(classified.source).toBe("sdk");
|
|
157
|
+
expect(classified.category).toBe("network");
|
|
158
|
+
expect(classified.message).toBe("upstream connect error");
|
|
159
|
+
expect(classified.retryable).toBe(true);
|
|
160
|
+
expect(shouldRetryWithFreshAgent(classified)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("hopeless extraction yields to the conversation introspection source", () => {
|
|
164
|
+
const extracted = extractRunErrorSources(errorResult({ result: { weird: "shape" } }));
|
|
165
|
+
const classified = synthesizeError({
|
|
166
|
+
...base(),
|
|
167
|
+
...extracted,
|
|
168
|
+
conversationErrorText: "grpc-status 12: routing failure",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(classified.source).toBe("conversation");
|
|
172
|
+
expect(classified.message).toBe("grpc-status 12: routing failure");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("hopeless extraction yields to the captured rejection source", () => {
|
|
176
|
+
const extracted = extractRunErrorSources(errorResult({ result: { weird: "shape" } }));
|
|
177
|
+
const classified = synthesizeError({
|
|
178
|
+
...base(),
|
|
179
|
+
...extracted,
|
|
180
|
+
capturedRejection: { code: "unavailable", message: "socket hang up", timestamp: Date.now() },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(classified.source).toBe("rejection");
|
|
184
|
+
expect(classified.message).toContain("socket hang up");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("classifyFromSources '[object Object]' defense-in-depth guard", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
191
|
+
});
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
vi.restoreAllMocks();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("treats a leaked '[object Object]' sdkResultFields as absent", () => {
|
|
197
|
+
// Extraction never emits it, but any other producer of the junk string
|
|
198
|
+
// must not shadow the sources below it.
|
|
199
|
+
const classified = synthesizeError({
|
|
200
|
+
...base(),
|
|
201
|
+
sdkResultFields: "[object Object]",
|
|
202
|
+
streamErrorMessage: "fetch failed",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(classified.source).toBe("stream");
|
|
206
|
+
expect(classified.category).toBe("network");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { Config } from "../../../config.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("../../../shared/model-registry.js", () => ({
|
|
5
|
+
getEconomyModel: vi.fn().mockResolvedValue("gpt-4o-mini"),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const mockInvoke = vi.fn();
|
|
9
|
+
const mockWithStructuredOutput = vi.fn().mockReturnValue({ invoke: mockInvoke });
|
|
10
|
+
|
|
11
|
+
vi.mock("../../../shared/model-client.js", () => ({
|
|
12
|
+
buildChatModel: vi.fn().mockResolvedValue({
|
|
13
|
+
model: { withStructuredOutput: (...args: unknown[]) => mockWithStructuredOutput(...args) },
|
|
14
|
+
provider: "openai",
|
|
15
|
+
apiModelId: "gpt-4o-mini",
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// llm-backend.js and llm-proxy.js stay real: the pre-check behavior under
|
|
20
|
+
// test IS their composition, and both are pure modules.
|
|
21
|
+
|
|
22
|
+
const SCHEMA = { type: "object", properties: { answer: { type: "string" } } };
|
|
23
|
+
|
|
24
|
+
function makeConfig(overrides: Partial<Config> = {}): Config {
|
|
25
|
+
return {
|
|
26
|
+
proxyEndpoint: null,
|
|
27
|
+
stigmerToken: null,
|
|
28
|
+
...overrides,
|
|
29
|
+
} as Config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("extractStructuredOutput", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
// Deterministic regardless of the developer's shell: blank reads as
|
|
36
|
+
// missing, and backend vars must not leak in from outside.
|
|
37
|
+
vi.stubEnv("OPENAI_API_KEY", "");
|
|
38
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "");
|
|
39
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
40
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.unstubAllEnvs();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("direct mode with a key builds a direct-mode model (no endpoint threaded)", async () => {
|
|
48
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct");
|
|
49
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
50
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
51
|
+
mockInvoke.mockResolvedValueOnce({ answer: "42" });
|
|
52
|
+
|
|
53
|
+
const result = await extractStructuredOutput("the answer is 42", SCHEMA, makeConfig(), "gpt-4.1");
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual({ answer: "42" });
|
|
56
|
+
// The regression pin: the gRPC control-plane endpoint must never
|
|
57
|
+
// reappear here as a stand-in LLM proxy.
|
|
58
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({ proxyEndpoint: undefined }),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("throws the credential message before any construction when no path exists", async () => {
|
|
64
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
65
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
66
|
+
|
|
67
|
+
await expect(
|
|
68
|
+
extractStructuredOutput("text", SCHEMA, makeConfig(), "gpt-4.1"),
|
|
69
|
+
).rejects.toThrow(/'gpt-4o-mini'.*OPENAI_API_KEY/s);
|
|
70
|
+
expect(buildChatModel).not.toHaveBeenCalled();
|
|
71
|
+
expect(mockInvoke).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("proxy mode threads the proxy endpoint and token, consulting no keys", async () => {
|
|
75
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
76
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
77
|
+
mockInvoke.mockResolvedValueOnce({ answer: "ok" });
|
|
78
|
+
|
|
79
|
+
const result = await extractStructuredOutput(
|
|
80
|
+
"text", SCHEMA,
|
|
81
|
+
makeConfig({ proxyEndpoint: "https://api.stigmer.ai", stigmerToken: "tok" }),
|
|
82
|
+
"gpt-4.1",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({ answer: "ok" });
|
|
86
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
proxyEndpoint: "https://api.stigmer.ai",
|
|
89
|
+
stigmerToken: "tok",
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("defers an un-inferable extraction model to buildChatModel's own error", async () => {
|
|
95
|
+
// The registry-empty fallback returns the primary model verbatim; when
|
|
96
|
+
// its provider can't be inferred the pre-check must not guess — the
|
|
97
|
+
// construction path owns the precise message.
|
|
98
|
+
const { getEconomyModel } = await import("../../../shared/model-registry.js");
|
|
99
|
+
vi.mocked(getEconomyModel).mockResolvedValueOnce("mystery-model");
|
|
100
|
+
const { buildChatModel } = await import("../../../shared/model-client.js");
|
|
101
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
102
|
+
mockInvoke.mockResolvedValueOnce({ answer: "ok" });
|
|
103
|
+
|
|
104
|
+
await extractStructuredOutput("text", SCHEMA, makeConfig(), "mystery-model");
|
|
105
|
+
|
|
106
|
+
expect(buildChatModel).toHaveBeenCalledWith(
|
|
107
|
+
expect.objectContaining({ modelName: "mystery-model" }),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("normalizes an empty extraction result to null", async () => {
|
|
112
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct");
|
|
113
|
+
const { extractStructuredOutput } = await import("../extract-structured-output.js");
|
|
114
|
+
mockInvoke.mockResolvedValueOnce(undefined);
|
|
115
|
+
|
|
116
|
+
const result = await extractStructuredOutput("text", SCHEMA, makeConfig(), "gpt-4.1");
|
|
117
|
+
|
|
118
|
+
expect(result).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -245,6 +245,99 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
|
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
// The enabled_tools capability manifest (issue #350): mcpServerEnabledTools
|
|
249
|
+
// holds ONLY restricted servers; the hook denies a listed server's
|
|
250
|
+
// non-listed tool with the non-pausing, permanent "disabled" kind — BEFORE
|
|
251
|
+
// autoApproveAll and the grant checks, because a manifest is not an
|
|
252
|
+
// approval gate (nothing may resurrect a disabled tool). hookMcp payloads
|
|
253
|
+
// carry mcp_server_name "srv".
|
|
254
|
+
describe("MCP enabled_tools manifest (beforeMCPExecution, issue #350)", () => {
|
|
255
|
+
it("denies a non-enabled tool with kind disabled (content-free, single record) and the manifest message", () => {
|
|
256
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
257
|
+
|
|
258
|
+
const res = h.decide(hookMcp("click", { app: "Slack" }));
|
|
259
|
+
|
|
260
|
+
expect(res.permission).toBe("deny");
|
|
261
|
+
// Permanent-denial framing, never the approval promise: the model must
|
|
262
|
+
// adapt, not wait for a resume that will never come.
|
|
263
|
+
expect(res.raw).toContain("not enabled for this agent");
|
|
264
|
+
expect(res.raw).not.toContain("submitted to the user for approval");
|
|
265
|
+
const ledger = h.ledger();
|
|
266
|
+
expect(ledger).toHaveLength(1);
|
|
267
|
+
expect(ledger[0].kind).toBe("disabled");
|
|
268
|
+
// Attributable under the MCP name-token (the identity the stream row
|
|
269
|
+
// computes), content-free like every non-approval kind.
|
|
270
|
+
expect(ledger[0].token).toBe(grantToken("click", ""));
|
|
271
|
+
expect(ledger[0]).not.toHaveProperty("input");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("allows an enabled tool on a restricted server", () => {
|
|
275
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
276
|
+
expect(h.decide(hookMcp("list_apps")).permission).toBe("allow");
|
|
277
|
+
expect(h.ledger()).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("denies even under autoApproveAll (a manifest is not an approval gate)", () => {
|
|
281
|
+
const h = setup({
|
|
282
|
+
autoApproveAll: true,
|
|
283
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
284
|
+
});
|
|
285
|
+
const res = h.decide(hookMcp("click"));
|
|
286
|
+
expect(res.permission).toBe("deny");
|
|
287
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("denies even when the tool holds a reinvocation grant (no approval may resurrect it)", () => {
|
|
291
|
+
const h = setup({
|
|
292
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
293
|
+
grants: [{ toolName: "click", mcpServerSlug: "srv", key: "click", salient: "", contentDigest: "", sourceToolCallId: "consent-1" }],
|
|
294
|
+
});
|
|
295
|
+
const res = h.decide(hookMcp("click"));
|
|
296
|
+
expect(res.permission).toBe("deny");
|
|
297
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("stays kind disabled under unattended mode (mode-independent, like secret)", () => {
|
|
301
|
+
const h = setup({
|
|
302
|
+
unattendedSkip: true,
|
|
303
|
+
mcpServerEnabledTools: { srv: ["list_apps"] },
|
|
304
|
+
});
|
|
305
|
+
const res = h.decide(hookMcp("click"));
|
|
306
|
+
expect(res.permission).toBe("deny");
|
|
307
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("an enabled tool still flows into the normal approval arm (manifest and gate compose)", () => {
|
|
311
|
+
const h = setup({
|
|
312
|
+
mcpPolicies: { click: { requiresApproval: true, message: "Approve click?" } },
|
|
313
|
+
mcpServerEnabledTools: { srv: ["click"] },
|
|
314
|
+
});
|
|
315
|
+
const res = h.decide(hookMcp("click"));
|
|
316
|
+
expect(res.permission).toBe("deny");
|
|
317
|
+
expect(res.raw).toContain("Approve click?");
|
|
318
|
+
expect(h.ledger()[0].kind).toBe("approval");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("a restriction on ANOTHER server never narrows this one (server-scoped matching)", () => {
|
|
322
|
+
const h = setup({ mcpServerEnabledTools: { other: ["something_else"] } });
|
|
323
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
324
|
+
expect(h.ledger()).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("quoted-name matching is exact — an enabled name never allows its prefix-sibling", () => {
|
|
328
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps_extended"] } });
|
|
329
|
+
const res = h.decide(hookMcp("list_apps"));
|
|
330
|
+
expect(res.permission).toBe("deny");
|
|
331
|
+
expect(h.ledger()[0].kind).toBe("disabled");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("never gates a preToolUse (built-in) payload — the manifest arm is MCP-event-scoped", () => {
|
|
335
|
+
const h = setup({ mcpServerEnabledTools: { srv: ["list_apps"] } });
|
|
336
|
+
expect(h.decide(hookRead("/x/a.txt")).permission).toBe("allow");
|
|
337
|
+
expect(h.ledger()).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
248
341
|
// The hook captures the COMPLETE tool_input on every denial (base64(JSON)),
|
|
249
342
|
// so the runner can overlay the proposed change onto the gated tool call for
|
|
250
343
|
// the approval preview — the cursor analog of the native harness reading args
|