@stigmer/runner 3.11.1-dev.20260812192248 → 3.12.1
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 +34 -8
- package/dist/activities/execute-deep-agent/attachment-injector.js +111 -106
- 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 +25 -8
- 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 +13 -45
- 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/middleware/path-normalization.d.ts +15 -2
- package/dist/middleware/path-normalization.js +39 -5
- package/dist/middleware/path-normalization.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-enabled-tools.d.ts +6 -2
- package/dist/shared/mcp-enabled-tools.js +6 -2
- package/dist/shared/mcp-enabled-tools.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/plan-mode-permissions.d.ts +46 -10
- package/dist/shared/plan-mode-permissions.js +56 -12
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- package/dist/shared/zip-extract.d.ts +24 -6
- package/dist/shared/zip-extract.js +31 -90
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/shared/zip-structure.d.ts +61 -0
- package/dist/shared/zip-structure.js +128 -0
- package/dist/shared/zip-structure.js.map +1 -0
- package/package.json +2 -2
- package/src/__test-utils__/fake-artifact-storage.ts +10 -1
- package/src/__test-utils__/zip-fixtures.ts +206 -0
- 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/__tests__/skill-resolver.test.ts +3 -42
- 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 +270 -126
- 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__/plan-mode-path-normalization.test.ts +246 -37
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +78 -2
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +35 -4
- package/src/activities/execute-deep-agent/attachment-injector.ts +166 -143
- 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 +38 -16
- package/src/activities/execute-deep-agent/setup.ts +13 -68
- package/src/config.ts +6 -5
- package/src/middleware/__tests__/path-normalization.test.ts +29 -3
- package/src/middleware/path-normalization.ts +42 -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__/plan-mode-permissions.test.ts +58 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
- package/src/shared/__tests__/zip-extract.test.ts +106 -89
- 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-enabled-tools.ts +6 -2
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/plan-mode-permissions.ts +59 -12
- package/src/shared/synthesized-attachment.ts +4 -4
- package/src/shared/zip-extract.ts +35 -117
- package/src/shared/zip-structure.ts +181 -0
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ZIP fixture builder for runner unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Emits complete, real-shaped archives — local file headers (optionally
|
|
5
|
+
* streaming-style, the Go stdlib writer's default), payloads, central
|
|
6
|
+
* directory, and EOCD — because that is the only shape that can reach the
|
|
7
|
+
* runner: both editions' skill push gates validate artifacts with
|
|
8
|
+
* central-directory-based readers (see zip-extract.ts's module doc and
|
|
9
|
+
* design record 017). Earlier fixtures emitted local headers only, a
|
|
10
|
+
* shape no real ZIP writer produces, and were coupled to the old parser's
|
|
11
|
+
* front-to-back walk.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the conformance suite's `zipFilesStreaming()`
|
|
14
|
+
* (test/conformance/src/support/skills.ts) so unit fixtures and
|
|
15
|
+
* cross-edition fixtures share one shape.
|
|
16
|
+
*
|
|
17
|
+
* Lives in src/__test-utils__/ so `tsc --noEmit` covers it while
|
|
18
|
+
* tsconfig.build.json keeps it out of dist/.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { deflateRawSync } from "node:zlib";
|
|
22
|
+
|
|
23
|
+
export interface ZipFixtureFile {
|
|
24
|
+
name: string;
|
|
25
|
+
/** Text content is UTF-8 encoded; pass bytes directly for binary payloads. */
|
|
26
|
+
content: string | Uint8Array;
|
|
27
|
+
/** Compression method for the entry. Defaults to stored. */
|
|
28
|
+
method?: "stored" | "deflated";
|
|
29
|
+
/**
|
|
30
|
+
* Emit the entry the way Go's archive/zip does by default: general-purpose
|
|
31
|
+
* flag bit 3 set, zeroed sizes in the local header, and a trailing data
|
|
32
|
+
* descriptor (with the conventional signature) carrying the real values.
|
|
33
|
+
*/
|
|
34
|
+
streaming?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Lie about the entry's uncompressed size everywhere the writer would
|
|
37
|
+
* declare it (local header, data descriptor, central directory). Models a
|
|
38
|
+
* crafted archive whose declarations disagree with its actual payload —
|
|
39
|
+
* the input class the attachment injector's declared-size enforcement
|
|
40
|
+
* exists to reject (issue #567).
|
|
41
|
+
*/
|
|
42
|
+
declaredUncompressedSize?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ZipFixtureOptions {
|
|
46
|
+
/** Trailing archive comment appended after the EOCD record. */
|
|
47
|
+
comment?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Drop the central directory and EOCD, leaving only local headers and
|
|
50
|
+
* payloads. No real ZIP writer produces this shape — it models a download
|
|
51
|
+
* truncated before the archive's index.
|
|
52
|
+
*/
|
|
53
|
+
omitCentralDirectory?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a ZIP archive from path + content pairs. */
|
|
57
|
+
export function buildZip(files: ZipFixtureFile[], options?: ZipFixtureOptions): Uint8Array {
|
|
58
|
+
const out = new ByteWriter();
|
|
59
|
+
|
|
60
|
+
interface WrittenEntry {
|
|
61
|
+
nameBytes: Uint8Array;
|
|
62
|
+
payload: Uint8Array;
|
|
63
|
+
declaredUncompressed: number;
|
|
64
|
+
crc: number;
|
|
65
|
+
flags: number;
|
|
66
|
+
method: number;
|
|
67
|
+
localHeaderOffset: number;
|
|
68
|
+
}
|
|
69
|
+
const written: WrittenEntry[] = [];
|
|
70
|
+
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const contentBytes =
|
|
73
|
+
typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content;
|
|
74
|
+
const deflated = file.method === "deflated";
|
|
75
|
+
const payload = deflated ? new Uint8Array(deflateRawSync(contentBytes)) : contentBytes;
|
|
76
|
+
const entry: WrittenEntry = {
|
|
77
|
+
nameBytes: new TextEncoder().encode(file.name),
|
|
78
|
+
payload,
|
|
79
|
+
declaredUncompressed: file.declaredUncompressedSize ?? contentBytes.length,
|
|
80
|
+
crc: crc32(contentBytes),
|
|
81
|
+
flags: file.streaming ? 0x0008 : 0,
|
|
82
|
+
method: deflated ? 8 : 0,
|
|
83
|
+
localHeaderOffset: out.length,
|
|
84
|
+
};
|
|
85
|
+
written.push(entry);
|
|
86
|
+
|
|
87
|
+
out.u32(0x04034b50); // local file header signature
|
|
88
|
+
out.u16(20); // version needed to extract
|
|
89
|
+
out.u16(entry.flags);
|
|
90
|
+
out.u16(entry.method);
|
|
91
|
+
out.u16(0); // mod time
|
|
92
|
+
out.u16(0); // mod date
|
|
93
|
+
out.u32(file.streaming ? 0 : entry.crc);
|
|
94
|
+
out.u32(file.streaming ? 0 : payload.length);
|
|
95
|
+
out.u32(file.streaming ? 0 : entry.declaredUncompressed);
|
|
96
|
+
out.u16(entry.nameBytes.length);
|
|
97
|
+
out.u16(0); // extra field length
|
|
98
|
+
out.raw(entry.nameBytes);
|
|
99
|
+
out.raw(payload);
|
|
100
|
+
|
|
101
|
+
if (file.streaming) {
|
|
102
|
+
out.u32(0x08074b50); // data descriptor signature (Go writes it)
|
|
103
|
+
out.u32(entry.crc);
|
|
104
|
+
out.u32(payload.length);
|
|
105
|
+
out.u32(entry.declaredUncompressed);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options?.omitCentralDirectory) {
|
|
110
|
+
return out.toUint8Array();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const centralDirectoryOffset = out.length;
|
|
114
|
+
for (const entry of written) {
|
|
115
|
+
out.u32(0x02014b50); // central directory record signature
|
|
116
|
+
out.u16(20); // version made by
|
|
117
|
+
out.u16(20); // version needed to extract
|
|
118
|
+
out.u16(entry.flags);
|
|
119
|
+
out.u16(entry.method);
|
|
120
|
+
out.u16(0); // mod time
|
|
121
|
+
out.u16(0); // mod date
|
|
122
|
+
out.u32(entry.crc);
|
|
123
|
+
out.u32(entry.payload.length);
|
|
124
|
+
out.u32(entry.declaredUncompressed);
|
|
125
|
+
out.u16(entry.nameBytes.length);
|
|
126
|
+
out.u16(0); // extra field length
|
|
127
|
+
out.u16(0); // comment length
|
|
128
|
+
out.u16(0); // disk number start
|
|
129
|
+
out.u16(0); // internal attributes
|
|
130
|
+
out.u32(0); // external attributes
|
|
131
|
+
out.u32(entry.localHeaderOffset);
|
|
132
|
+
out.raw(entry.nameBytes);
|
|
133
|
+
}
|
|
134
|
+
const centralDirectorySize = out.length - centralDirectoryOffset;
|
|
135
|
+
|
|
136
|
+
const commentBytes = new TextEncoder().encode(options?.comment ?? "");
|
|
137
|
+
out.u32(0x06054b50); // EOCD signature
|
|
138
|
+
out.u16(0); // disk number
|
|
139
|
+
out.u16(0); // disk with central directory
|
|
140
|
+
out.u16(written.length); // entries on this disk
|
|
141
|
+
out.u16(written.length); // total entries
|
|
142
|
+
out.u32(centralDirectorySize);
|
|
143
|
+
out.u32(centralDirectoryOffset);
|
|
144
|
+
out.u16(commentBytes.length);
|
|
145
|
+
out.raw(commentBytes);
|
|
146
|
+
|
|
147
|
+
return out.toUint8Array();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Chunked assembly instead of a number[]-per-byte accumulator: fixtures at
|
|
151
|
+
// the injector's 100 MB zip-bomb limit are built from payload-sized chunks,
|
|
152
|
+
// which a per-byte spread-push cannot survive (argument-count overflow).
|
|
153
|
+
class ByteWriter {
|
|
154
|
+
private readonly chunks: Uint8Array[] = [];
|
|
155
|
+
private size = 0;
|
|
156
|
+
|
|
157
|
+
get length(): number {
|
|
158
|
+
return this.size;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
u16(v: number): void {
|
|
162
|
+
this.raw(new Uint8Array([v & 0xff, (v >>> 8) & 0xff]));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
u32(v: number): void {
|
|
166
|
+
this.raw(new Uint8Array([v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff]));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
raw(b: Uint8Array): void {
|
|
170
|
+
this.chunks.push(b);
|
|
171
|
+
this.size += b.length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toUint8Array(): Uint8Array {
|
|
175
|
+
const result = new Uint8Array(this.size);
|
|
176
|
+
let offset = 0;
|
|
177
|
+
for (const chunk of this.chunks) {
|
|
178
|
+
result.set(chunk, offset);
|
|
179
|
+
offset += chunk.length;
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Standard CRC-32 (IEEE 802.3, the ZIP checksum), table-driven so fixtures
|
|
186
|
+
// at the injector's 100 MB limit stay cheap to build. Semantically identical
|
|
187
|
+
// to the conformance suite's inline bit-loop helper.
|
|
188
|
+
const CRC_TABLE = (() => {
|
|
189
|
+
const table = new Uint32Array(256);
|
|
190
|
+
for (let n = 0; n < 256; n++) {
|
|
191
|
+
let c = n;
|
|
192
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
193
|
+
c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
|
|
194
|
+
}
|
|
195
|
+
table[n] = c;
|
|
196
|
+
}
|
|
197
|
+
return table;
|
|
198
|
+
})();
|
|
199
|
+
|
|
200
|
+
function crc32(data: Uint8Array): number {
|
|
201
|
+
let crc = 0xffffffff;
|
|
202
|
+
for (let i = 0; i < data.length; i++) {
|
|
203
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ data[i]!) & 0xff]!;
|
|
204
|
+
}
|
|
205
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
206
|
+
}
|
|
@@ -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({
|
|
@@ -3,6 +3,7 @@ import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { resolveSkills } from "../skill-resolver.js";
|
|
6
|
+
import { buildZip } from "../../../__test-utils__/zip-fixtures.js";
|
|
6
7
|
|
|
7
8
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -10,46 +11,6 @@ function makeTempDir(prefix: string): string {
|
|
|
10
11
|
return mkdtempSync(join(tmpdir(), prefix));
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Build a minimal stored (method 0) ZIP archive for testing.
|
|
15
|
-
*/
|
|
16
|
-
function buildStoredZip(files: { name: string; content: string }[]): Uint8Array {
|
|
17
|
-
const parts: Uint8Array[] = [];
|
|
18
|
-
|
|
19
|
-
for (const file of files) {
|
|
20
|
-
const nameBytes = new TextEncoder().encode(file.name);
|
|
21
|
-
const contentBytes = new TextEncoder().encode(file.content);
|
|
22
|
-
const isDir = file.name.endsWith("/");
|
|
23
|
-
|
|
24
|
-
const header = new ArrayBuffer(30);
|
|
25
|
-
const view = new DataView(header);
|
|
26
|
-
view.setUint32(0, 0x04034b50, true);
|
|
27
|
-
view.setUint16(4, 20, true);
|
|
28
|
-
view.setUint16(6, 0, true);
|
|
29
|
-
view.setUint16(8, 0, true);
|
|
30
|
-
view.setUint16(10, 0, true);
|
|
31
|
-
view.setUint16(12, 0, true);
|
|
32
|
-
view.setUint32(14, 0, true);
|
|
33
|
-
view.setUint32(18, isDir ? 0 : contentBytes.length, true);
|
|
34
|
-
view.setUint32(22, isDir ? 0 : contentBytes.length, true);
|
|
35
|
-
view.setUint16(26, nameBytes.length, true);
|
|
36
|
-
view.setUint16(28, 0, true);
|
|
37
|
-
|
|
38
|
-
parts.push(new Uint8Array(header));
|
|
39
|
-
parts.push(nameBytes);
|
|
40
|
-
if (!isDir) parts.push(contentBytes);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
44
|
-
const result = new Uint8Array(totalLength);
|
|
45
|
-
let offset = 0;
|
|
46
|
-
for (const part of parts) {
|
|
47
|
-
result.set(part, offset);
|
|
48
|
-
offset += part.length;
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
14
|
function makeSkillProto(overrides: {
|
|
54
15
|
name?: string;
|
|
55
16
|
slug?: string;
|
|
@@ -141,7 +102,7 @@ describe("resolveSkills — artifact extraction", () => {
|
|
|
141
102
|
const skillMd = "# Garden Design Makeover\n\nSee [references/database-schema.md](references/database-schema.md)";
|
|
142
103
|
const schemaContent = "# Database Schema\n\nTable definitions here.";
|
|
143
104
|
|
|
144
|
-
const artifact =
|
|
105
|
+
const artifact = buildZip([
|
|
145
106
|
{ name: "SKILL.md", content: skillMd },
|
|
146
107
|
{ name: "references/", content: "" },
|
|
147
108
|
{ name: "references/database-schema.md", content: schemaContent },
|
|
@@ -232,7 +193,7 @@ describe("resolveSkills — artifact extraction", () => {
|
|
|
232
193
|
const specContent = "# Authoritative SKILL.md from spec";
|
|
233
194
|
const zipContent = "# Stale SKILL.md from ZIP";
|
|
234
195
|
|
|
235
|
-
const artifact =
|
|
196
|
+
const artifact = buildZip([
|
|
236
197
|
{ name: "SKILL.md", content: zipContent },
|
|
237
198
|
{ name: "references/data.md", content: "data" },
|
|
238
199
|
]);
|
|
@@ -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
|
};
|