@stigmer/runner 3.11.1-dev.20260812192248 → 3.12.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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
- 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 +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +6 -2
- package/dist/activities/execute-cursor/index.js +16 -32
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +15 -11
- package/dist/activities/execute-cursor/prompt-builder.js +20 -10
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +11 -0
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +0 -15
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
- package/dist/activities/execute-deep-agent/setup.js +6 -42
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -0
- package/dist/shared/artifact-storage.js +2 -0
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-download-urls.d.ts +62 -0
- package/dist/shared/attachment-download-urls.js +79 -0
- package/dist/shared/attachment-download-urls.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js +1 -1
- package/dist/shared/conversation-attachment.d.ts +2 -3
- package/dist/shared/conversation-attachment.js +2 -3
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/mcp-transport-guard.d.ts +2 -2
- package/dist/shared/mcp-transport-guard.js +2 -2
- package/dist/shared/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- package/package.json +2 -2
- package/src/__test-utils__/fake-artifact-storage.ts +10 -1
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +62 -29
- package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
- package/src/activities/execute-cursor/index.ts +22 -37
- package/src/activities/execute-cursor/prompt-builder.ts +38 -22
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +71 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
- package/src/activities/execute-deep-agent/index.ts +0 -19
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
- package/src/activities/execute-deep-agent/setup.ts +6 -65
- package/src/config.ts +6 -5
- package/src/shared/__tests__/artifact-storage.test.ts +9 -0
- package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
- package/src/shared/__tests__/channel-attachment.test.ts +2 -2
- package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
- package/src/shared/artifact-storage.ts +11 -0
- package/src/shared/attachment-download-urls.ts +98 -0
- package/src/shared/channel-attachment.ts +1 -1
- package/src/shared/conversation-attachment.ts +2 -3
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/synthesized-attachment.ts +4 -4
- package/dist/shared/datastore-attachment.d.ts +0 -99
- package/dist/shared/datastore-attachment.js +0 -184
- package/dist/shared/datastore-attachment.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
- package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
- package/src/shared/datastore-attachment.ts +0 -209
|
@@ -1014,3 +1014,108 @@ describe("injectAttachments — vision selection", () => {
|
|
|
1014
1014
|
expect(injected[1].visionDegraded).toBe("budget_exhausted");
|
|
1015
1015
|
});
|
|
1016
1016
|
});
|
|
1017
|
+
|
|
1018
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1019
|
+
// injectAttachments — download-URL hand-off (issue #532)
|
|
1020
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1021
|
+
// The URL is strictly additive (shared/attachment-download-urls.ts): every
|
|
1022
|
+
// case also asserts the file injected exactly as it would without one.
|
|
1023
|
+
|
|
1024
|
+
describe("injectAttachments — download-URL hand-off", () => {
|
|
1025
|
+
it("mints a URL for a storage-key attachment", async () => {
|
|
1026
|
+
const content = Buffer.from("cloud data");
|
|
1027
|
+
const storage = makeMockStorage();
|
|
1028
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1029
|
+
const backend = mockWorkspaceBackend();
|
|
1030
|
+
|
|
1031
|
+
const result = await injectAttachments({
|
|
1032
|
+
backend,
|
|
1033
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1034
|
+
storage,
|
|
1035
|
+
isLocalMode: false,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
expect(result[0]).toMatchObject({
|
|
1039
|
+
path: ".stigmer/inputs/data.csv",
|
|
1040
|
+
downloadUrl: "mem://attachments/xyz/data.csv",
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it("mints a URL for an explicit-mountPath attachment (the rule ignores mount location)", async () => {
|
|
1045
|
+
const storage = makeMockStorage();
|
|
1046
|
+
await storage.upload("attachments/xyz/config.yaml", Buffer.from("a: 1"));
|
|
1047
|
+
const backend = mockWorkspaceBackend();
|
|
1048
|
+
|
|
1049
|
+
const result = await injectAttachments({
|
|
1050
|
+
backend,
|
|
1051
|
+
attachments: [makeAttachment({
|
|
1052
|
+
filename: "config.yaml",
|
|
1053
|
+
storageKey: "attachments/xyz/config.yaml",
|
|
1054
|
+
mountPath: "config/app.yaml",
|
|
1055
|
+
})],
|
|
1056
|
+
storage,
|
|
1057
|
+
isLocalMode: false,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
expect(result[0]).toMatchObject({
|
|
1061
|
+
path: "config/app.yaml",
|
|
1062
|
+
downloadUrl: "mem://attachments/xyz/config.yaml",
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("carries no URL on extracted ZIP entries — the stored object is the ZIP, not any listed file", async () => {
|
|
1067
|
+
const zip = makeZip({ "src/a.txt": "alpha", "src/b.txt": "beta" });
|
|
1068
|
+
const storage = makeMockStorage();
|
|
1069
|
+
await storage.upload("attachments/xyz/code.zip", zip);
|
|
1070
|
+
const backend = mockWorkspaceBackend();
|
|
1071
|
+
|
|
1072
|
+
const result = await injectAttachments({
|
|
1073
|
+
backend,
|
|
1074
|
+
attachments: [makeAttachment({
|
|
1075
|
+
filename: "code.zip",
|
|
1076
|
+
storageKey: "attachments/xyz/code.zip",
|
|
1077
|
+
extract: true,
|
|
1078
|
+
})],
|
|
1079
|
+
storage,
|
|
1080
|
+
isLocalMode: false,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
expect(result.length).toBeGreaterThan(0);
|
|
1084
|
+
for (const file of result) {
|
|
1085
|
+
expect(file.downloadUrl).toBeUndefined();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("carries no URL for a key-less local file", async () => {
|
|
1090
|
+
const localFile = join(tmpdir(), `injector-url-${Date.now()}.txt`);
|
|
1091
|
+
await writeFile(localFile, "local only");
|
|
1092
|
+
const backend = mockWorkspaceBackend();
|
|
1093
|
+
|
|
1094
|
+
const result = await injectAttachments({
|
|
1095
|
+
backend,
|
|
1096
|
+
attachments: [makeAttachment({ filename: "local.txt", storageKey: "", localPath: localFile })],
|
|
1097
|
+
storage: makeMockStorage(),
|
|
1098
|
+
isLocalMode: true,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("degrades to no URL when the mint fails — file still injected, no throw", async () => {
|
|
1105
|
+
const content = Buffer.from("cloud data");
|
|
1106
|
+
const storage = makeMockStorage();
|
|
1107
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1108
|
+
storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
|
|
1109
|
+
const backend = mockWorkspaceBackend();
|
|
1110
|
+
|
|
1111
|
+
const result = await injectAttachments({
|
|
1112
|
+
backend,
|
|
1113
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1114
|
+
storage,
|
|
1115
|
+
isLocalMode: false,
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1119
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/data.csv", content);
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
@@ -184,7 +184,6 @@ function fakeHttpRejectSetup(): SetupResult {
|
|
|
184
184
|
session: { spec: { workspaceEntries: [] } },
|
|
185
185
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-http" },
|
|
186
186
|
mcpConnection: null,
|
|
187
|
-
datastoreToolsMissing: [],
|
|
188
187
|
mergedEnvVars: {},
|
|
189
188
|
secretKeys: new Set<string>(),
|
|
190
189
|
modelName: "claude-sonnet",
|
|
@@ -220,7 +219,6 @@ function fakeMemoryRejectSetup(): SetupResult {
|
|
|
220
219
|
session: { spec: { workspaceEntries: [] } },
|
|
221
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-mem" },
|
|
222
221
|
mcpConnection: null,
|
|
223
|
-
datastoreToolsMissing: [],
|
|
224
222
|
mergedEnvVars: {},
|
|
225
223
|
secretKeys: new Set<string>(),
|
|
226
224
|
modelName: "claude-sonnet",
|
|
@@ -264,7 +264,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
264
264
|
// path shared across files would serialize parallel test workers for real.
|
|
265
265
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-approve-all" },
|
|
266
266
|
mcpConnection: null,
|
|
267
|
-
datastoreToolsMissing: [],
|
|
268
267
|
mergedEnvVars: {},
|
|
269
268
|
secretKeys: new Set<string>(),
|
|
270
269
|
modelName: "claude-sonnet",
|
|
@@ -219,7 +219,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
219
219
|
// path shared across files would serialize parallel test workers for real.
|
|
220
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-history" },
|
|
221
221
|
mcpConnection: null,
|
|
222
|
-
datastoreToolsMissing: [],
|
|
223
222
|
mergedEnvVars: {},
|
|
224
223
|
secretKeys: new Set<string>(),
|
|
225
224
|
modelName: "claude-sonnet",
|
|
@@ -12,7 +12,6 @@ import { shouldConnectMcp } from "../mcp-gate.js";
|
|
|
12
12
|
|
|
13
13
|
const nothing = {
|
|
14
14
|
mcpServerUsageCount: 0,
|
|
15
|
-
datastoreUsageCount: 0,
|
|
16
15
|
channelMessagingCount: 0,
|
|
17
16
|
conversationChannelId: undefined,
|
|
18
17
|
};
|
|
@@ -26,17 +25,13 @@ describe("shouldConnectMcp", () => {
|
|
|
26
25
|
expect(shouldConnectMcp({ ...nothing, mcpServerUsageCount: 1 })).toBe(true);
|
|
27
26
|
});
|
|
28
27
|
|
|
29
|
-
it("enters on datastore usages alone (the records attachment)", () => {
|
|
30
|
-
expect(shouldConnectMcp({ ...nothing, datastoreUsageCount: 1 })).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
28
|
it("enters on a serving proactive channel alone (the channels attachment)", () => {
|
|
34
29
|
expect(shouldConnectMcp({ ...nothing, channelMessagingCount: 1 })).toBe(true);
|
|
35
30
|
});
|
|
36
31
|
|
|
37
32
|
it("enters on a channel conversation alone (the conversation attachment)", () => {
|
|
38
|
-
// The reply-only pilot shape: no declared servers, no
|
|
39
|
-
//
|
|
33
|
+
// The reply-only pilot shape: no declared servers, no proactive
|
|
34
|
+
// channel — the escalation tool is the ONLY source.
|
|
40
35
|
expect(shouldConnectMcp({ ...nothing, conversationChannelId: "agch_1" })).toBe(true);
|
|
41
36
|
});
|
|
42
37
|
});
|
|
@@ -190,6 +190,77 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
190
190
|
expect(prompt).toContain("`.stigmer/inputs/report.pdf` (10 bytes)\n");
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it("lists a minted download URL beside its file with the presigned hand-off line (issue #532)", () => {
|
|
194
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
195
|
+
instructions: "Test",
|
|
196
|
+
provisionResults: [],
|
|
197
|
+
containerRoot: "",
|
|
198
|
+
skillsPromptSection: "",
|
|
199
|
+
workspaceFileRefs: [],
|
|
200
|
+
workspaceRoot: "/workspace",
|
|
201
|
+
injectedFiles: [
|
|
202
|
+
{
|
|
203
|
+
filename: "lease.pdf",
|
|
204
|
+
path: ".stigmer/inputs/lease.pdf",
|
|
205
|
+
sizeBytes: 2048,
|
|
206
|
+
downloadUrl: "https://r2.example/lease?sig=abc",
|
|
207
|
+
},
|
|
208
|
+
{ filename: "notes.md", path: ".stigmer/inputs/notes.md", sizeBytes: 12 },
|
|
209
|
+
],
|
|
210
|
+
downloadUrlKind: "presigned",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(prompt).toContain(
|
|
214
|
+
"`.stigmer/inputs/lease.pdf` (2048 bytes) — download URL: https://r2.example/lease?sig=abc",
|
|
215
|
+
);
|
|
216
|
+
// The URL-less file keeps a clean entry.
|
|
217
|
+
expect(prompt).toContain("`.stigmer/inputs/notes.md` (12 bytes)\n");
|
|
218
|
+
expect(prompt).toContain("These URLs are time-limited");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("words a local-serve URL honestly — reachable only from this machine", () => {
|
|
222
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
223
|
+
instructions: "Test",
|
|
224
|
+
provisionResults: [],
|
|
225
|
+
containerRoot: "",
|
|
226
|
+
skillsPromptSection: "",
|
|
227
|
+
workspaceFileRefs: [],
|
|
228
|
+
workspaceRoot: "/workspace",
|
|
229
|
+
injectedFiles: [
|
|
230
|
+
{
|
|
231
|
+
filename: "lease.pdf",
|
|
232
|
+
path: ".stigmer/inputs/lease.pdf",
|
|
233
|
+
sizeBytes: 2048,
|
|
234
|
+
downloadUrl: "http://localhost:7235/attachments/01A/lease.pdf",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
downloadUrlKind: "local-serve",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(prompt).toContain("download URL: http://localhost:7235/attachments/01A/lease.pdf");
|
|
241
|
+
expect(prompt).toContain("reachable only from this machine");
|
|
242
|
+
expect(prompt).not.toContain("time-limited");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders no hand-off line when no listed file carries a URL (kind alone is not enough)", () => {
|
|
246
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
247
|
+
instructions: "Test",
|
|
248
|
+
provisionResults: [],
|
|
249
|
+
containerRoot: "",
|
|
250
|
+
skillsPromptSection: "",
|
|
251
|
+
workspaceFileRefs: [],
|
|
252
|
+
workspaceRoot: "/workspace",
|
|
253
|
+
injectedFiles: [
|
|
254
|
+
{ filename: "local.csv", path: ".stigmer/inputs/local.csv", sizeBytes: 5 },
|
|
255
|
+
],
|
|
256
|
+
downloadUrlKind: "presigned",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(prompt).toContain("## Input Files");
|
|
260
|
+
expect(prompt).not.toContain("download URL");
|
|
261
|
+
expect(prompt).not.toContain("time-limited");
|
|
262
|
+
});
|
|
263
|
+
|
|
193
264
|
it("produces a git repo description for git workspace entries", () => {
|
|
194
265
|
const prompt = buildEnhancedSystemPrompt({
|
|
195
266
|
instructions: "Test",
|
|
@@ -27,6 +27,7 @@ import { posix } from "node:path";
|
|
|
27
27
|
import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
28
28
|
import type { WorkspaceBackend } from "../../shared/workspace/types.js";
|
|
29
29
|
import type { ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
30
|
+
import { mintAttachmentDownloadUrl } from "../../shared/attachment-download-urls.js";
|
|
30
31
|
import { allocateUniqueName } from "../../shared/attachment-naming.js";
|
|
31
32
|
import type {
|
|
32
33
|
VisionBudget,
|
|
@@ -66,6 +67,17 @@ export interface InjectedFile {
|
|
|
66
67
|
* Attachments that were never image-shaped carry neither field.
|
|
67
68
|
*/
|
|
68
69
|
readonly visionDegraded?: VisionDegradedReason;
|
|
70
|
+
/**
|
|
71
|
+
* A download URL for the attachment's stored object, listed in the system
|
|
72
|
+
* prompt's Input Files section so the agent can hand the file to a tool
|
|
73
|
+
* whose backend cannot read the sandbox filesystem (issue #532). Minted
|
|
74
|
+
* whenever the attachment has a `storageKey` and a usable storage; never
|
|
75
|
+
* present on extracted ZIP entries (no attachment-level object). Absent
|
|
76
|
+
* when there is nothing to mint or the mint failed (non-fatal — policy,
|
|
77
|
+
* degrade log, and prompt wording live in
|
|
78
|
+
* shared/attachment-download-urls.ts).
|
|
79
|
+
*/
|
|
80
|
+
readonly downloadUrl?: string;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
export interface InjectAttachmentsOptions {
|
|
@@ -300,7 +312,10 @@ export async function injectAttachments(opts: InjectAttachmentsOptions): Promise
|
|
|
300
312
|
);
|
|
301
313
|
// A renamed mount DIR is visible through every extracted path; the
|
|
302
314
|
// entries themselves were not renamed, so they carry no renamedFrom
|
|
303
|
-
// (per-file disclosure would misattribute the rename).
|
|
315
|
+
// (per-file disclosure would misattribute the rename). Extracted
|
|
316
|
+
// entries also carry no downloadUrl: the stored object is the ZIP, not
|
|
317
|
+
// any listed file, and a URL to bytes that differ from the listing
|
|
318
|
+
// would be a false promise.
|
|
304
319
|
injectedFiles.push(...extracted);
|
|
305
320
|
} else {
|
|
306
321
|
await backend.writeFileBuffer(mountPath, content);
|
|
@@ -313,6 +328,9 @@ export async function injectAttachments(opts: InjectAttachmentsOptions): Promise
|
|
|
313
328
|
// the vision budget before they go out of scope (the sniff decides
|
|
314
329
|
// eligibility; the budget owns every size/count rule).
|
|
315
330
|
const vision = visionBudget?.offer(filename, attachment.contentType, content);
|
|
331
|
+
const downloadUrl = await mintAttachmentDownloadUrl(
|
|
332
|
+
storage, attachment.storageKey, filename,
|
|
333
|
+
);
|
|
316
334
|
injectedFiles.push({
|
|
317
335
|
filename,
|
|
318
336
|
path: mountPath,
|
|
@@ -320,6 +338,7 @@ export async function injectAttachments(opts: InjectAttachmentsOptions): Promise
|
|
|
320
338
|
...(renamedFrom !== undefined ? { renamedFrom } : {}),
|
|
321
339
|
...(vision?.kind === "accepted" ? { vision: vision.image } : {}),
|
|
322
340
|
...(vision?.kind === "degraded" ? { visionDegraded: vision.reason } : {}),
|
|
341
|
+
...(downloadUrl !== undefined ? { downloadUrl } : {}),
|
|
323
342
|
});
|
|
324
343
|
}
|
|
325
344
|
}
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
} from "../../shared/workspace/stigmer-link.js";
|
|
33
33
|
import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
|
|
34
34
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
35
|
-
import { formatDatastoreDegradationNotice } from "../../shared/datastore-attachment.js";
|
|
36
35
|
import { classifyTool } from "../../shared/tool-kind.js";
|
|
37
36
|
import {
|
|
38
37
|
POLICY_ENGINE_VERSION,
|
|
@@ -153,24 +152,6 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
153
152
|
? seedStatusFromExecution(setup.execution)
|
|
154
153
|
: create(AgentExecutionStatusSchema, {});
|
|
155
154
|
|
|
156
|
-
// Datastore degradation disclosure, operator half (issue #325): the
|
|
157
|
-
// prompt already tells the agent to disclose the outage; this makes
|
|
158
|
-
// it visible on the execution record without reading a transcript.
|
|
159
|
-
// Appended to initialStatus BEFORE streaming (not via a status
|
|
160
|
-
// builder): both the v2 and v3 stream paths construct their own
|
|
161
|
-
// builder around this same proto and only ever append messages, so
|
|
162
|
-
// this row survives whichever path runs and rides every persist.
|
|
163
|
-
if (setup.datastoreToolsMissing.length > 0) {
|
|
164
|
-
initialStatus.messages.push(create(AgentMessageSchema, {
|
|
165
|
-
type: MessageType.MESSAGE_SYSTEM,
|
|
166
|
-
content: formatDatastoreDegradationNotice(
|
|
167
|
-
setup.agent.spec?.datastoreUsages?.length ?? 0,
|
|
168
|
-
setup.datastoreToolsMissing,
|
|
169
|
-
),
|
|
170
|
-
timestamp: utcTimestamp(),
|
|
171
|
-
}));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
155
|
const statusBuilder = new StatusBuilder(executionId, initialStatus);
|
|
175
156
|
|
|
176
157
|
statusBuilder.setApprovalProvider({
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
export interface McpToolSources {
|
|
18
18
|
/** Declared MCP server usages (agent spec + session spec). */
|
|
19
19
|
readonly mcpServerUsageCount: number;
|
|
20
|
-
/** Declared datastore usages (the records attachment, T05). */
|
|
21
|
-
readonly datastoreUsageCount: number;
|
|
22
20
|
/** Serving proactive-messaging channels (the channels attachment, DD-006). */
|
|
23
21
|
readonly channelMessagingCount: number;
|
|
24
22
|
/** The serving channel id when this session IS a live channel
|
|
@@ -30,7 +28,6 @@ export interface McpToolSources {
|
|
|
30
28
|
export function shouldConnectMcp(sources: McpToolSources): boolean {
|
|
31
29
|
return (
|
|
32
30
|
sources.mcpServerUsageCount > 0 ||
|
|
33
|
-
sources.datastoreUsageCount > 0 ||
|
|
34
31
|
sources.channelMessagingCount > 0 ||
|
|
35
32
|
sources.conversationChannelId !== undefined
|
|
36
33
|
);
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
visionDisclosureLines,
|
|
22
22
|
type NotViewableEntry,
|
|
23
23
|
} from "../../shared/attachment-vision.js";
|
|
24
|
+
import {
|
|
25
|
+
downloadUrlDisclosureLine,
|
|
26
|
+
type DownloadUrlKind,
|
|
27
|
+
} from "../../shared/attachment-download-urls.js";
|
|
24
28
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
25
29
|
import {
|
|
26
30
|
buildImplementPlanDirective,
|
|
@@ -99,14 +103,6 @@ export interface PromptBuilderInput {
|
|
|
99
103
|
provisionResults: ProvisionResult[];
|
|
100
104
|
containerRoot: string;
|
|
101
105
|
skillsPromptSection: string;
|
|
102
|
-
/**
|
|
103
|
-
* The datastores section (shared/datastore-attachment.ts
|
|
104
|
-
* formatDatastoresSection): `<available_datastores>` when the connected
|
|
105
|
-
* record-tool roster is complete, `<unavailable_datastores>` when setup's
|
|
106
|
-
* reconciliation found record tools missing (issue #325). Empty when the
|
|
107
|
-
* agent uses no datastores.
|
|
108
|
-
*/
|
|
109
|
-
datastoresPromptSection?: string;
|
|
110
106
|
/**
|
|
111
107
|
* The `<available_channel_templates>` section
|
|
112
108
|
* (shared/channel-attachment.ts formatChannelTemplatesSection); absent
|
|
@@ -122,6 +118,12 @@ export interface PromptBuilderInput {
|
|
|
122
118
|
* Rendered inside the Input Files section.
|
|
123
119
|
*/
|
|
124
120
|
vision?: VisionPromptInfo;
|
|
121
|
+
/**
|
|
122
|
+
* What kind of URL the turn's storage backend mints (issue #532) — keys
|
|
123
|
+
* the Input Files section's hand-off wording (attachment-download-urls.ts).
|
|
124
|
+
* One turn-level fact: all attachments ride the one configured storage.
|
|
125
|
+
*/
|
|
126
|
+
downloadUrlKind?: DownloadUrlKind;
|
|
125
127
|
/**
|
|
126
128
|
* The execution's interaction mode. PLAN appends the shared plan-mode
|
|
127
129
|
* directive so the model knows the turn's deliverable is a plan document.
|
|
@@ -201,10 +203,6 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
201
203
|
prompt += input.skillsPromptSection;
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
if (input.datastoresPromptSection) {
|
|
205
|
-
prompt += "\n\n" + input.datastoresPromptSection;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
206
|
if (input.channelTemplatesPromptSection) {
|
|
209
207
|
prompt += "\n\n" + input.channelTemplatesPromptSection;
|
|
210
208
|
}
|
|
@@ -220,7 +218,9 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
if (input.injectedFiles.length > 0) {
|
|
223
|
-
prompt += buildInjectedFilesSection(
|
|
221
|
+
prompt += buildInjectedFilesSection(
|
|
222
|
+
input.injectedFiles, input.vision, input.downloadUrlKind,
|
|
223
|
+
);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (input.senderIdentity) {
|
|
@@ -401,6 +401,7 @@ function buildReferencedFilesSection(
|
|
|
401
401
|
function buildInjectedFilesSection(
|
|
402
402
|
files: readonly InjectedFile[],
|
|
403
403
|
vision?: VisionPromptInfo,
|
|
404
|
+
downloadUrlKind?: DownloadUrlKind,
|
|
404
405
|
): string {
|
|
405
406
|
let section = "\n\n## Input Files\n\n";
|
|
406
407
|
section +=
|
|
@@ -416,12 +417,23 @@ function buildInjectedFilesSection(
|
|
|
416
417
|
const sizeInfo = ` (${f.sizeBytes} bytes)`;
|
|
417
418
|
// A duplicate-renamed file (attachment-naming.ts) discloses its original
|
|
418
419
|
// name so the agent can connect "the two report.pdfs" in the user's
|
|
419
|
-
// message to distinct files on disk.
|
|
420
|
+
// message to distinct files on disk. A file with a minted download URL
|
|
421
|
+
// (attachment-download-urls.ts) lists it beside the path for the remote
|
|
422
|
+
// hand-off story.
|
|
420
423
|
const renameInfo =
|
|
421
424
|
f.renamedFrom !== undefined
|
|
422
425
|
? ` (renamed from duplicate '${f.renamedFrom}')`
|
|
423
426
|
: "";
|
|
424
|
-
|
|
427
|
+
const urlInfo =
|
|
428
|
+
f.downloadUrl !== undefined ? ` — download URL: ${f.downloadUrl}` : "";
|
|
429
|
+
section += `- \`${f.path}\`${sizeInfo}${renameInfo}${urlInfo}\n`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The URL hand-off line (shared wording, attachment-download-urls.ts)
|
|
433
|
+
// renders only when some listed file actually carries a URL — its wording
|
|
434
|
+
// keys on what kind of URL the storage backend mints.
|
|
435
|
+
if (downloadUrlKind !== undefined && files.some((f) => f.downloadUrl !== undefined)) {
|
|
436
|
+
section += "\n" + downloadUrlDisclosureLine(downloadUrlKind) + "\n";
|
|
425
437
|
}
|
|
426
438
|
|
|
427
439
|
// The vision lines (shared wording, attachment-vision.ts) tell the model
|
|
@@ -36,12 +36,6 @@ import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-ma
|
|
|
36
36
|
import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
37
37
|
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
38
38
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
39
|
-
import {
|
|
40
|
-
DATASTORE_ATTACHMENT_SLUG,
|
|
41
|
-
formatDatastoresSection,
|
|
42
|
-
missingRecordTools,
|
|
43
|
-
synthesizeDatastoreAttachment,
|
|
44
|
-
} from "../../shared/datastore-attachment.js";
|
|
45
39
|
import {
|
|
46
40
|
discoverChannelMessaging,
|
|
47
41
|
formatChannelTemplatesSection,
|
|
@@ -129,16 +123,6 @@ export interface SetupResult {
|
|
|
129
123
|
readonly session: Session;
|
|
130
124
|
readonly workspaceBackend: WorkspaceBackend;
|
|
131
125
|
readonly mcpConnection: McpConnectionResult | null;
|
|
132
|
-
/**
|
|
133
|
-
* Record tools declared via datastore_usages but absent from the connected
|
|
134
|
-
* stigmer-records roster this turn (issue #325). Empty = healthy (also when
|
|
135
|
-
* the agent uses no datastores). Non-empty selects the degraded prompt
|
|
136
|
-
* section in Step 8, and the activity surfaces it to operators as a
|
|
137
|
-
* MESSAGE_SYSTEM row — the vision-facts pattern: setup computes the honest
|
|
138
|
-
* struct, the caller discloses. Recomputed every turn, so a healed outage
|
|
139
|
-
* stops disclosing on the next turn.
|
|
140
|
-
*/
|
|
141
|
-
readonly datastoreToolsMissing: readonly string[];
|
|
142
126
|
readonly mergedEnvVars: Record<string, string>;
|
|
143
127
|
readonly secretKeys: ReadonlySet<string>;
|
|
144
128
|
readonly modelName: string;
|
|
@@ -369,7 +353,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
369
353
|
agent.spec!.mcpServerUsages || [],
|
|
370
354
|
session.spec!.mcpServerUsages || [],
|
|
371
355
|
);
|
|
372
|
-
const datastoreUsages = agent.spec!.datastoreUsages || [];
|
|
373
356
|
|
|
374
357
|
// The synthesized attachments' credential story (DD-006 D4): the
|
|
375
358
|
// exchanged token authenticates the discovery reads per-call (the
|
|
@@ -410,7 +393,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
410
393
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
411
394
|
if (shouldConnectMcp({
|
|
412
395
|
mcpServerUsageCount: mcpServerUsages.length,
|
|
413
|
-
datastoreUsageCount: datastoreUsages.length,
|
|
414
396
|
channelMessagingCount: channelMessaging.length,
|
|
415
397
|
conversationChannelId,
|
|
416
398
|
})) {
|
|
@@ -444,25 +426,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
444
426
|
envResult.secretKeys,
|
|
445
427
|
);
|
|
446
428
|
|
|
447
|
-
// The
|
|
448
|
-
// backfill so the destructiveHint tightener can never gate
|
|
449
|
-
// approval maps keep it approval-free by construction
|
|
450
|
-
// shared/datastore-attachment.ts).
|
|
451
|
-
if (datastoreUsages.length > 0) {
|
|
452
|
-
const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
|
|
453
|
-
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
454
|
-
credential: attachmentCredential,
|
|
455
|
-
backendEndpoint: config.stigmerBackendEndpoint,
|
|
456
|
-
});
|
|
457
|
-
if (attachment) {
|
|
458
|
-
backfilledServers = injectSynthesizedAttachment(
|
|
459
|
-
backfilledServers, attachment, "datastore records",
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// The channel messaging attachment (DD-006 D7/D8) — the records
|
|
465
|
-
// attachment's twin, injected under the same after-backfill rule.
|
|
429
|
+
// The channel messaging attachment (DD-006 D7/D8) — injected AFTER
|
|
430
|
+
// resolve + backfill so the destructiveHint tightener can never gate
|
|
431
|
+
// it; empty approval maps keep it approval-free by construction.
|
|
466
432
|
if (channelMessaging.length > 0) {
|
|
467
433
|
const attachment = synthesizeChannelAttachment(channelMessaging, {
|
|
468
434
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
@@ -477,8 +443,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
477
443
|
}
|
|
478
444
|
|
|
479
445
|
// The conversation participation attachment (DD-008 D-c) — the
|
|
480
|
-
//
|
|
481
|
-
// answers undefined with no bridge endpoint by design (see
|
|
446
|
+
// channel attachment's sibling, same after-backfill rule. HTTP-only:
|
|
447
|
+
// synthesize answers undefined with no bridge endpoint by design (see
|
|
482
448
|
// shared/conversation-attachment.ts).
|
|
483
449
|
const conversationAttachment = synthesizeConversationAttachment(conversationChannelId, {
|
|
484
450
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
@@ -585,37 +551,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
585
551
|
}
|
|
586
552
|
timing.mark("inject_attachments");
|
|
587
553
|
|
|
588
|
-
// Reconcile the declared datastores against the record tools actually
|
|
589
|
-
// connected (issue #325): the records roster always serves all five
|
|
590
|
-
// tools, so any absence means the store is degraded, and the prompt
|
|
591
|
-
// must say so instead of promising tools the agent does not have. The
|
|
592
|
-
// connected roster is post-connect truth — when usages exist, MCP
|
|
593
|
-
// connect always ran above (a thrown connect already failed the run).
|
|
594
|
-
const datastoreToolsMissing = datastoreUsages.length > 0 && mcpConnection
|
|
595
|
-
? missingRecordTools(
|
|
596
|
-
(mcpConnection.serverToolMap[DATASTORE_ATTACHMENT_SLUG] ?? [])
|
|
597
|
-
.map((t) => t.name),
|
|
598
|
-
)
|
|
599
|
-
: [];
|
|
600
|
-
if (datastoreToolsMissing.length > 0) {
|
|
601
|
-
console.warn(
|
|
602
|
-
`[setup] Datastore record tools degraded: declared ` +
|
|
603
|
-
`${datastoreUsages.length} datastore usage(s) but the ` +
|
|
604
|
-
`'${DATASTORE_ATTACHMENT_SLUG}' roster is missing ` +
|
|
605
|
-
`[${datastoreToolsMissing.join(", ")}] — the prompt discloses the ` +
|
|
606
|
-
`outage instead of the available-datastores section.`,
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
554
|
// Step 8: Build enhanced system prompt
|
|
611
555
|
const systemPrompt = buildEnhancedSystemPrompt({
|
|
612
556
|
instructions,
|
|
613
557
|
provisionResults,
|
|
614
558
|
containerRoot: workspaceBackend.rootDir,
|
|
615
559
|
skillsPromptSection,
|
|
616
|
-
datastoresPromptSection: datastoreUsages.length > 0
|
|
617
|
-
? formatDatastoresSection(datastoreUsages, datastoreToolsMissing)
|
|
618
|
-
: undefined,
|
|
619
560
|
// "" (nothing sendable) threads as undefined: the tool alone still
|
|
620
561
|
// serves text sends inside a 24-hour window (DD-006 D6).
|
|
621
562
|
channelTemplatesPromptSection: channelMessaging.length > 0
|
|
@@ -625,6 +566,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
625
566
|
workspaceRoot: workspaceBackend.rootDir,
|
|
626
567
|
injectedFiles,
|
|
627
568
|
vision: visionPromptInfo,
|
|
569
|
+
downloadUrlKind: artifactStorage?.downloadUrlKind,
|
|
628
570
|
interactionMode: execution.spec!.executionConfig?.interactionMode,
|
|
629
571
|
buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
|
|
630
572
|
contextBridge: readContextBridge(session.spec!.metadata),
|
|
@@ -936,7 +878,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
936
878
|
session,
|
|
937
879
|
workspaceBackend,
|
|
938
880
|
mcpConnection,
|
|
939
|
-
datastoreToolsMissing,
|
|
940
881
|
mergedEnvVars: envResult.mergedEnvVars,
|
|
941
882
|
secretKeys: envResult.secretKeys,
|
|
942
883
|
modelName,
|
package/src/config.ts
CHANGED
|
@@ -61,11 +61,12 @@ export interface Config {
|
|
|
61
61
|
readonly stigmerBackendEndpoint: string;
|
|
62
62
|
readonly stigmerToken: string | null;
|
|
63
63
|
/**
|
|
64
|
-
* The MCP bridge endpoint for the runner-synthesized
|
|
65
|
-
*
|
|
66
|
-
* https://mcp.stigmer.ai). Null
|
|
67
|
-
* spawned `stigmer mcp-server` stdio
|
|
68
|
-
* See shared/
|
|
64
|
+
* The MCP bridge endpoint for the runner-synthesized attachments —
|
|
65
|
+
* channel messaging and conversation participation
|
|
66
|
+
* (STIGMER_MCP_BRIDGE_ENDPOINT, e.g. https://mcp.stigmer.ai). Null
|
|
67
|
+
* selects the OSS/local shape: a spawned `stigmer mcp-server` stdio
|
|
68
|
+
* child against the local backend. See shared/channel-attachment.ts
|
|
69
|
+
* and shared/conversation-attachment.ts.
|
|
69
70
|
*/
|
|
70
71
|
readonly mcpBridgeEndpoint: string | null;
|
|
71
72
|
/**
|
|
@@ -49,6 +49,10 @@ describe("LocalArtifactStorage", () => {
|
|
|
49
49
|
expect(url).toBe("http://localhost:7235/artifacts/file.txt");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("self-describes its URLs as local-serve (loopback reach, no expiry — issue #532)", () => {
|
|
53
|
+
expect(storage.downloadUrlKind).toBe("local-serve");
|
|
54
|
+
});
|
|
55
|
+
|
|
52
56
|
it("returns true for existing keys", async () => {
|
|
53
57
|
const key = "artifacts/exec-1/file.bin";
|
|
54
58
|
await storage.upload(key, Buffer.from("data"));
|
|
@@ -167,6 +171,11 @@ describe("ProxyArtifactStorage", () => {
|
|
|
167
171
|
globalThis.fetch = originalFetch;
|
|
168
172
|
});
|
|
169
173
|
|
|
174
|
+
it("self-describes its URLs as presigned (time-limited, remotely fetchable — issue #532)", () => {
|
|
175
|
+
const s = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
176
|
+
expect(s.downloadUrlKind).toBe("presigned");
|
|
177
|
+
});
|
|
178
|
+
|
|
170
179
|
it("uploads via presigned URL flow", async () => {
|
|
171
180
|
const calls: { url: string; method: string; body?: string }[] = [];
|
|
172
181
|
|