@desplega.ai/agent-swarm 1.108.0 → 1.108.2
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/{actions-sdr91cfp.js → actions-1hxdkcp6.js} +6 -6
- package/dist/{app-0mcf7mwz.js → app-vn2sy15m.js} +3 -3
- package/dist/{assistant-7f3cmabp.js → assistant-sj71nx88.js} +9 -9
- package/dist/{boot-reembed-a9btk2eh.js → boot-reembed-nc1dpmdj.js} +4 -4
- package/dist/{boot-reembed-47cfre4w.js → boot-reembed-nw9vp9ez.js} +3 -3
- package/dist/{boot-scrub-logs-hy8pqhej.js → boot-scrub-logs-g2mpp0cg.js} +2 -2
- package/dist/{cli-j9eqkarv.js → cli-1brke24m.js} +3 -3
- package/dist/{cli-vemx13wc.js → cli-1wzrmxaj.js} +3 -3
- package/dist/{cli-h2qhdwhr.js → cli-27pxayz8.js} +1 -1
- package/dist/{cli-a0w6tqyc.js → cli-3p0e74xv.js} +1 -1
- package/dist/{cli-xy83cmfc.js → cli-4wrdrmr3.js} +33 -6
- package/dist/{cli-vhpa5j27.js → cli-5m4da9px.js} +1 -1
- package/dist/{cli-9wv4e7qs.js → cli-6jf8xfnt.js} +7 -3
- package/dist/{cli-1zs2dm5w.js → cli-6z0y1339.js} +4 -4
- package/dist/{cli-mkn2ryba.js → cli-8gtrpqcw.js} +1 -1
- package/dist/{cli-7r40bgvr.js → cli-8kssf4wv.js} +4 -4
- package/dist/{cli-nmt32dxp.js → cli-9zz27c9g.js} +1 -1
- package/dist/{cli-m0k99m2s.js → cli-ae9hy8yh.js} +1 -1
- package/dist/{cli-entbgsd5.js → cli-b4hwyzev.js} +1 -1
- package/dist/{cli-3cfvx630.js → cli-b7ytq2z8.js} +2 -2
- package/dist/{cli-a8gkr14c.js → cli-bz6qha8p.js} +30 -4
- package/dist/{cli-tehpf2fm.js → cli-cr2wze6h.js} +1 -1
- package/dist/{cli-psz2xq0n.js → cli-er5kffrx.js} +5 -5
- package/dist/{cli-mmaz2btq.js → cli-fxk0as0h.js} +1 -1
- package/dist/{cli-kppbkzj4.js → cli-gg6zxrx1.js} +5 -5
- package/dist/{cli-cya9knf9.js → cli-hc7hxy2m.js} +2 -2
- package/dist/{cli-75b0wmer.js → cli-nt60dmfm.js} +2 -2
- package/dist/{cli-apdrt1ch.js → cli-tc19ec02.js} +1 -1
- package/dist/{cli-gx7gnvha.js → cli-xekgq1r8.js} +17 -17
- package/dist/cli.js +8 -8
- package/dist/{commands-4p93q1rf.js → commands-11d7nc0g.js} +2 -2
- package/dist/{db-hb0chrmg.js → db-0r0wz8a4.js} +2 -2
- package/dist/{handlers-pz3zc83m.js → handlers-v8th05yh.js} +9 -9
- package/dist/{hook-mye8ket2.js → hook-9n0np62b.js} +1 -1
- package/dist/{http-3nrx1tp1.js → http-pmaqk60c.js} +73 -37
- package/dist/{index-x9n06ze1.js → index-35x6ptdt.js} +8 -8
- package/dist/{index-17n7z8k8.js → index-9vma4xmv.js} +9 -9
- package/dist/{index-6sgpkhby.js → index-c9tx34ke.js} +6 -6
- package/dist/{index-rys8ctm6.js → index-vgvvr6yk.js} +7 -7
- package/dist/{keepalive-b6eax0y0.js → keepalive-pjd1kjv7.js} +4 -4
- package/dist/{lead-v1g4j7he.js → lead-czd1xgxr.js} +17 -17
- package/dist/{maintenance-xwf1vf0v.js → maintenance-xjqt3ne7.js} +4 -4
- package/dist/{onboard-vy0z8b11.js → onboard-whf6w0mr.js} +2 -2
- package/dist/{otel-impl-94sm94jm.js → otel-impl-9n4p19tt.js} +1 -1
- package/dist/{pricing-refresh-an9bxjcd.js → pricing-refresh-wg80me14.js} +4 -4
- package/dist/{seed-pricing-3t22x5ax.js → seed-pricing-garwba27.js} +3 -3
- package/dist/{setup-8hd3jk8s.js → setup-dktff2q2.js} +2 -2
- package/dist/{worker-s0xm8naj.js → worker-t26gwn0n.js} +17 -17
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/plugin/skills/download-task-attachment/SKILL.md +59 -0
- package/src/be/seed/agent-fs-provision.ts +51 -4
- package/src/commands/runner.ts +40 -0
- package/src/commands/templates.ts +6 -1
- package/src/fs/local-fs-provider.ts +36 -4
- package/src/http/poll.ts +24 -2
- package/src/telemetry.ts +12 -2
- package/src/tests/agent-fs-provision-seeder.test.ts +204 -3
- package/src/tests/claude-adapter.test.ts +17 -3
- package/src/tests/fs-provider.test.ts +34 -0
- package/src/tests/http-api-integration.test.ts +51 -0
- package/src/tests/prompt-template-remaining.test.ts +25 -0
- package/src/tests/runner-attachments-section.test.ts +55 -0
- package/src/tests/telemetry-init.test.ts +23 -0
package/src/http/poll.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getInboxSummary,
|
|
17
17
|
getOfferedTasksForAgent,
|
|
18
18
|
getPendingTaskForAgent,
|
|
19
|
+
getTaskAttachments,
|
|
19
20
|
getTaskById,
|
|
20
21
|
getUnassignedTaskIds,
|
|
21
22
|
getUserById,
|
|
@@ -90,6 +91,23 @@ function getRequesterNotes(notes: string | undefined): string | undefined {
|
|
|
90
91
|
return typeof notes === "string" && notes.trim().length > 0 ? notes : undefined;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Slim attachment projection for the `task_assigned` poll trigger — just
|
|
96
|
+
* enough (id, name, mimeType, sizeBytes) for the worker to build a one-shot
|
|
97
|
+
* `/api/fs/tasks/{taskId}/files/{id}/raw` fetch recipe into the dispatch
|
|
98
|
+
* prompt, without shipping the full capabilities/provider blob over poll.
|
|
99
|
+
*/
|
|
100
|
+
function attachmentsForTrigger(
|
|
101
|
+
taskId: string,
|
|
102
|
+
): Array<{ id: string; name: string; mimeType?: string; sizeBytes?: number }> {
|
|
103
|
+
return getTaskAttachments(taskId).map((a) => ({
|
|
104
|
+
id: a.id,
|
|
105
|
+
name: a.name,
|
|
106
|
+
mimeType: a.mimeType,
|
|
107
|
+
sizeBytes: a.sizeBytes,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
// ─── Cursor Commit Endpoint ─────────────────────────────────────────────────
|
|
94
112
|
|
|
95
113
|
const commitCursorsRoute = route({
|
|
@@ -266,7 +284,11 @@ export async function handlePoll(
|
|
|
266
284
|
trigger: {
|
|
267
285
|
type: "task_assigned",
|
|
268
286
|
taskId: pendingTask.id,
|
|
269
|
-
task: {
|
|
287
|
+
task: {
|
|
288
|
+
...pendingTask,
|
|
289
|
+
status: "in_progress",
|
|
290
|
+
attachments: attachmentsForTrigger(pendingTask.id),
|
|
291
|
+
},
|
|
270
292
|
...(requestedByUser && {
|
|
271
293
|
requestedBy: {
|
|
272
294
|
name: requestedByUser.name,
|
|
@@ -381,7 +403,7 @@ export async function handlePoll(
|
|
|
381
403
|
trigger: {
|
|
382
404
|
type: "task_assigned",
|
|
383
405
|
taskId: claimed.id,
|
|
384
|
-
task: claimed,
|
|
406
|
+
task: { ...claimed, attachments: attachmentsForTrigger(claimed.id) },
|
|
385
407
|
},
|
|
386
408
|
};
|
|
387
409
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -188,13 +188,23 @@ export function track(options: TrackOptions): void {
|
|
|
188
188
|
actor_anonymous_id: installationId,
|
|
189
189
|
properties: {
|
|
190
190
|
...(options.properties ?? {}),
|
|
191
|
-
// Cloud-cohort signal
|
|
191
|
+
// Cloud-cohort signal. Two independent signals OR'd together:
|
|
192
|
+
// `cachedIsCloud` (MCP_BASE_URL points at a host we own, resolved at
|
|
193
|
+
// init time) catches self-host operators who point their swarm at
|
|
194
|
+
// our managed MCP endpoint; `isCloudDeployment()` (SWARM_CLOUD env
|
|
195
|
+
// var, read fresh) catches hosted Swarm Cloud deployments, whose
|
|
196
|
+
// MCP_BASE_URL is the intra-compose `http://api:3013` address and so
|
|
197
|
+
// never matches a cloud hostname on its own — SWARM_CLOUD=true is the
|
|
198
|
+
// signal those deployments actually carry (see agent-swarm-internal's
|
|
199
|
+
// .env.personalization). Growth reporting keys off this exact field
|
|
200
|
+
// (`properties_json.is_cloud` in ClickHouse), so both cohorts must
|
|
201
|
+
// resolve true here, not just in `metadata.is_cloud` below.
|
|
192
202
|
// Placed at the top level of `properties_json` so ClickHouse can
|
|
193
203
|
// GROUP BY without descending into nested objects. Spread LAST so
|
|
194
204
|
// caller-supplied keys can never spoof the cohort classification.
|
|
195
205
|
// The hostname is intentionally NOT included — telemetry must stay
|
|
196
206
|
// anonymous, and the boolean is sufficient to split cloud vs self-host.
|
|
197
|
-
is_cloud: cachedIsCloud,
|
|
207
|
+
is_cloud: cachedIsCloud || isCloudDeployment(),
|
|
198
208
|
is_e2b: cachedIsE2b,
|
|
199
209
|
swarmVersion: pkg.version,
|
|
200
210
|
},
|
|
@@ -38,6 +38,12 @@ type RequestRecord = {
|
|
|
38
38
|
hasSignal: boolean;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
type StubMember = {
|
|
42
|
+
email?: string;
|
|
43
|
+
role?: string;
|
|
44
|
+
user?: { email?: string };
|
|
45
|
+
};
|
|
46
|
+
|
|
41
47
|
async function removeDbFiles(path: string): Promise<void> {
|
|
42
48
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
43
49
|
await unlink(path + suffix).catch(() => {});
|
|
@@ -58,7 +64,10 @@ function configValue(key: string): string | undefined {
|
|
|
58
64
|
return getSwarmConfigs({ scope: "global", key })[0]?.value;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
function createFetchStub(
|
|
67
|
+
function createFetchStub(
|
|
68
|
+
records: RequestRecord[],
|
|
69
|
+
options: { members?: StubMember[] } = {},
|
|
70
|
+
): typeof fetch {
|
|
62
71
|
return (async (input, init) => {
|
|
63
72
|
const url = new URL(String(input));
|
|
64
73
|
const method = init?.method ?? "GET";
|
|
@@ -111,6 +120,9 @@ function createFetchStub(records: RequestRecord[]): typeof fetch {
|
|
|
111
120
|
if (url.pathname === "/orgs/shared-org/drives" && method === "POST") {
|
|
112
121
|
return Response.json({ id: "shared-drive", name: "shared" }, { status: 201 });
|
|
113
122
|
}
|
|
123
|
+
if (url.pathname === "/orgs/shared-org/members" && method === "GET") {
|
|
124
|
+
return Response.json({ members: options.members ?? [] });
|
|
125
|
+
}
|
|
114
126
|
if (url.pathname === "/orgs/shared-org/members/invite" && method === "POST") {
|
|
115
127
|
return Response.json({ ok: true });
|
|
116
128
|
}
|
|
@@ -160,8 +172,8 @@ describe("agent-fs provisioning seeder", () => {
|
|
|
160
172
|
process.env.AGENT_FS_EMAIL_DOMAIN = "agents.example.test";
|
|
161
173
|
|
|
162
174
|
createUser({
|
|
163
|
-
name: "
|
|
164
|
-
email: `
|
|
175
|
+
name: "Designer User",
|
|
176
|
+
email: `designer-${suffix}@example.test`,
|
|
165
177
|
role: "Customer",
|
|
166
178
|
});
|
|
167
179
|
createUser({
|
|
@@ -169,6 +181,11 @@ describe("agent-fs provisioning seeder", () => {
|
|
|
169
181
|
email: `operator-${suffix}@example.test`,
|
|
170
182
|
role: "Operator",
|
|
171
183
|
});
|
|
184
|
+
createUser({
|
|
185
|
+
name: "Explicit Viewer User",
|
|
186
|
+
email: `viewer-${suffix}@example.test`,
|
|
187
|
+
role: "read-only contractor",
|
|
188
|
+
});
|
|
172
189
|
const records: RequestRecord[] = [];
|
|
173
190
|
setAgentFsProvisionFetchForTests(createFetchStub(records));
|
|
174
191
|
|
|
@@ -202,6 +219,7 @@ describe("agent-fs provisioning seeder", () => {
|
|
|
202
219
|
|
|
203
220
|
expect(invites).toEqual(
|
|
204
221
|
[
|
|
222
|
+
{ email: `designer-${suffix}@example.test`, role: "editor" },
|
|
205
223
|
{ email: `operator-${suffix}@example.test`, role: "editor" },
|
|
206
224
|
{ email: `viewer-${suffix}@example.test`, role: "viewer" },
|
|
207
225
|
].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
@@ -215,6 +233,189 @@ describe("agent-fs provisioning seeder", () => {
|
|
|
215
233
|
expect(records).toEqual([]);
|
|
216
234
|
});
|
|
217
235
|
|
|
236
|
+
test("classifies arbitrary non-viewer human roles as agent-fs editors", async () => {
|
|
237
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
238
|
+
process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
|
|
239
|
+
process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
|
|
240
|
+
upsertSwarmConfig({
|
|
241
|
+
scope: "global",
|
|
242
|
+
key: "API_AGENT_FS_API_KEY",
|
|
243
|
+
value: "afs-admin-key",
|
|
244
|
+
isSecret: true,
|
|
245
|
+
});
|
|
246
|
+
upsertSwarmConfig({
|
|
247
|
+
scope: "global",
|
|
248
|
+
key: "AGENT_FS_DEFAULT_ORG_ID",
|
|
249
|
+
value: "shared-org",
|
|
250
|
+
});
|
|
251
|
+
upsertSwarmConfig({
|
|
252
|
+
scope: "global",
|
|
253
|
+
key: "AGENT_FS_DEFAULT_DRIVE_ID",
|
|
254
|
+
value: "shared-drive",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
createUser({
|
|
258
|
+
name: "Taras Founder",
|
|
259
|
+
email: `taras-founder-${suffix}@example.test`,
|
|
260
|
+
role: "co-founder, CTO",
|
|
261
|
+
});
|
|
262
|
+
createUser({
|
|
263
|
+
name: "Eze Founder",
|
|
264
|
+
email: `eze-founder-${suffix}@example.test`,
|
|
265
|
+
role: "co-founder, CEO",
|
|
266
|
+
});
|
|
267
|
+
createUser({
|
|
268
|
+
name: "Designer Human",
|
|
269
|
+
email: `designer-${suffix}@example.test`,
|
|
270
|
+
role: "designer",
|
|
271
|
+
});
|
|
272
|
+
createUser({
|
|
273
|
+
name: "Unknown Human",
|
|
274
|
+
email: `whatever-${suffix}@example.test`,
|
|
275
|
+
role: "whatever",
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const records: RequestRecord[] = [];
|
|
279
|
+
setAgentFsProvisionFetchForTests(createFetchStub(records));
|
|
280
|
+
|
|
281
|
+
const result = await runSeeder(agentFsProvisionSeeder, { quiet: true });
|
|
282
|
+
|
|
283
|
+
expect(result.failed).toEqual([]);
|
|
284
|
+
const invites = records
|
|
285
|
+
.filter((r) => r.path === "/orgs/shared-org/members/invite")
|
|
286
|
+
.map((r) => r.body);
|
|
287
|
+
expect(invites).toContainEqual({
|
|
288
|
+
email: `taras-founder-${suffix}@example.test`,
|
|
289
|
+
role: "editor",
|
|
290
|
+
});
|
|
291
|
+
expect(invites).toContainEqual({
|
|
292
|
+
email: `eze-founder-${suffix}@example.test`,
|
|
293
|
+
role: "editor",
|
|
294
|
+
});
|
|
295
|
+
expect(invites).toContainEqual({
|
|
296
|
+
email: `designer-${suffix}@example.test`,
|
|
297
|
+
role: "editor",
|
|
298
|
+
});
|
|
299
|
+
expect(invites).toContainEqual({
|
|
300
|
+
email: `whatever-${suffix}@example.test`,
|
|
301
|
+
role: "editor",
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("classifies explicitly viewer human roles as agent-fs viewers", async () => {
|
|
306
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
307
|
+
process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
|
|
308
|
+
process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
|
|
309
|
+
upsertSwarmConfig({
|
|
310
|
+
scope: "global",
|
|
311
|
+
key: "API_AGENT_FS_API_KEY",
|
|
312
|
+
value: "afs-admin-key",
|
|
313
|
+
isSecret: true,
|
|
314
|
+
});
|
|
315
|
+
upsertSwarmConfig({
|
|
316
|
+
scope: "global",
|
|
317
|
+
key: "AGENT_FS_DEFAULT_ORG_ID",
|
|
318
|
+
value: "shared-org",
|
|
319
|
+
});
|
|
320
|
+
upsertSwarmConfig({
|
|
321
|
+
scope: "global",
|
|
322
|
+
key: "AGENT_FS_DEFAULT_DRIVE_ID",
|
|
323
|
+
value: "shared-drive",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
createUser({
|
|
327
|
+
name: "Viewer Human",
|
|
328
|
+
email: `viewer-${suffix}@example.test`,
|
|
329
|
+
role: "Viewer",
|
|
330
|
+
});
|
|
331
|
+
createUser({
|
|
332
|
+
name: "Read Only Human",
|
|
333
|
+
email: `read-only-${suffix}@example.test`,
|
|
334
|
+
role: "read only",
|
|
335
|
+
});
|
|
336
|
+
createUser({
|
|
337
|
+
name: "Guest Human",
|
|
338
|
+
email: `guest-${suffix}@example.test`,
|
|
339
|
+
role: "guest",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const records: RequestRecord[] = [];
|
|
343
|
+
setAgentFsProvisionFetchForTests(createFetchStub(records));
|
|
344
|
+
|
|
345
|
+
const result = await runSeeder(agentFsProvisionSeeder, { quiet: true });
|
|
346
|
+
|
|
347
|
+
expect(result.failed).toEqual([]);
|
|
348
|
+
const invites = records
|
|
349
|
+
.filter((r) => r.path === "/orgs/shared-org/members/invite")
|
|
350
|
+
.map((r) => r.body);
|
|
351
|
+
expect(invites).toContainEqual({
|
|
352
|
+
email: `viewer-${suffix}@example.test`,
|
|
353
|
+
role: "viewer",
|
|
354
|
+
});
|
|
355
|
+
expect(invites).toContainEqual({
|
|
356
|
+
email: `read-only-${suffix}@example.test`,
|
|
357
|
+
role: "viewer",
|
|
358
|
+
});
|
|
359
|
+
expect(invites).toContainEqual({
|
|
360
|
+
email: `guest-${suffix}@example.test`,
|
|
361
|
+
role: "viewer",
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("does not invite existing members when provisioning would keep or lower their role", async () => {
|
|
366
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
367
|
+
const existingAdmin = `existing-admin-${suffix}@example.test`;
|
|
368
|
+
const existingEditor = `existing-editor-${suffix}@example.test`;
|
|
369
|
+
const existingViewer = `existing-viewer-${suffix}@example.test`;
|
|
370
|
+
const explicitViewer = `explicit-viewer-${suffix}@example.test`;
|
|
371
|
+
process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
|
|
372
|
+
process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
|
|
373
|
+
upsertSwarmConfig({
|
|
374
|
+
scope: "global",
|
|
375
|
+
key: "API_AGENT_FS_API_KEY",
|
|
376
|
+
value: "afs-admin-key",
|
|
377
|
+
isSecret: true,
|
|
378
|
+
});
|
|
379
|
+
upsertSwarmConfig({
|
|
380
|
+
scope: "global",
|
|
381
|
+
key: "AGENT_FS_DEFAULT_ORG_ID",
|
|
382
|
+
value: "shared-org",
|
|
383
|
+
});
|
|
384
|
+
upsertSwarmConfig({
|
|
385
|
+
scope: "global",
|
|
386
|
+
key: "AGENT_FS_DEFAULT_DRIVE_ID",
|
|
387
|
+
value: "shared-drive",
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
createUser({ name: "Existing Admin", email: existingAdmin, role: "Customer" });
|
|
391
|
+
createUser({ name: "Existing Editor", email: existingEditor, role: "Designer" });
|
|
392
|
+
createUser({ name: "Existing Viewer", email: existingViewer, role: "Whatever" });
|
|
393
|
+
createUser({ name: "Explicit Viewer", email: explicitViewer, role: "Guest" });
|
|
394
|
+
|
|
395
|
+
const records: RequestRecord[] = [];
|
|
396
|
+
setAgentFsProvisionFetchForTests(
|
|
397
|
+
createFetchStub(records, {
|
|
398
|
+
members: [
|
|
399
|
+
{ email: existingAdmin, role: "admin" },
|
|
400
|
+
{ user: { email: existingEditor }, role: "editor" },
|
|
401
|
+
{ email: existingViewer, role: "viewer" },
|
|
402
|
+
],
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const result = await runSeeder(agentFsProvisionSeeder, { quiet: true });
|
|
407
|
+
|
|
408
|
+
expect(result.failed).toEqual([]);
|
|
409
|
+
const inviteBodies = records
|
|
410
|
+
.filter((r) => r.path === "/orgs/shared-org/members/invite")
|
|
411
|
+
.map((r) => r.body);
|
|
412
|
+
|
|
413
|
+
expect(inviteBodies).not.toContainEqual({ email: existingAdmin, role: "editor" });
|
|
414
|
+
expect(inviteBodies).not.toContainEqual({ email: existingEditor, role: "editor" });
|
|
415
|
+
expect(inviteBodies).toContainEqual({ email: existingViewer, role: "editor" });
|
|
416
|
+
expect(inviteBodies).toContainEqual({ email: explicitViewer, role: "viewer" });
|
|
417
|
+
});
|
|
418
|
+
|
|
218
419
|
test("provisions agent-scoped credentials through the API-owned bootstrap key", async () => {
|
|
219
420
|
const suffix = crypto.randomUUID().slice(0, 8);
|
|
220
421
|
process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
|
|
@@ -86,6 +86,16 @@ describe("ClaudeSession spawn env — reasoning_effort", () => {
|
|
|
86
86
|
let spawnSpy: ReturnType<typeof spyOn>;
|
|
87
87
|
let spawnedEnvs: Array<Record<string, string> | undefined>;
|
|
88
88
|
|
|
89
|
+
// `createSession`/`ClaudeSession` fall back to `process.env` when `config.env`
|
|
90
|
+
// is omitted, and spread it wholesale into the spawned process env. Two
|
|
91
|
+
// problems follow in a credential-less sandbox: (1) `validateClaudeCredentials`
|
|
92
|
+
// throws without CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEY, and (2) any ambient
|
|
93
|
+
// env var sharing a name with what these tests assert on (CLAUDE_CODE_EFFORT_LEVEL,
|
|
94
|
+
// MAX_THINKING_TOKENS) would leak through and pollute the `toBeUndefined()`
|
|
95
|
+
// assertions. Passing an explicit, minimal `env` sidesteps both — deterministic
|
|
96
|
+
// regardless of the ambient process env the test happens to run in.
|
|
97
|
+
const CLEAN_ENV: Record<string, string> = { CLAUDE_CODE_OAUTH_TOKEN: "test-oauth-token" };
|
|
98
|
+
|
|
89
99
|
beforeEach(() => {
|
|
90
100
|
spawnedEnvs = [];
|
|
91
101
|
spawnSpy = spyOn(Bun, "spawn").mockImplementation(((
|
|
@@ -103,7 +113,9 @@ describe("ClaudeSession spawn env — reasoning_effort", () => {
|
|
|
103
113
|
|
|
104
114
|
test("reasoningEffort: 'high' on claude-opus-4-8 sets CLAUDE_CODE_EFFORT_LEVEL", async () => {
|
|
105
115
|
const adapter = new ClaudeAdapter();
|
|
106
|
-
await adapter.createSession(
|
|
116
|
+
await adapter.createSession(
|
|
117
|
+
makeConfig({ model: "claude-opus-4-8", reasoningEffort: "high", env: CLEAN_ENV }),
|
|
118
|
+
);
|
|
107
119
|
|
|
108
120
|
expect(spawnedEnvs).toHaveLength(1);
|
|
109
121
|
expect(spawnedEnvs[0]?.CLAUDE_CODE_EFFORT_LEVEL).toBe("high");
|
|
@@ -111,7 +123,9 @@ describe("ClaudeSession spawn env — reasoning_effort", () => {
|
|
|
111
123
|
|
|
112
124
|
test("reasoningEffort: 'off' on a legacy budget_tokens-capable model sets MAX_THINKING_TOKENS=0, no effort env", async () => {
|
|
113
125
|
const adapter = new ClaudeAdapter();
|
|
114
|
-
await adapter.createSession(
|
|
126
|
+
await adapter.createSession(
|
|
127
|
+
makeConfig({ model: "claude-opus-4-0", reasoningEffort: "off", env: CLEAN_ENV }),
|
|
128
|
+
);
|
|
115
129
|
|
|
116
130
|
expect(spawnedEnvs).toHaveLength(1);
|
|
117
131
|
expect(spawnedEnvs[0]?.MAX_THINKING_TOKENS).toBe("0");
|
|
@@ -120,7 +134,7 @@ describe("ClaudeSession spawn env — reasoning_effort", () => {
|
|
|
120
134
|
|
|
121
135
|
test("undefined reasoningEffort leaves spawn env unchanged (no effort/budget keys)", async () => {
|
|
122
136
|
const adapter = new ClaudeAdapter();
|
|
123
|
-
await adapter.createSession(makeConfig({ model: "claude-opus-4-8" }));
|
|
137
|
+
await adapter.createSession(makeConfig({ model: "claude-opus-4-8", env: CLEAN_ENV }));
|
|
124
138
|
|
|
125
139
|
expect(spawnedEnvs).toHaveLength(1);
|
|
126
140
|
expect(spawnedEnvs[0]?.CLAUDE_CODE_EFFORT_LEVEL).toBeUndefined();
|
|
@@ -85,6 +85,40 @@ describe("LocalFsProvider", () => {
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
test("head() returns the real upload Content-Type, not an extension guess from the storage key", async () => {
|
|
89
|
+
const rootDir = await mkdtemp(join(tmpdir(), "agent-swarm-fs-"));
|
|
90
|
+
try {
|
|
91
|
+
const provider = new LocalFsProvider({ rootDir });
|
|
92
|
+
// Storage key has no extension Bun can sniff correctly from — the JPEG bytes
|
|
93
|
+
// would otherwise be mis-reported (e.g. application/octet-stream or worse).
|
|
94
|
+
const scope = { taskId: "task-1", name: "attachment-blob" };
|
|
95
|
+
|
|
96
|
+
const uploaded = await provider.upload(scope, new Blob([new Uint8Array([1, 2, 3])]), {
|
|
97
|
+
contentType: "image/jpeg",
|
|
98
|
+
});
|
|
99
|
+
expect(uploaded.contentType).toBe("image/jpeg");
|
|
100
|
+
|
|
101
|
+
const head = await provider.head(scope);
|
|
102
|
+
expect(head.contentType).toBe("image/jpeg");
|
|
103
|
+
|
|
104
|
+
// copy() must carry the stored Content-Type forward too.
|
|
105
|
+
const copied = await provider.copy(scope, { taskId: "task-1", name: "attachment-copy" });
|
|
106
|
+
expect(copied.contentType).toBe("image/jpeg");
|
|
107
|
+
|
|
108
|
+
// The sidecar metadata file must not surface as a listed file.
|
|
109
|
+
const listed = await provider.list({ taskId: "task-1" });
|
|
110
|
+
expect(listed.map((item) => item.name).sort()).toEqual([
|
|
111
|
+
"attachment-blob",
|
|
112
|
+
"attachment-copy",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
await provider.delete(scope);
|
|
116
|
+
expect(await provider.exists(scope)).toBe(false);
|
|
117
|
+
} finally {
|
|
118
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
test("signedUploadUrl throws a normalized ReadOnly error", async () => {
|
|
89
123
|
const provider = new LocalFsProvider({
|
|
90
124
|
rootDir: await mkdtemp(join(tmpdir(), "agent-swarm-fs-")),
|
|
@@ -860,6 +860,57 @@ describe("Polling", () => {
|
|
|
860
860
|
expect(status).toBe(200);
|
|
861
861
|
expect(body).toBeDefined();
|
|
862
862
|
});
|
|
863
|
+
|
|
864
|
+
test("GET /api/poll — task_assigned trigger carries a slim attachments projection", async () => {
|
|
865
|
+
// Dedicated agent + task so this doesn't race other tests' fixtures for
|
|
866
|
+
// the shared worker agents.
|
|
867
|
+
const attachAgent = randomUUID();
|
|
868
|
+
const registered = await post("/api/agents", {
|
|
869
|
+
agentId: attachAgent,
|
|
870
|
+
body: { name: "AttachmentPollWorker" },
|
|
871
|
+
});
|
|
872
|
+
expect(registered.status).toBe(201);
|
|
873
|
+
|
|
874
|
+
const created = await post("/api/tasks", {
|
|
875
|
+
agentId: ids.leadAgent,
|
|
876
|
+
body: { task: "Task with an attachment", agentId: attachAgent },
|
|
877
|
+
});
|
|
878
|
+
expect(created.status).toBe(201);
|
|
879
|
+
const taskId = created.body.id as string;
|
|
880
|
+
|
|
881
|
+
// Upload via the real provider-agnostic route (same one PR #850 added) —
|
|
882
|
+
// this is the endpoint the injected fetch recipe below points at.
|
|
883
|
+
const upload = await fetch(`${BASE}/api/fs/tasks/${taskId}/files?name=IMG_1357.jpeg`, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: {
|
|
886
|
+
Authorization: `Bearer ${TEST_API_KEY}`,
|
|
887
|
+
"x-agent-id": ids.leadAgent,
|
|
888
|
+
"Content-Type": "image/jpeg",
|
|
889
|
+
},
|
|
890
|
+
body: Buffer.from("fake-jpeg-bytes"),
|
|
891
|
+
});
|
|
892
|
+
expect(upload.status).toBe(201);
|
|
893
|
+
const attachment = await upload.json();
|
|
894
|
+
|
|
895
|
+
const { status, body } = await get("/api/poll", { agentId: attachAgent });
|
|
896
|
+
expect(status).toBe(200);
|
|
897
|
+
expect(body.trigger?.type).toBe("task_assigned");
|
|
898
|
+
expect(body.trigger?.taskId).toBe(taskId);
|
|
899
|
+
|
|
900
|
+
const attachments = body.trigger?.task?.attachments;
|
|
901
|
+
expect(attachments).toHaveLength(1);
|
|
902
|
+
expect(attachments[0].id).toBe(attachment.id);
|
|
903
|
+
expect(attachments[0].name).toBe("IMG_1357.jpeg");
|
|
904
|
+
// NOTE: mimeType is whatever the provider's head() reports, which has a
|
|
905
|
+
// known pre-existing bug (local-fs infers it from the storage key rather
|
|
906
|
+
// than the upload's real Content-Type) — not asserted here, out of scope
|
|
907
|
+
// for this fix. What matters for the attachment-read fix is id/name.
|
|
908
|
+
expect(attachments[0].mimeType).toBe(attachment.mimeType);
|
|
909
|
+
// Slim projection — no need to ship the full capabilities/provider blob
|
|
910
|
+
// over every poll response.
|
|
911
|
+
expect(attachments[0]).not.toHaveProperty("capabilities");
|
|
912
|
+
expect(attachments[0]).not.toHaveProperty("providerKey");
|
|
913
|
+
});
|
|
863
914
|
});
|
|
864
915
|
|
|
865
916
|
// ===========================================================================
|
|
@@ -393,6 +393,31 @@ describe("Runner triggers — backward compatibility", () => {
|
|
|
393
393
|
|
|
394
394
|
Task: "Fix the bug"
|
|
395
395
|
|
|
396
|
+
When done, use \`store-progress\` with status: "completed" and include your output.`;
|
|
397
|
+
|
|
398
|
+
expect(result.text).toBe(expected);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("task.trigger.assigned threads attachments_section between task_desc_section and output_instructions", () => {
|
|
402
|
+
const result = resolveTemplate("task.trigger.assigned", {
|
|
403
|
+
work_on_task_cmd: "/work-on-task",
|
|
404
|
+
task_id: "abc123",
|
|
405
|
+
task_desc_section: '\n\nTask: "Read the attached file"',
|
|
406
|
+
attachments_section:
|
|
407
|
+
"\n\n📎 Attachment(s) — fetch directly, no need to discover the storage path yourself:\n- IMG_1357.jpeg: `curl ...`",
|
|
408
|
+
output_instructions:
|
|
409
|
+
'\n\nWhen done, use `store-progress` with status: "completed" and include your output.',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(result.skipped).toBe(false);
|
|
413
|
+
|
|
414
|
+
const expected = `/work-on-task abc123
|
|
415
|
+
|
|
416
|
+
Task: "Read the attached file"
|
|
417
|
+
|
|
418
|
+
📎 Attachment(s) — fetch directly, no need to discover the storage path yourself:
|
|
419
|
+
- IMG_1357.jpeg: \`curl ...\`
|
|
420
|
+
|
|
396
421
|
When done, use \`store-progress\` with status: "completed" and include your output.`;
|
|
397
422
|
|
|
398
423
|
expect(result.text).toBe(expected);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildAttachmentsSection } from "../commands/runner";
|
|
3
|
+
|
|
4
|
+
describe("buildAttachmentsSection", () => {
|
|
5
|
+
test("returns empty string when there are no attachments", () => {
|
|
6
|
+
expect(buildAttachmentsSection("task-1", undefined)).toBe("");
|
|
7
|
+
expect(buildAttachmentsSection("task-1", null)).toBe("");
|
|
8
|
+
expect(buildAttachmentsSection("task-1", [])).toBe("");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns empty string when taskId is missing", () => {
|
|
12
|
+
expect(buildAttachmentsSection(undefined, [{ id: "att-1", name: "photo.jpeg" }])).toBe("");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("builds a one-shot curl recipe against the provider-agnostic raw route", () => {
|
|
16
|
+
const section = buildAttachmentsSection("task-123", [
|
|
17
|
+
{ id: "att-1", name: "IMG_1357.jpeg", mimeType: "image/jpeg", sizeBytes: 2816227 },
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
expect(section).toContain("IMG_1357.jpeg");
|
|
21
|
+
expect(section).toContain("image/jpeg, 2816227 bytes");
|
|
22
|
+
expect(section).toContain(
|
|
23
|
+
'$MCP_BASE_URL/api/fs/tasks/task-123/files/att-1/raw" -o /tmp/IMG_1357.jpeg',
|
|
24
|
+
);
|
|
25
|
+
// No org/drive discovery should be required — the whole point of the fix.
|
|
26
|
+
expect(section).not.toContain("agent-fs");
|
|
27
|
+
expect(section).toContain("X-Agent-ID: $AGENT_ID");
|
|
28
|
+
expect(section).toContain("Authorization: Bearer ");
|
|
29
|
+
expect(section).toContain("AGENT_SWARM_API_KEY:-$API_KEY");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("handles multiple attachments, one line each", () => {
|
|
33
|
+
const section = buildAttachmentsSection("task-123", [
|
|
34
|
+
{ id: "att-1", name: "a.png" },
|
|
35
|
+
{ id: "att-2", name: "b.pdf" },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const lines = section.split("\n").filter((l) => l.startsWith("- "));
|
|
39
|
+
expect(lines).toHaveLength(2);
|
|
40
|
+
expect(lines[0]).toContain("a.png");
|
|
41
|
+
expect(lines[1]).toContain("b.pdf");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("skips malformed attachment entries without throwing", () => {
|
|
45
|
+
const section = buildAttachmentsSection("task-123", [
|
|
46
|
+
{ id: "att-1" }, // missing name — id used as fallback name
|
|
47
|
+
{ name: "no-id.txt" }, // missing id — dropped
|
|
48
|
+
"not-an-object",
|
|
49
|
+
null,
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
expect(section).toContain("att-1");
|
|
53
|
+
expect(section).not.toContain("no-id.txt");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -457,6 +457,29 @@ describe("initTelemetry", () => {
|
|
|
457
457
|
expect(properties.mcp_host).toBeUndefined();
|
|
458
458
|
});
|
|
459
459
|
|
|
460
|
+
test("hosted Swarm Cloud shape (intra-compose MCP_BASE_URL + SWARM_CLOUD=true) → properties.is_cloud=true", async () => {
|
|
461
|
+
// Mirrors agent-swarm-internal's Hetzner compose template: MCP_BASE_URL
|
|
462
|
+
// is the intra-compose service address (never a cloud hostname), and
|
|
463
|
+
// SWARM_CLOUD=true is seeded via .env.personalization. The hostname
|
|
464
|
+
// heuristic alone would misclassify this as self-host.
|
|
465
|
+
process.env.MCP_BASE_URL = "http://api:3013";
|
|
466
|
+
process.env.SWARM_CLOUD = "true";
|
|
467
|
+
await initTelemetry(
|
|
468
|
+
"api-server",
|
|
469
|
+
async () => "install_hosted_cloud_test",
|
|
470
|
+
async () => {},
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
track({ event: "server.started", properties: { port: 3013 } });
|
|
474
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
475
|
+
|
|
476
|
+
const properties = (captured as { properties: Record<string, unknown> }).properties;
|
|
477
|
+
expect(properties.is_cloud).toBe(true);
|
|
478
|
+
expect(properties.mcp_host).toBeUndefined();
|
|
479
|
+
|
|
480
|
+
delete process.env.SWARM_CLOUD;
|
|
481
|
+
});
|
|
482
|
+
|
|
460
483
|
test("caller properties cannot override is_cloud", async () => {
|
|
461
484
|
// Defense-in-depth: even if a caller passes through user-supplied
|
|
462
485
|
// values, the cohort signal shipped on every event must come from
|