@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.
Files changed (81) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
  3. package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
  4. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  5. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  6. package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
  7. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  8. package/dist/activities/execute-cursor/index.d.ts +6 -2
  9. package/dist/activities/execute-cursor/index.js +16 -32
  10. package/dist/activities/execute-cursor/index.js.map +1 -1
  11. package/dist/activities/execute-cursor/prompt-builder.d.ts +15 -11
  12. package/dist/activities/execute-cursor/prompt-builder.js +20 -10
  13. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +11 -0
  15. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
  16. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  17. package/dist/activities/execute-deep-agent/index.js +0 -15
  18. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  19. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
  20. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
  21. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  22. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
  23. package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
  24. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  25. package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
  26. package/dist/activities/execute-deep-agent/setup.js +6 -42
  27. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  28. package/dist/config.d.ts +6 -5
  29. package/dist/config.js.map +1 -1
  30. package/dist/shared/artifact-storage.d.ts +11 -0
  31. package/dist/shared/artifact-storage.js +2 -0
  32. package/dist/shared/artifact-storage.js.map +1 -1
  33. package/dist/shared/attachment-download-urls.d.ts +62 -0
  34. package/dist/shared/attachment-download-urls.js +79 -0
  35. package/dist/shared/attachment-download-urls.js.map +1 -0
  36. package/dist/shared/channel-attachment.d.ts +1 -1
  37. package/dist/shared/channel-attachment.js +1 -1
  38. package/dist/shared/conversation-attachment.d.ts +2 -3
  39. package/dist/shared/conversation-attachment.js +2 -3
  40. package/dist/shared/conversation-attachment.js.map +1 -1
  41. package/dist/shared/mcp-transport-guard.d.ts +2 -2
  42. package/dist/shared/mcp-transport-guard.js +2 -2
  43. package/dist/shared/synthesized-attachment.d.ts +4 -4
  44. package/dist/shared/synthesized-attachment.js +4 -4
  45. package/package.json +2 -2
  46. package/src/__test-utils__/fake-artifact-storage.ts +10 -1
  47. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
  48. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +62 -29
  49. package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
  50. package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
  51. package/src/activities/execute-cursor/index.ts +22 -37
  52. package/src/activities/execute-cursor/prompt-builder.ts +38 -22
  53. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
  54. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
  55. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
  56. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
  57. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
  58. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +71 -0
  59. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
  60. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
  61. package/src/activities/execute-deep-agent/index.ts +0 -19
  62. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
  63. package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
  64. package/src/activities/execute-deep-agent/setup.ts +6 -65
  65. package/src/config.ts +6 -5
  66. package/src/shared/__tests__/artifact-storage.test.ts +9 -0
  67. package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
  68. package/src/shared/__tests__/channel-attachment.test.ts +2 -2
  69. package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
  70. package/src/shared/artifact-storage.ts +11 -0
  71. package/src/shared/attachment-download-urls.ts +98 -0
  72. package/src/shared/channel-attachment.ts +1 -1
  73. package/src/shared/conversation-attachment.ts +2 -3
  74. package/src/shared/mcp-transport-guard.ts +2 -2
  75. package/src/shared/synthesized-attachment.ts +4 -4
  76. package/dist/shared/datastore-attachment.d.ts +0 -99
  77. package/dist/shared/datastore-attachment.js +0 -184
  78. package/dist/shared/datastore-attachment.js.map +0 -1
  79. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
  80. package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
  81. 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 datastores,
39
- // no proactive channel — the escalation tool is the ONLY source.
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",
@@ -196,7 +196,6 @@ function fakeMemoryGateSetup(opts: {
196
196
  readFile: vi.fn(async () => ""),
197
197
  },
198
198
  mcpConnection: null,
199
- datastoreToolsMissing: [],
200
199
  mergedEnvVars: {},
201
200
  secretKeys: new Set<string>(),
202
201
  modelName: "claude-sonnet",
@@ -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(input.injectedFiles, input.vision);
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
- section += `- \`${f.path}\`${sizeInfo}${renameInfo}\n`;
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 datastore records attachment (T05) — injected AFTER resolve +
448
- // backfill so the destructiveHint tightener can never gate it; empty
449
- // approval maps keep it approval-free by construction (see
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
- // third sibling, same after-backfill rule. HTTP-only: synthesize
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 datastore
65
- * records attachment (STIGMER_MCP_BRIDGE_ENDPOINT, e.g.
66
- * https://mcp.stigmer.ai). Null selects the OSS/local shape: a
67
- * spawned `stigmer mcp-server` stdio child against the local backend.
68
- * See shared/datastore-attachment.ts.
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