@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared mechanics of runner-synthesized MCP attachments — the pieces
|
|
3
|
-
* the
|
|
4
|
-
* attachment (
|
|
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. "
|
|
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.
|
|
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.
|
|
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]:
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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,
|
|
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
|
|
669
|
-
// Deliberately AFTER resolve + backfill: the attachment has
|
|
670
|
-
// McpServerUsage and reports discovered capabilities, so the
|
|
671
|
-
// backfill's destructiveHint tightener can never force-gate
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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 {
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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");
|