@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
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Shared mechanics of runner-synthesized MCP attachments — the pieces
3
- * the datastore records attachment (T05) and the channel messaging
4
- * attachment (proactive-messaging DD-006 D8) have in common, extracted
5
- * when the second attachment arrived.
3
+ * the channel messaging attachment (proactive-messaging DD-006 D8) and
4
+ * the conversation participation attachment (DD-008 D-c) have in
5
+ * common, extracted when the second attachment arrived.
6
6
  *
7
7
  * A synthesized attachment is a first-party MCP server entry the runner
8
8
  * builds itself (no McpServer resource, no Environment, no credential in
@@ -17,7 +17,7 @@
17
17
  * Inject a synthesized attachment into a resolved server list — AFTER
18
18
  * resolve + backfill (see the module header). A user server shadowing
19
19
  * the reserved slug is replaced, loudly; `label` names the attachment
20
- * in that warning (e.g. "datastore records").
20
+ * in that warning (e.g. "channel messaging").
21
21
  */
22
22
  export function injectSynthesizedAttachment(resolvedServers, attachment, label) {
23
23
  const shadowed = resolvedServers.some((s) => s.slug === attachment.slug);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/runner",
3
- "version": "3.11.1-dev.20260812192248",
3
+ "version": "3.12.0",
4
4
  "description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -90,7 +90,7 @@
90
90
  "@opentelemetry/sdk-metrics": "^2.0.0",
91
91
  "@opentelemetry/sdk-trace-base": "^2.0.0",
92
92
  "@opentelemetry/sdk-trace-node": "^2.0.0",
93
- "@stigmer/protos": "3.11.1-dev.20260812192248",
93
+ "@stigmer/protos": "3.12.0",
94
94
  "@temporalio/activity": "^1.11.0",
95
95
  "@temporalio/common": "^1.11.0",
96
96
  "@temporalio/interceptors-opentelemetry": "~1.16.0",
@@ -25,12 +25,20 @@ import type { ArtifactStorage } from "../shared/artifact-storage.js";
25
25
 
26
26
  /** An {@link ArtifactStorage} whose methods are `vi.fn` spies over a shared Map. */
27
27
  export type InMemoryArtifactStorage = {
28
- [K in keyof ArtifactStorage]: ReturnType<typeof vi.fn>;
28
+ [K in keyof ArtifactStorage as ArtifactStorage[K] extends (...args: never[]) => unknown
29
+ ? K
30
+ : never]: ReturnType<typeof vi.fn>;
29
31
  } & ArtifactStorage;
30
32
 
31
33
  export interface InMemoryArtifactStorageOptions {
32
34
  /** Prefix for the URLs `getDownloadUrl` returns. Defaults to `mem://`. */
33
35
  readonly urlBase?: string;
36
+ /**
37
+ * The URL kind the double self-describes as (attachment-download-urls.ts).
38
+ * Defaults to "presigned" — the backend whose URLs the hand-off story is
39
+ * built for; tests exercising the local-serve wording pass "local-serve".
40
+ */
41
+ readonly downloadUrlKind?: ArtifactStorage["downloadUrlKind"];
34
42
  }
35
43
 
36
44
  export interface InMemoryArtifactStorageHandle {
@@ -55,6 +63,7 @@ export function makeInMemoryArtifactStorage(
55
63
  const blobs = new Map<string, Buffer>();
56
64
 
57
65
  const storage = {
66
+ downloadUrlKind: opts.downloadUrlKind ?? "presigned",
58
67
  upload: vi.fn(async (key: string, content: Buffer, _contentType?: string) => {
59
68
  blobs.set(key, Buffer.from(content));
60
69
  return key;
@@ -78,7 +78,11 @@ describe("resolveAttachments", () => {
78
78
  const result = await resolveAttachments([makeAttachment()], options({ storage }));
79
79
 
80
80
  expect(result).toEqual([
81
- { filename: "plan.md", relativePath: ".stigmer/inputs/plan.md" },
81
+ {
82
+ filename: "plan.md",
83
+ relativePath: ".stigmer/inputs/plan.md",
84
+ downloadUrl: "mem://attachments/01ABC/plan.md",
85
+ },
82
86
  ]);
83
87
  expect(readFileSync(join(platformDir, "inputs", "plan.md"), "utf-8")).toBe("# The Plan");
84
88
  });
@@ -130,11 +134,16 @@ describe("resolveAttachments", () => {
130
134
  );
131
135
 
132
136
  expect(result).toEqual([
133
- { filename: "report.pdf", relativePath: ".stigmer/inputs/report.pdf" },
137
+ {
138
+ filename: "report.pdf",
139
+ relativePath: ".stigmer/inputs/report.pdf",
140
+ downloadUrl: "mem://attachments/01AAA/report.pdf",
141
+ },
134
142
  {
135
143
  filename: "report-2.pdf",
136
144
  relativePath: ".stigmer/inputs/report-2.pdf",
137
145
  renamedFrom: "report.pdf",
146
+ downloadUrl: "mem://attachments/01BBB/report.pdf",
138
147
  },
139
148
  ]);
140
149
  expect(readFileSync(join(platformDir, "inputs", "report.pdf"), "utf-8")).toBe("first bytes");
@@ -155,12 +164,15 @@ describe("resolveAttachments", () => {
155
164
  options({ storage }),
156
165
  );
157
166
 
167
+ // The key-less local file lists no URL; the storage twin does — the
168
+ // per-file split the prompt renders.
158
169
  expect(result).toEqual([
159
170
  { filename: "notes.md", relativePath: ".stigmer/inputs/notes.md" },
160
171
  {
161
172
  filename: "notes-2.md",
162
173
  relativePath: ".stigmer/inputs/notes-2.md",
163
174
  renamedFrom: "notes.md",
175
+ downloadUrl: "mem://attachments/01ABC/notes.md",
164
176
  },
165
177
  ]);
166
178
  expect(readFileSync(join(platformDir, "inputs", "notes.md"), "utf-8")).toBe("local copy");
@@ -225,7 +237,11 @@ describe("resolveAttachments", () => {
225
237
  );
226
238
 
227
239
  expect(result).toEqual([
228
- { filename: "evil.md", relativePath: ".stigmer/inputs/evil.md" },
240
+ {
241
+ filename: "evil.md",
242
+ relativePath: ".stigmer/inputs/evil.md",
243
+ downloadUrl: "mem://attachments/01ABC/x",
244
+ },
229
245
  ]);
230
246
  expect(readFileSync(join(platformDir, "inputs", "evil.md"), "utf-8")).toBe("contained");
231
247
  // Nothing escaped two levels up (where `../../evil.md` would have landed).
@@ -247,6 +263,50 @@ describe("resolveAttachments", () => {
247
263
  expect(readFileSync(join(platformDir, "inputs", "evil.txt"), "utf-8")).toBe("local-contained");
248
264
  });
249
265
 
266
+ // ── Download-URL hand-off (issue #532) ───────────────────────────────────
267
+ // The URL is strictly additive (shared/attachment-download-urls.ts): every
268
+ // case below also asserts the file materialized exactly as it would
269
+ // without one.
270
+
271
+ describe("download-URL minting during resolution", () => {
272
+ it("degrades to no URL when the mint fails — file still materialized, turn proceeds", async () => {
273
+ const { storage } = makeInMemoryArtifactStorage();
274
+ await storage.upload("attachments/01ABC/plan.md", Buffer.from("# The Plan"), "text/markdown");
275
+ storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
276
+
277
+ const result = await resolveAttachments([makeAttachment()], options({ storage }));
278
+
279
+ expect(result).toEqual([
280
+ { filename: "plan.md", relativePath: ".stigmer/inputs/plan.md" },
281
+ ]);
282
+ expect(readFileSync(join(platformDir, "inputs", "plan.md"), "utf-8")).toBe("# The Plan");
283
+ });
284
+
285
+ it("mints a URL on the localPath fast path when an uploaded copy also exists", async () => {
286
+ // The fast path skips storage for the BYTES; the mint rule is
287
+ // branch-independent, so the uploaded copy still backs the URL.
288
+ const srcPath = join(workspaceDir, "src.csv");
289
+ writeFileSync(srcPath, "a,b,c");
290
+ const { storage } = makeInMemoryArtifactStorage();
291
+
292
+ const result = await resolveAttachments(
293
+ [makeAttachment({ filename: "data.csv", storageKey: "attachments/01ABC/data.csv", localPath: srcPath })],
294
+ options({ storage }),
295
+ );
296
+
297
+ expect(result).toEqual([
298
+ {
299
+ filename: "data.csv",
300
+ relativePath: ".stigmer/inputs/data.csv",
301
+ downloadUrl: "mem://attachments/01ABC/data.csv",
302
+ },
303
+ ]);
304
+ // Bytes came off local disk, not storage — the fast path is intact.
305
+ expect(storage.download).not.toHaveBeenCalled();
306
+ expect(readFileSync(join(platformDir, "inputs", "data.csv"), "utf-8")).toBe("a,b,c");
307
+ });
308
+ });
309
+
250
310
  // ── Vision selection (T04) ────────────────────────────────────────────────
251
311
  // Vision is strictly additive: every case below also asserts the file
252
312
  // materialized exactly as it would without a budget.
@@ -9,10 +9,8 @@
9
9
 
10
10
  import { describe, it, expect } from "vitest";
11
11
  import { create } from "@bufbuild/protobuf";
12
- import { DatastoreUsageSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
13
12
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
13
  import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
15
- import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
16
14
 
17
15
  import { appendStructuredOutputDirective, buildPrompt, isHitlReinvocation, primarySendCarriesImages } from "../index.js";
18
16
  import type { BuildPromptInput } from "../index.js";
@@ -89,33 +87,6 @@ describe("buildPrompt", () => {
89
87
  expect(prompt).toContain(USER_MESSAGE);
90
88
  });
91
89
 
92
- it("renders the <available_datastores> section on the first execution (T05)", () => {
93
- const prompt = buildPrompt(
94
- input({
95
- resolution: resolution("local", "created_first_execution"),
96
- datastoreUsages: [
97
- create(DatastoreUsageSchema, {
98
- datastoreRef: create(ApiResourceReferenceSchema, { slug: "clinic" }),
99
- }),
100
- ],
101
- }),
102
- );
103
- expect(prompt).toContain("<available_datastores>");
104
- expect(prompt).toContain("- clinic");
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");
110
- });
111
-
112
- it("omits the datastores section when the agent uses no datastores", () => {
113
- const prompt = buildPrompt(
114
- input({ resolution: resolution("local", "created_first_execution") }),
115
- );
116
- expect(prompt).not.toContain("<available_datastores>");
117
- });
118
-
119
90
  it("carries the rollover context bridge on the first execution (DD-013)", () => {
120
91
  const prompt = buildPrompt(
121
92
  input({
@@ -454,6 +425,68 @@ describe("attachments on a resumed turn (T04 — the mid-session WhatsApp case)"
454
425
  expect(prompt).toContain("Attached inline and visible to you, in order: 1. a.jpg");
455
426
  });
456
427
 
428
+ it("lists a minted download URL beside its file with the presigned hand-off line (issue #532)", () => {
429
+ const prompt = buildPrompt(
430
+ input({
431
+ ...RESUMED,
432
+ attachments: [
433
+ { path: ".stigmer/inputs/lease.pdf", downloadUrl: "https://r2.example/lease?sig=abc" },
434
+ { path: ".stigmer/inputs/notes.md" },
435
+ ],
436
+ downloadUrlKind: "presigned",
437
+ }),
438
+ );
439
+ expect(prompt).toContain(
440
+ "- `.stigmer/inputs/lease.pdf` — download URL: https://r2.example/lease?sig=abc",
441
+ );
442
+ // The URL-less file keeps a clean entry.
443
+ expect(prompt).toContain("- `.stigmer/inputs/notes.md`\n");
444
+ expect(prompt).toContain("These URLs are time-limited");
445
+ });
446
+
447
+ it("words a local-serve URL honestly — reachable only from this machine", () => {
448
+ const prompt = buildPrompt(
449
+ input({
450
+ ...RESUMED,
451
+ attachments: [
452
+ { path: ".stigmer/inputs/lease.pdf", downloadUrl: "http://localhost:7235/attachments/01A/lease.pdf" },
453
+ ],
454
+ downloadUrlKind: "local-serve",
455
+ }),
456
+ );
457
+ expect(prompt).toContain("download URL: http://localhost:7235/attachments/01A/lease.pdf");
458
+ expect(prompt).toContain("reachable only from this machine");
459
+ expect(prompt).not.toContain("time-limited");
460
+ });
461
+
462
+ it("renders no hand-off line when no listed file carries a URL (kind alone is not enough)", () => {
463
+ const prompt = buildPrompt(
464
+ input({
465
+ ...RESUMED,
466
+ attachments: [{ path: ".stigmer/inputs/local-only.csv" }],
467
+ downloadUrlKind: "presigned",
468
+ }),
469
+ );
470
+ expect(prompt).toContain("<input_files>");
471
+ expect(prompt).not.toContain("download URL");
472
+ expect(prompt).not.toContain("time-limited");
473
+ });
474
+
475
+ it("renders the download URL inside the enhanced first-turn prompt too", () => {
476
+ const prompt = buildPrompt(
477
+ input({
478
+ resolution: resolution("local", "created_first_execution"),
479
+ attachments: [
480
+ { path: ".stigmer/inputs/lease.pdf", downloadUrl: "https://r2.example/lease?sig=abc" },
481
+ ],
482
+ downloadUrlKind: "presigned",
483
+ }),
484
+ );
485
+ expect(prompt).toContain("<input_files>");
486
+ expect(prompt).toContain("download URL: https://r2.example/lease?sig=abc");
487
+ expect(prompt).toContain("These URLs are time-limited");
488
+ });
489
+
457
490
  it("a HITL re-invocation carries no input-files section — its prompt has no user message at all", () => {
458
491
  const prompt = buildPrompt(
459
492
  input({
@@ -40,6 +40,7 @@ import { mkdir, copyFile, readFile, stat, writeFile } from "node:fs/promises";
40
40
  import { join, basename } from "node:path";
41
41
  import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
42
42
  import type { ArtifactStorage } from "../../shared/artifact-storage.js";
43
+ import { mintAttachmentDownloadUrl } from "../../shared/attachment-download-urls.js";
43
44
  import { allocateUniqueName } from "../../shared/attachment-naming.js";
44
45
  import {
45
46
  isVisionCandidate,
@@ -73,6 +74,17 @@ export interface ResolvedAttachment {
73
74
  * Attachments that were never image-shaped carry neither field.
74
75
  */
75
76
  visionDegraded?: VisionDegradedReason;
77
+ /**
78
+ * A download URL for the attachment's stored object, listed in the prompt's
79
+ * `<input_files>` section so the agent can hand the file to a tool whose
80
+ * backend cannot read the sandbox filesystem (issue #532). Minted whenever
81
+ * the attachment has a `storageKey` and a usable storage — including on the
82
+ * localPath fast path when an uploaded copy also exists. Absent when there
83
+ * is nothing to mint or the mint failed (non-fatal — the policy, the
84
+ * degrade log, and the prompt wording all live in
85
+ * shared/attachment-download-urls.ts).
86
+ */
87
+ downloadUrl?: string;
76
88
  }
77
89
 
78
90
  export interface AttachmentResolverOptions {
@@ -165,11 +177,18 @@ async function resolveAttachment(
165
177
  `${err instanceof Error ? err.message : String(err)}`,
166
178
  );
167
179
  }
180
+ // The fast path skips storage for the BYTES, but an uploaded copy (an
181
+ // attachment carrying both localPath and storageKey) still supports the
182
+ // URL hand-off — the mint rule is branch-independent.
183
+ const downloadUrl = await mintAttachmentDownloadUrl(
184
+ options.storage, attachment.storageKey, filename,
185
+ );
168
186
  return {
169
187
  filename,
170
188
  relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
171
189
  ...(renamedFrom !== undefined ? { renamedFrom } : {}),
172
190
  ...visionOutcomeFields(vision),
191
+ ...(downloadUrl !== undefined ? { downloadUrl } : {}),
173
192
  };
174
193
  }
175
194
 
@@ -209,11 +228,16 @@ async function resolveAttachment(
209
228
  // no pre-filter needed on this branch).
210
229
  const vision = options.visionBudget?.offer(filename, attachment.contentType, content);
211
230
 
231
+ const downloadUrl = await mintAttachmentDownloadUrl(
232
+ options.storage, attachment.storageKey, filename,
233
+ );
234
+
212
235
  return {
213
236
  filename,
214
237
  relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
215
238
  ...(renamedFrom !== undefined ? { renamedFrom } : {}),
216
239
  ...visionOutcomeFields(vision),
240
+ ...(downloadUrl !== undefined ? { downloadUrl } : {}),
217
241
  };
218
242
  }
219
243
 
@@ -14,7 +14,7 @@
14
14
  import { resolve } from "node:path";
15
15
  import type { StigmerClient } from "../../client/stigmer-client.js";
16
16
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
17
- import type { AgentSpec, DatastoreUsage, McpServerUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
17
+ import type { AgentSpec, McpServerUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
18
18
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
19
19
  import type { SessionSpec } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
20
20
  import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
@@ -46,14 +46,6 @@ export interface ResolvedBlueprint {
46
46
  subAgents: SubAgent[];
47
47
  mergedMcpServerUsages: McpServerUsage[];
48
48
  mergedSkillRefs: ApiResourceReference[];
49
- /**
50
- * The agent's datastore usages (AgentSpec only — sessions carry no
51
- * datastore overlay by design: the usage edge is authorization-bearing
52
- * for the server's reach chain, DD-006, so it binds per-blueprint).
53
- * Non-empty triggers the synthesized records attachment
54
- * (shared/datastore-attachment.ts).
55
- */
56
- datastoreUsages: DatastoreUsage[];
57
49
  workspaceDirs: string[];
58
50
  cloudRepos: CloudRepo[];
59
51
  }
@@ -98,7 +90,6 @@ export async function resolveBlueprint(
98
90
  subAgents: agentSpec.subAgents,
99
91
  mergedMcpServerUsages,
100
92
  mergedSkillRefs,
101
- datastoreUsages: agentSpec.datastoreUsages,
102
93
  workspaceDirs,
103
94
  cloudRepos,
104
95
  };
@@ -72,7 +72,6 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
72
72
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
73
73
  import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
74
74
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
75
- import { synthesizeDatastoreAttachment } from "../../shared/datastore-attachment.js";
76
75
  import {
77
76
  discoverChannelMessaging,
78
77
  synthesizeChannelAttachment,
@@ -665,35 +664,16 @@ async function executeCursorInner(
665
664
  ?? config.stigmerTokenRef?.current
666
665
  ?? config.stigmerToken;
667
666
 
668
- // Phase 4a2: Synthesize the datastore records attachment (T05).
669
- // Deliberately AFTER resolve + backfill: the attachment has no
670
- // McpServerUsage and reports discovered capabilities, so the
671
- // backfill's destructiveHint tightener can never force-gate
672
- // delete_record (which on channels would be silently skipped).
673
- // Empty approval maps keep it approval-free by construction.
674
- if (blueprint.datastoreUsages.length > 0) {
675
- const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
676
- bridgeEndpoint: config.mcpBridgeEndpoint,
677
- credential: attachmentCredential,
678
- backendEndpoint: config.stigmerBackendEndpoint,
679
- });
680
- if (attachment) {
681
- const resolvedServers = injectSynthesizedAttachment(
682
- mcpResolution.resolvedServers, attachment, "datastore records",
683
- );
684
- mcpResolution = {
685
- resolvedServers,
686
- cursorConfig: toCursorMcpConfig(resolvedServers),
687
- };
688
- }
689
- }
690
-
691
- // Phase 4a3: Synthesize the channel messaging attachment (DD-006
692
- // D7/D8), the records attachment's twin. The discovery read is the
693
- // attachment decision — the control plane runs the SAME candidate
694
- // computation the send authorization uses — and every failure mode
695
- // (no channel, OSS, registry down, pre-3a control plane) degrades
696
- // to honest absence: no tool, no section, execution unharmed.
667
+ // Phase 4a2: Synthesize the channel messaging attachment (DD-006
668
+ // D7/D8). Deliberately AFTER resolve + backfill: the attachment has
669
+ // no McpServerUsage and reports discovered capabilities, so the
670
+ // backfill's destructiveHint tightener can never force-gate its
671
+ // tools; empty approval maps keep it approval-free by construction.
672
+ // The discovery read is the attachment decision — the control plane
673
+ // runs the SAME candidate computation the send authorization uses —
674
+ // and every failure mode (no channel, OSS, registry down, pre-3a
675
+ // control plane) degrades to honest absence: no tool, no section,
676
+ // execution unharmed.
697
677
  const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
698
678
  if (channelMessaging.length > 0) {
699
679
  const attachment = synthesizeChannelAttachment(channelMessaging, {
@@ -802,6 +782,7 @@ async function executeCursorInner(
802
782
  const attachmentEntries = attachmentResults.map((a) => ({
803
783
  path: a.relativePath,
804
784
  ...(a.renamedFrom !== undefined ? { renamedFrom: a.renamedFrom } : {}),
785
+ ...(a.downloadUrl !== undefined ? { downloadUrl: a.downloadUrl } : {}),
805
786
  }));
806
787
  // Vision facts, derived once from the single resolution result: the
807
788
  // images the model will see inline (in attachment order) and the ones
@@ -1147,13 +1128,13 @@ async function executeCursorInner(
1147
1128
  instructions: blueprint.instructions,
1148
1129
  userMessage: spec.message,
1149
1130
  skills: skillMetadata,
1150
- datastoreUsages: blueprint.datastoreUsages,
1151
1131
  channelMessaging,
1152
1132
  subAgents: blueprint.subAgents,
1153
1133
  workspaceDirs: blueprint.workspaceDirs,
1154
1134
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1155
1135
  attachments: attachmentEntries,
1156
1136
  vision: visionPromptInfo,
1137
+ downloadUrlKind: artifactStorage?.downloadUrlKind,
1157
1138
  pendingApprovals: adjudicatedApprovals,
1158
1139
  appliedToolCallIds,
1159
1140
  interactionMode,
@@ -1821,13 +1802,13 @@ async function executeCursorInner(
1821
1802
  instructions: blueprint.instructions,
1822
1803
  userMessage: spec.message,
1823
1804
  skills: skillMetadata,
1824
- datastoreUsages: blueprint.datastoreUsages,
1825
1805
  channelMessaging,
1826
1806
  subAgents: blueprint.subAgents,
1827
1807
  workspaceDirs: blueprint.workspaceDirs,
1828
1808
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1829
1809
  attachments: attachmentEntries,
1830
1810
  vision: visionPromptInfo,
1811
+ downloadUrlKind: artifactStorage?.downloadUrlKind,
1831
1812
  pendingApprovals: adjudicatedApprovals,
1832
1813
  // Without the applied set, the HITL-recovery prompt would tell
1833
1814
  // the fresh agent to carry out writes the runner already
@@ -2367,8 +2348,6 @@ export interface BuildPromptInput {
2367
2348
  instructions: string;
2368
2349
  userMessage: string;
2369
2350
  skills: import("./prompt-builder.js").SkillMetadata[];
2370
- /** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
2371
- datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
2372
2351
  /**
2373
2352
  * Serving proactive channels + their templates (the DD-006 D2
2374
2353
  * discovery read) — the `<available_channel_templates>` section.
@@ -2388,6 +2367,12 @@ export interface BuildPromptInput {
2388
2367
  * catchup — it rides both the enhanced prompt and a resumed turn's prefix.
2389
2368
  */
2390
2369
  vision?: import("./prompt-builder.js").VisionPromptInfo;
2370
+ /**
2371
+ * What kind of URL the turn's storage backend mints (issue #532) — keys
2372
+ * the input-files hand-off wording. Sourced from the resolved
2373
+ * artifactStorage's self-description; absent when no storage resolved.
2374
+ */
2375
+ downloadUrlKind?: import("../../shared/attachment-download-urls.js").DownloadUrlKind;
2391
2376
  pendingApprovals: import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb").PendingApproval[];
2392
2377
  /**
2393
2378
  * Approved whole-file writes the runner already applied itself (exact-apply).
@@ -2546,13 +2531,13 @@ export function buildPrompt(input: BuildPromptInput): string {
2546
2531
  instructions,
2547
2532
  userMessage,
2548
2533
  skills,
2549
- datastoreUsages: input.datastoreUsages ?? [],
2550
2534
  channelMessaging: input.channelMessaging ?? [],
2551
2535
  subAgents,
2552
2536
  workspaceDirs,
2553
2537
  workspaceFileRefs,
2554
2538
  attachments,
2555
2539
  vision: input.vision,
2540
+ downloadUrlKind: input.downloadUrlKind,
2556
2541
  interactionMode,
2557
2542
  buildFromPlan,
2558
2543
  contextBridge: input.contextBridge,
@@ -2594,7 +2579,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2594
2579
  formatInteractionModePrefix(interactionMode),
2595
2580
  formatImplementPlanSection(buildFromPlan, attachments),
2596
2581
  attachments.length > 0
2597
- ? formatInputFiles(attachments, input.vision)
2582
+ ? formatInputFiles(attachments, input.vision, input.downloadUrlKind)
2598
2583
  : undefined,
2599
2584
  conversationCatchup !== undefined
2600
2585
  ? formatConversationCatchupSection(conversationCatchup)
@@ -2612,13 +2597,13 @@ export function buildPrompt(input: BuildPromptInput): string {
2612
2597
  instructions,
2613
2598
  userMessage,
2614
2599
  skills,
2615
- datastoreUsages: input.datastoreUsages ?? [],
2616
2600
  channelMessaging: input.channelMessaging ?? [],
2617
2601
  subAgents,
2618
2602
  workspaceDirs,
2619
2603
  workspaceFileRefs,
2620
2604
  attachments,
2621
2605
  vision: input.vision,
2606
+ downloadUrlKind: input.downloadUrlKind,
2622
2607
  interactionMode,
2623
2608
  buildFromPlan,
2624
2609
  contextBridge: input.contextBridge,
@@ -17,12 +17,11 @@
17
17
  */
18
18
 
19
19
  import { resolve } from "node:path";
20
- import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
20
+ import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
21
21
  import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
22
22
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
23
23
  import { formatContextBridgeText } from "../../shared/context-bridge.js";
24
24
  import { formatConversationCatchupText } from "../../shared/conversation-catchup.js";
25
- import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
26
25
  import {
27
26
  formatChannelTemplatesSection,
28
27
  type ChannelMessagingInfo,
@@ -36,6 +35,10 @@ import {
36
35
  visionDisclosureLines,
37
36
  type NotViewableEntry,
38
37
  } from "../../shared/attachment-vision.js";
38
+ import {
39
+ downloadUrlDisclosureLine,
40
+ type DownloadUrlKind,
41
+ } from "../../shared/attachment-download-urls.js";
39
42
  import { formatTurnRecoveryText } from "./turn-recovery.js";
40
43
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
41
44
  import {
@@ -82,21 +85,18 @@ export interface AttachmentPromptEntry {
82
85
  path: string;
83
86
  /** Original filename, present only when a duplicate name was renamed. */
84
87
  renamedFrom?: string;
88
+ /**
89
+ * Download URL for the attachment's stored object, when one was minted
90
+ * (shared/attachment-download-urls.ts) — rendered beside the path so the
91
+ * agent can hand the file to tools that cannot read this filesystem.
92
+ */
93
+ downloadUrl?: string;
85
94
  }
86
95
 
87
96
  export interface EnhancedPromptOptions {
88
97
  instructions: string;
89
98
  userMessage: string;
90
99
  skills: SkillMetadata[];
91
- /**
92
- * Datastores attached via `datastore_usages` — rendered as the
93
- * `<available_datastores>` section (DD-005 SD-5, skills precedent)
94
- * pointing the model at the synthesized record tools. Always the healthy
95
- * rendering here: the Cursor SDK connects MCP itself, so this harness can
96
- * never observe the live roster — the section's standing
97
- * failure-disclosure instruction is its whole coverage (issue #325).
98
- */
99
- datastoreUsages?: DatastoreUsage[];
100
100
  /**
101
101
  * Serving proactive channels + their approved templates — rendered as
102
102
  * the `<available_channel_templates>` section (proactive-messaging
@@ -109,6 +109,12 @@ export interface EnhancedPromptOptions {
109
109
  attachments: AttachmentPromptEntry[];
110
110
  /** Inline/degraded image facts for the input-files section (T04 vision). */
111
111
  vision?: VisionPromptInfo;
112
+ /**
113
+ * What kind of URL the turn's storage backend mints — keys the input-files
114
+ * section's hand-off wording (attachment-download-urls.ts). One turn-level
115
+ * fact: all attachments ride the one configured storage.
116
+ */
117
+ downloadUrlKind?: DownloadUrlKind;
112
118
  interactionMode?: InteractionMode;
113
119
  /**
114
120
  * The execution is a Build-from-plan turn (spec.execution_config
@@ -183,10 +189,6 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
183
189
  sections.push(formatSkillsSection(options.skills));
184
190
  }
185
191
 
186
- if (options.datastoreUsages !== undefined && options.datastoreUsages.length > 0) {
187
- sections.push(formatDatastoresSection(options.datastoreUsages));
188
- }
189
-
190
192
  if (options.channelMessaging !== undefined && options.channelMessaging.length > 0) {
191
193
  // "" when nothing is sendable — the tool alone still serves text
192
194
  // sends inside a 24-hour window (DD-006 D6).
@@ -212,7 +214,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
212
214
  }
213
215
 
214
216
  if (options.attachments.length > 0) {
215
- sections.push(formatInputFiles(options.attachments, options.vision));
217
+ sections.push(formatInputFiles(options.attachments, options.vision, options.downloadUrlKind));
216
218
  }
217
219
 
218
220
  if (options.workspaceFileRefs.length > 0) {
@@ -548,15 +550,28 @@ export function formatWorkspaceContext(dirs: string[]): string {
548
550
  export function formatInputFiles(
549
551
  attachments: readonly AttachmentPromptEntry[],
550
552
  vision?: VisionPromptInfo,
553
+ downloadUrlKind?: DownloadUrlKind,
551
554
  ): string {
552
555
  // A duplicate-renamed file (attachment-naming.ts) discloses its original
553
556
  // name so the agent can connect "the two report.pdfs" in the user's
554
- // message to distinct files on disk.
555
- const entries = attachments.map((a) =>
556
- a.renamedFrom !== undefined
557
- ? `- \`${a.path}\` (renamed from duplicate '${a.renamedFrom}')`
558
- : `- \`${a.path}\``,
559
- );
557
+ // message to distinct files on disk. A file with a minted download URL
558
+ // (attachment-download-urls.ts) lists it beside the path for the remote
559
+ // hand-off story.
560
+ const entries = attachments.map((a) => {
561
+ const rename =
562
+ a.renamedFrom !== undefined
563
+ ? ` (renamed from duplicate '${a.renamedFrom}')`
564
+ : "";
565
+ const url = a.downloadUrl !== undefined ? ` — download URL: ${a.downloadUrl}` : "";
566
+ return `- \`${a.path}\`${rename}${url}`;
567
+ });
568
+ // The URL hand-off line (shared wording, attachment-download-urls.ts)
569
+ // renders only when some listed file actually carries a URL — its wording
570
+ // keys on what kind of URL the storage backend mints.
571
+ const urlDisclosure =
572
+ downloadUrlKind !== undefined && attachments.some((a) => a.downloadUrl !== undefined)
573
+ ? [downloadUrlDisclosureLine(downloadUrlKind)]
574
+ : [];
560
575
  // The vision lines (shared wording, attachment-vision.ts) tell the model
561
576
  // which of these files it can already SEE inline versus which degraded to
562
577
  // path-only — without them an agent silently ignores a photo the user
@@ -568,6 +583,7 @@ export function formatInputFiles(
568
583
  "<input_files>",
569
584
  "The following files have been provided as inputs. Read them when relevant to the task:",
570
585
  ...entries,
586
+ ...urlDisclosure,
571
587
  ...disclosure,
572
588
  "</input_files>",
573
589
  ].join("\n");